]> git.tdb.fi Git - ext/openal.git/commitdiff
Import OpenAL Soft 1.23.1 sources
authorMikko Rasa <tdb@tdb.fi>
Mon, 11 Sep 2023 18:16:09 +0000 (21:16 +0300)
committerMikko Rasa <tdb@tdb.fi>
Mon, 11 Sep 2023 18:16:09 +0000 (21:16 +0300)
322 files changed:
.github/workflows/ci.yml [new file with mode: 0644]
.github/workflows/makemhr.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
BSD-3Clause [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
OpenALConfig.cmake.in [new file with mode: 0644]
README.md [new file with mode: 0644]
XCompile-Android.txt [new file with mode: 0644]
XCompile.txt [new file with mode: 0644]
al/auxeffectslot.cpp [new file with mode: 0644]
al/auxeffectslot.h [new file with mode: 0644]
al/buffer.cpp [new file with mode: 0644]
al/buffer.h [new file with mode: 0644]
al/eax/api.cpp [new file with mode: 0644]
al/eax/api.h [new file with mode: 0644]
al/eax/call.cpp [new file with mode: 0644]
al/eax/call.h [new file with mode: 0644]
al/eax/effect.h [new file with mode: 0644]
al/eax/exception.cpp [new file with mode: 0644]
al/eax/exception.h [new file with mode: 0644]
al/eax/fx_slot_index.cpp [new file with mode: 0644]
al/eax/fx_slot_index.h [new file with mode: 0644]
al/eax/fx_slots.cpp [new file with mode: 0644]
al/eax/fx_slots.h [new file with mode: 0644]
al/eax/globals.cpp [new file with mode: 0644]
al/eax/globals.h [new file with mode: 0644]
al/eax/utils.cpp [new file with mode: 0644]
al/eax/utils.h [new file with mode: 0644]
al/eax/x_ram.h [new file with mode: 0644]
al/effect.cpp [new file with mode: 0644]
al/effect.h [new file with mode: 0644]
al/effects/autowah.cpp [new file with mode: 0644]
al/effects/chorus.cpp [new file with mode: 0644]
al/effects/compressor.cpp [new file with mode: 0644]
al/effects/convolution.cpp [new file with mode: 0644]
al/effects/dedicated.cpp [new file with mode: 0644]
al/effects/distortion.cpp [new file with mode: 0644]
al/effects/echo.cpp [new file with mode: 0644]
al/effects/effects.cpp [new file with mode: 0644]
al/effects/effects.h [new file with mode: 0644]
al/effects/equalizer.cpp [new file with mode: 0644]
al/effects/fshifter.cpp [new file with mode: 0644]
al/effects/modulator.cpp [new file with mode: 0644]
al/effects/null.cpp [new file with mode: 0644]
al/effects/pshifter.cpp [new file with mode: 0644]
al/effects/reverb.cpp [new file with mode: 0644]
al/effects/vmorpher.cpp [new file with mode: 0644]
al/error.cpp [new file with mode: 0644]
al/event.cpp [new file with mode: 0644]
al/event.h [new file with mode: 0644]
al/extension.cpp [new file with mode: 0644]
al/filter.cpp [new file with mode: 0644]
al/filter.h [new file with mode: 0644]
al/listener.cpp [new file with mode: 0644]
al/listener.h [new file with mode: 0644]
al/source.cpp [new file with mode: 0644]
al/source.h [new file with mode: 0644]
al/state.cpp [new file with mode: 0644]
alc/alc.cpp [new file with mode: 0644]
alc/alconfig.cpp [new file with mode: 0644]
alc/alconfig.h [new file with mode: 0644]
alc/alu.cpp [new file with mode: 0644]
alc/alu.h [new file with mode: 0644]
alc/backends/alsa.cpp [new file with mode: 0644]
alc/backends/alsa.h [new file with mode: 0644]
alc/backends/base.cpp [new file with mode: 0644]
alc/backends/base.h [new file with mode: 0644]
alc/backends/coreaudio.cpp [new file with mode: 0644]
alc/backends/coreaudio.h [new file with mode: 0644]
alc/backends/dsound.cpp [new file with mode: 0644]
alc/backends/dsound.h [new file with mode: 0644]
alc/backends/jack.cpp [new file with mode: 0644]
alc/backends/jack.h [new file with mode: 0644]
alc/backends/loopback.cpp [new file with mode: 0644]
alc/backends/loopback.h [new file with mode: 0644]
alc/backends/null.cpp [new file with mode: 0644]
alc/backends/null.h [new file with mode: 0644]
alc/backends/oboe.cpp [new file with mode: 0644]
alc/backends/oboe.h [new file with mode: 0644]
alc/backends/opensl.cpp [new file with mode: 0644]
alc/backends/opensl.h [new file with mode: 0644]
alc/backends/oss.cpp [new file with mode: 0644]
alc/backends/oss.h [new file with mode: 0644]
alc/backends/pipewire.cpp [new file with mode: 0644]
alc/backends/pipewire.h [new file with mode: 0644]
alc/backends/portaudio.cpp [new file with mode: 0644]
alc/backends/portaudio.h [new file with mode: 0644]
alc/backends/pulseaudio.cpp [new file with mode: 0644]
alc/backends/pulseaudio.h [new file with mode: 0644]
alc/backends/sdl2.cpp [new file with mode: 0644]
alc/backends/sdl2.h [new file with mode: 0644]
alc/backends/sndio.cpp [new file with mode: 0644]
alc/backends/sndio.h [new file with mode: 0644]
alc/backends/solaris.cpp [new file with mode: 0644]
alc/backends/solaris.h [new file with mode: 0644]
alc/backends/wasapi.cpp [new file with mode: 0644]
alc/backends/wasapi.h [new file with mode: 0644]
alc/backends/wave.cpp [new file with mode: 0644]
alc/backends/wave.h [new file with mode: 0644]
alc/backends/winmm.cpp [new file with mode: 0644]
alc/backends/winmm.h [new file with mode: 0644]
alc/context.cpp [new file with mode: 0644]
alc/context.h [new file with mode: 0644]
alc/device.cpp [new file with mode: 0644]
alc/device.h [new file with mode: 0644]
alc/effects/autowah.cpp [new file with mode: 0644]
alc/effects/base.h [new file with mode: 0644]
alc/effects/chorus.cpp [new file with mode: 0644]
alc/effects/compressor.cpp [new file with mode: 0644]
alc/effects/convolution.cpp [new file with mode: 0644]
alc/effects/dedicated.cpp [new file with mode: 0644]
alc/effects/distortion.cpp [new file with mode: 0644]
alc/effects/echo.cpp [new file with mode: 0644]
alc/effects/equalizer.cpp [new file with mode: 0644]
alc/effects/fshifter.cpp [new file with mode: 0644]
alc/effects/modulator.cpp [new file with mode: 0644]
alc/effects/null.cpp [new file with mode: 0644]
alc/effects/pshifter.cpp [new file with mode: 0644]
alc/effects/reverb.cpp [new file with mode: 0644]
alc/effects/vmorpher.cpp [new file with mode: 0644]
alc/inprogext.h [new file with mode: 0644]
alc/panning.cpp [new file with mode: 0644]
alsoftrc.sample [new file with mode: 0644]
appveyor.yml [new file with mode: 0644]
cmake/FindALSA.cmake [new file with mode: 0644]
cmake/FindAudioIO.cmake [new file with mode: 0644]
cmake/FindFFmpeg.cmake [new file with mode: 0644]
cmake/FindJACK.cmake [new file with mode: 0644]
cmake/FindMySOFA.cmake [new file with mode: 0644]
cmake/FindOSS.cmake [new file with mode: 0644]
cmake/FindOboe.cmake [new file with mode: 0644]
cmake/FindOpenSL.cmake [new file with mode: 0644]
cmake/FindPortAudio.cmake [new file with mode: 0644]
cmake/FindPulseAudio.cmake [new file with mode: 0644]
cmake/FindSndFile.cmake [new file with mode: 0644]
cmake/FindSoundIO.cmake [new file with mode: 0644]
cmake/bin2h.script.cmake [new file with mode: 0644]
common/albit.h [new file with mode: 0644]
common/albyte.h [new file with mode: 0644]
common/alcomplex.cpp [new file with mode: 0644]
common/alcomplex.h [new file with mode: 0644]
common/aldeque.h [new file with mode: 0644]
common/alfstream.cpp [new file with mode: 0644]
common/alfstream.h [new file with mode: 0644]
common/almalloc.cpp [new file with mode: 0644]
common/almalloc.h [new file with mode: 0644]
common/alnumbers.h [new file with mode: 0644]
common/alnumeric.h [new file with mode: 0644]
common/aloptional.h [new file with mode: 0644]
common/alspan.h [new file with mode: 0644]
common/alstring.cpp [new file with mode: 0644]
common/alstring.h [new file with mode: 0644]
common/altraits.h [new file with mode: 0644]
common/atomic.h [new file with mode: 0644]
common/comptr.h [new file with mode: 0644]
common/dynload.cpp [new file with mode: 0644]
common/dynload.h [new file with mode: 0644]
common/intrusive_ptr.h [new file with mode: 0644]
common/opthelpers.h [new file with mode: 0644]
common/phase_shifter.h [new file with mode: 0644]
common/polyphase_resampler.cpp [new file with mode: 0644]
common/polyphase_resampler.h [new file with mode: 0644]
common/pragmadefs.h [new file with mode: 0644]
common/ringbuffer.cpp [new file with mode: 0644]
common/ringbuffer.h [new file with mode: 0644]
common/strutils.cpp [new file with mode: 0644]
common/strutils.h [new file with mode: 0644]
common/threads.cpp [new file with mode: 0644]
common/threads.h [new file with mode: 0644]
common/vecmat.h [new file with mode: 0644]
common/vector.h [new file with mode: 0644]
common/win_main_utf8.h [new file with mode: 0644]
config.h.in [new file with mode: 0644]
core/ambdec.cpp [new file with mode: 0644]
core/ambdec.h [new file with mode: 0644]
core/ambidefs.cpp [new file with mode: 0644]
core/ambidefs.h [new file with mode: 0644]
core/async_event.h [new file with mode: 0644]
core/bformatdec.cpp [new file with mode: 0644]
core/bformatdec.h [new file with mode: 0644]
core/bs2b.cpp [new file with mode: 0644]
core/bs2b.h [new file with mode: 0644]
core/bsinc_defs.h [new file with mode: 0644]
core/bsinc_tables.cpp [new file with mode: 0644]
core/bsinc_tables.h [new file with mode: 0644]
core/buffer_storage.cpp [new file with mode: 0644]
core/buffer_storage.h [new file with mode: 0644]
core/bufferline.h [new file with mode: 0644]
core/context.cpp [new file with mode: 0644]
core/context.h [new file with mode: 0644]
core/converter.cpp [new file with mode: 0644]
core/converter.h [new file with mode: 0644]
core/cpu_caps.cpp [new file with mode: 0644]
core/cpu_caps.h [new file with mode: 0644]
core/cubic_defs.h [new file with mode: 0644]
core/cubic_tables.cpp [new file with mode: 0644]
core/cubic_tables.h [new file with mode: 0644]
core/dbus_wrap.cpp [new file with mode: 0644]
core/dbus_wrap.h [new file with mode: 0644]
core/devformat.cpp [new file with mode: 0644]
core/devformat.h [new file with mode: 0644]
core/device.cpp [new file with mode: 0644]
core/device.h [new file with mode: 0644]
core/effects/base.h [new file with mode: 0644]
core/effectslot.cpp [new file with mode: 0644]
core/effectslot.h [new file with mode: 0644]
core/except.cpp [new file with mode: 0644]
core/except.h [new file with mode: 0644]
core/filters/biquad.cpp [new file with mode: 0644]
core/filters/biquad.h [new file with mode: 0644]
core/filters/nfc.cpp [new file with mode: 0644]
core/filters/nfc.h [new file with mode: 0644]
core/filters/splitter.cpp [new file with mode: 0644]
core/filters/splitter.h [new file with mode: 0644]
core/fmt_traits.cpp [new file with mode: 0644]
core/fmt_traits.h [new file with mode: 0644]
core/fpu_ctrl.cpp [new file with mode: 0644]
core/fpu_ctrl.h [new file with mode: 0644]
core/front_stablizer.h [new file with mode: 0644]
core/helpers.cpp [new file with mode: 0644]
core/helpers.h [new file with mode: 0644]
core/hrtf.cpp [new file with mode: 0644]
core/hrtf.h [new file with mode: 0644]
core/logging.cpp [new file with mode: 0644]
core/logging.h [new file with mode: 0644]
core/mastering.cpp [new file with mode: 0644]
core/mastering.h [new file with mode: 0644]
core/mixer.cpp [new file with mode: 0644]
core/mixer.h [new file with mode: 0644]
core/mixer/defs.h [new file with mode: 0644]
core/mixer/hrtfbase.h [new file with mode: 0644]
core/mixer/hrtfdefs.h [new file with mode: 0644]
core/mixer/mixer_c.cpp [new file with mode: 0644]
core/mixer/mixer_neon.cpp [new file with mode: 0644]
core/mixer/mixer_sse.cpp [new file with mode: 0644]
core/mixer/mixer_sse2.cpp [new file with mode: 0644]
core/mixer/mixer_sse3.cpp [new file with mode: 0644]
core/mixer/mixer_sse41.cpp [new file with mode: 0644]
core/resampler_limits.h [new file with mode: 0644]
core/rtkit.cpp [new file with mode: 0644]
core/rtkit.h [new file with mode: 0644]
core/uhjfilter.cpp [new file with mode: 0644]
core/uhjfilter.h [new file with mode: 0644]
core/uiddefs.cpp [new file with mode: 0644]
core/voice.cpp [new file with mode: 0644]
core/voice.h [new file with mode: 0644]
core/voice_change.h [new file with mode: 0644]
docs/3D7.1.txt [new file with mode: 0644]
docs/ambdec.txt [new file with mode: 0644]
docs/ambisonics.txt [new file with mode: 0644]
docs/env-vars.txt [new file with mode: 0644]
docs/hrtf.txt [new file with mode: 0644]
examples/alconvolve.c [new file with mode: 0644]
examples/alffplay.cpp [new file with mode: 0644]
examples/alhrtf.c [new file with mode: 0644]
examples/allatency.c [new file with mode: 0644]
examples/alloopback.c [new file with mode: 0644]
examples/almultireverb.c [new file with mode: 0644]
examples/alplay.c [new file with mode: 0644]
examples/alrecord.c [new file with mode: 0644]
examples/alreverb.c [new file with mode: 0644]
examples/alstream.c [new file with mode: 0644]
examples/alstreamcb.cpp [new file with mode: 0644]
examples/altonegen.c [new file with mode: 0644]
examples/common/alhelpers.c [new file with mode: 0644]
examples/common/alhelpers.h [new file with mode: 0644]
hrtf/Default HRTF.mhr [new file with mode: 0644]
include/AL/al.h [new file with mode: 0644]
include/AL/alc.h [new file with mode: 0644]
include/AL/alext.h [new file with mode: 0644]
include/AL/efx-creative.h [new file with mode: 0644]
include/AL/efx-presets.h [new file with mode: 0644]
include/AL/efx.h [new file with mode: 0644]
libopenal.version [new file with mode: 0644]
openal.pc.in [new file with mode: 0644]
presets/3D7.1.ambdec [new file with mode: 0644]
presets/hex-quad.ambdec [new file with mode: 0644]
presets/hexagon.ambdec [new file with mode: 0644]
presets/itu5.1-nocenter.ambdec [new file with mode: 0644]
presets/itu5.1.ambdec [new file with mode: 0644]
presets/presets.txt [new file with mode: 0644]
presets/rectangle.ambdec [new file with mode: 0644]
presets/square.ambdec [new file with mode: 0644]
resources/openal32.rc [new file with mode: 0644]
resources/resource.h [new file with mode: 0644]
resources/router.rc [new file with mode: 0644]
resources/soft_oal.rc [new file with mode: 0644]
router/al.cpp [new file with mode: 0644]
router/alc.cpp [new file with mode: 0644]
router/router.cpp [new file with mode: 0644]
router/router.h [new file with mode: 0644]
utils/CIAIR.def [new file with mode: 0644]
utils/IRC_1005.def [new file with mode: 0644]
utils/MIT_KEMAR.def [new file with mode: 0644]
utils/MIT_KEMAR_sofa.def [new file with mode: 0644]
utils/SCUT_KEMAR.def [new file with mode: 0644]
utils/alsoft-config/CMakeLists.txt [new file with mode: 0644]
utils/alsoft-config/main.cpp [new file with mode: 0644]
utils/alsoft-config/mainwindow.cpp [new file with mode: 0644]
utils/alsoft-config/mainwindow.h [new file with mode: 0644]
utils/alsoft-config/mainwindow.ui [new file with mode: 0644]
utils/alsoft-config/verstr.cpp [new file with mode: 0644]
utils/alsoft-config/verstr.h [new file with mode: 0644]
utils/getopt.c [new file with mode: 0644]
utils/getopt.h [new file with mode: 0644]
utils/makemhr/loaddef.cpp [new file with mode: 0644]
utils/makemhr/loaddef.h [new file with mode: 0644]
utils/makemhr/loadsofa.cpp [new file with mode: 0644]
utils/makemhr/loadsofa.h [new file with mode: 0644]
utils/makemhr/makemhr.cpp [new file with mode: 0644]
utils/makemhr/makemhr.h [new file with mode: 0644]
utils/openal-info.c [new file with mode: 0644]
utils/sofa-info.cpp [new file with mode: 0644]
utils/sofa-support.cpp [new file with mode: 0644]
utils/sofa-support.h [new file with mode: 0644]
utils/uhjdecoder.cpp [new file with mode: 0644]
utils/uhjencoder.cpp [new file with mode: 0644]
version.cmake [new file with mode: 0644]
version.h.in [new file with mode: 0644]

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644 (file)
index 0000000..1a94c76
--- /dev/null
@@ -0,0 +1,116 @@
+name: CI
+
+on: [push]
+
+jobs:
+  build:
+    name: ${{matrix.config.name}}
+    runs-on: ${{matrix.config.os}}
+    strategy:
+      fail-fast: false
+      matrix:
+        config:
+        - {
+            name: "Win32-Release",
+            os: windows-latest,
+            cmake_opts: "-A Win32 \
+              -DALSOFT_BUILD_ROUTER=ON \
+              -DALSOFT_REQUIRE_WINMM=ON \
+              -DALSOFT_REQUIRE_DSOUND=ON \
+              -DALSOFT_REQUIRE_WASAPI=ON",
+            build_type: "Release"
+          }
+        - {
+            name: "Win32-Debug",
+            os: windows-latest,
+            cmake_opts: "-A Win32 \
+              -DALSOFT_BUILD_ROUTER=ON \
+              -DALSOFT_REQUIRE_WINMM=ON \
+              -DALSOFT_REQUIRE_DSOUND=ON \
+              -DALSOFT_REQUIRE_WASAPI=ON",
+            build_type: "Debug"
+          }
+        - {
+            name: "Win64-Release",
+            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: "Win64-Debug",
+            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: "Debug"
+          }
+        - {
+            name: "macOS-Release",
+            os: macos-latest,
+            cmake_opts: "-DALSOFT_REQUIRE_COREAUDIO=ON",
+            build_type: "Release"
+          }
+        - {
+            name: "Linux-Release",
+            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 \
+              -DALSOFT_REQUIRE_PIPEWIRE=ON",
+            deps_cmdline: "sudo apt update && sudo apt-get install -qq \
+              libpulse-dev \
+              portaudio19-dev \
+              libasound2-dev \
+              libjack-dev \
+              libpipewire-0.3-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}}
+
+    - name: Create Archive
+      if: ${{ matrix.config.os == 'windows-latest' }}
+      shell: bash
+      run: |
+        cd build
+        mkdir archive
+        mkdir archive/router
+        cp ${{matrix.config.build_type}}/soft_oal.dll archive
+        cp ${{matrix.config.build_type}}/OpenAL32.dll archive/router
+
+    - name: Upload Archive
+      # Upload package as an artifact of this workflow.
+      uses: actions/upload-artifact@v3.1.1
+      if: ${{ matrix.config.os == 'windows-latest' }}
+      with:
+        name: soft_oal-${{matrix.config.name}}
+        path: build/archive
diff --git a/.github/workflows/makemhr.yml b/.github/workflows/makemhr.yml
new file mode 100644 (file)
index 0000000..7bde284
--- /dev/null
@@ -0,0 +1,76 @@
+name: makemhr
+
+on:
+  push:
+    paths:
+      - 'utils/makemhr/**'
+      - '.github/workflows/makemhr.yml'
+
+  workflow_dispatch:
+
+env:
+  BUILD_TYPE: Release
+
+jobs:
+  Win64:
+    runs-on: windows-latest
+
+    steps:
+    - uses: actions/checkout@v3
+
+    - name: Get current date
+      run: echo "CurrentDate=$(date +'%Y-%m-%d')" >> $env:GITHUB_ENV
+
+    - name: Get commit hash
+      run: echo "CommitHash=$(git rev-parse --short=7 HEAD)" >> $env:GITHUB_ENV
+
+    - name: Clone libmysofa
+      run: git clone --depth 1 --branch v1.3.1 https://github.com/hoene/libmysofa.git libmysofa
+
+    - name: Add MSBuild to PATH
+      uses: microsoft/setup-msbuild@v1.1.3
+
+    - name: Restore libmysofa NuGet packages
+      working-directory: ${{github.workspace}}/libmysofa
+      run: nuget restore ${{github.workspace}}/libmysofa/windows/libmysofa.sln
+
+    - name: Build libmysofa
+      working-directory: ${{github.workspace}}/libmysofa
+      run: msbuild /m /p:Configuration=${{env.BUILD_TYPE}} ${{github.workspace}}/libmysofa/windows/libmysofa.sln
+
+    - name: Configure CMake
+      run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -D "MYSOFA_LIBRARY=${{github.workspace}}/libmysofa/windows/bin/x64/Release/mysofa.lib" -D "MYSOFA_INCLUDE_DIR=${{github.workspace}}/libmysofa/src/hrtf" -D "ZLIB_LIBRARY=${{github.workspace}}/libmysofa/windows/third-party/zlib-1.2.11/lib/zlib.lib" -D "ZLIB_INCLUDE_DIR=${{github.workspace}}/libmysofa/windows/third-party/zlib-1.2.11/include"
+
+    - name: Build
+      run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
+
+    - name: Make Artifacts folder
+      run: |
+        mkdir "Artifacts"
+        mkdir "Release"
+
+    - name: Collect artifacts
+      run: |
+        copy "build/Release/makemhr.exe" "Artifacts/makemhr.exe"
+        copy "libmysofa/windows/third-party/zlib-1.2.11/bin/zlib.dll" "Artifacts/zlib.dll"
+
+    - name: Upload makemhr artifact
+      uses: actions/upload-artifact@v3.1.1
+      with:
+        name: makemhr
+        path: "Artifacts/"
+
+    - name: Compress artifacts
+      uses: papeloto/action-zip@v1
+      with:
+        files: Artifacts/
+        dest: "Release/makemhr.zip"
+
+    - name: GitHub pre-release
+      uses: "marvinpinto/action-automatic-releases@latest"
+      with:
+        repo_token: "${{secrets.GITHUB_TOKEN}}"
+        automatic_release_tag: "makemhr"
+        prerelease: true
+        title: "[${{env.CurrentDate}}] makemhr-${{env.CommitHash}}"
+        files: "Release/makemhr.zip"
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..4a8212b
--- /dev/null
@@ -0,0 +1,9 @@
+build*/
+winbuild
+win64build
+
+## kdevelop
+*.kdev4
+
+## qt-creator
+CMakeLists.txt.user*
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..f85da59
--- /dev/null
@@ -0,0 +1,125 @@
+language: cpp
+matrix:
+  include:
+    - os: linux
+      dist: xenial
+    - os: linux
+      dist: trusty
+      env:
+        - BUILD_ANDROID=true
+    - os: freebsd
+      compiler: clang
+    - os: osx
+    - os: osx
+      osx_image: xcode11
+      env:
+        - BUILD_IOS=true
+sudo: required
+install:
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "linux" && -z "${BUILD_ANDROID}" ]]; then
+      # Install pulseaudio, portaudio, ALSA, JACK dependencies for
+      # corresponding backends.
+      # Install Qt5 dependency for alsoft-config.
+      sudo apt-get install -qq \
+        libpulse-dev \
+        portaudio19-dev \
+        libasound2-dev \
+        libjack-dev \
+        qtbase5-dev \
+        libdbus-1-dev
+    fi
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "linux" && "${BUILD_ANDROID}" == "true" ]]; then
+      curl -o ~/android-ndk.zip https://dl.google.com/android/repository/android-ndk-r21-linux-x86_64.zip
+      unzip -q ~/android-ndk.zip -d ~ \
+        'android-ndk-r21/build/cmake/*' \
+        'android-ndk-r21/build/core/toolchains/arm-linux-androideabi-*/*' \
+        'android-ndk-r21/platforms/android-16/arch-arm/*' \
+        'android-ndk-r21/source.properties' \
+        'android-ndk-r21/sources/android/support/include/*' \
+        'android-ndk-r21/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/*' \
+        'android-ndk-r21/sources/cxx-stl/llvm-libc++/include/*' \
+        'android-ndk-r21/sysroot/*' \
+        'android-ndk-r21/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/*' \
+        'android-ndk-r21/toolchains/llvm/prebuilt/linux-x86_64/*'
+      export OBOE_LOC=~/oboe
+      git clone --depth 1 -b 1.3-stable https://github.com/google/oboe "$OBOE_LOC"
+    fi
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "freebsd" ]]; then
+      # Install Ninja as it's used downstream.
+      # Install dependencies for all supported backends.
+      # Install Qt5 dependency for alsoft-config.
+      # Install ffmpeg for examples.
+      sudo pkg install -y \
+        alsa-lib \
+        ffmpeg \
+        jackit \
+        libmysofa \
+        ninja \
+        portaudio \
+        pulseaudio \
+        qt5-buildtools \
+        qt5-qmake \
+        qt5-widgets \
+        sdl2 \
+        sndio \
+        $NULL
+    fi
+script:
+  - cmake --version
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "linux" && -z "${BUILD_ANDROID}" ]]; then
+      cmake \
+        -DALSOFT_REQUIRE_ALSA=ON \
+        -DALSOFT_REQUIRE_OSS=ON \
+        -DALSOFT_REQUIRE_PORTAUDIO=ON \
+        -DALSOFT_REQUIRE_PULSEAUDIO=ON \
+        -DALSOFT_REQUIRE_JACK=ON \
+        -DALSOFT_EMBED_HRTF_DATA=YES \
+        .
+    fi
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "linux" && "${BUILD_ANDROID}" == "true" ]]; then
+      cmake \
+        -DANDROID_STL=c++_shared \
+        -DCMAKE_TOOLCHAIN_FILE=~/android-ndk-r21/build/cmake/android.toolchain.cmake \
+        -DOBOE_SOURCE="$OBOE_LOC" \
+        -DALSOFT_REQUIRE_OBOE=ON \
+        -DALSOFT_REQUIRE_OPENSL=ON \
+        -DALSOFT_EMBED_HRTF_DATA=YES \
+        .
+    fi
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "freebsd" ]]; then
+      cmake -GNinja \
+        -DALSOFT_REQUIRE_ALSA=ON \
+        -DALSOFT_REQUIRE_JACK=ON \
+        -DALSOFT_REQUIRE_OSS=ON \
+        -DALSOFT_REQUIRE_PORTAUDIO=ON \
+        -DALSOFT_REQUIRE_PULSEAUDIO=ON \
+        -DALSOFT_REQUIRE_SDL2=ON \
+        -DALSOFT_REQUIRE_SNDIO=ON \
+        -DALSOFT_EMBED_HRTF_DATA=YES \
+        .
+    fi
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "osx" && -z "${BUILD_IOS}" ]]; then
+      cmake \
+        -DALSOFT_REQUIRE_COREAUDIO=ON \
+        -DALSOFT_EMBED_HRTF_DATA=YES \
+        .
+    fi
+  - >
+    if [[ "${TRAVIS_OS_NAME}" == "osx" && "${BUILD_IOS}" == "true" ]]; then
+      cmake \
+        -GXcode \
+        -DCMAKE_SYSTEM_NAME=iOS \
+        -DALSOFT_OSX_FRAMEWORK=ON \
+        -DALSOFT_REQUIRE_COREAUDIO=ON \
+        -DALSOFT_EMBED_HRTF_DATA=YES \
+        "-DCMAKE_OSX_ARCHITECTURES=armv7;arm64" \
+        .
+    fi
+  - cmake --build . --clean-first
diff --git a/BSD-3Clause b/BSD-3Clause
new file mode 100644 (file)
index 0000000..b1c2dbd
--- /dev/null
@@ -0,0 +1,31 @@
+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.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of Spherical-Harmonic-Transform nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..55644b0
--- /dev/null
@@ -0,0 +1,1778 @@
+# CMake build file list for OpenAL
+
+cmake_minimum_required(VERSION 3.0.2)
+
+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")
+            message(STATUS "Forcing iOS deployment target to 10.0 for armv7")
+            set(CMAKE_OSX_DEPLOYMENT_TARGET "10.0" CACHE STRING "Minimum OS X deployment version"
+                FORCE)
+        endif()
+    endif()
+endif()
+
+set(CMAKE_C_VISIBILITY_PRESET hidden)
+set(CMAKE_CXX_VISIBILITY_PRESET hidden)
+
+if(COMMAND CMAKE_POLICY)
+    cmake_policy(SET CMP0003 NEW)
+    cmake_policy(SET CMP0005 NEW)
+    if(POLICY CMP0020)
+        cmake_policy(SET CMP0020 NEW)
+    endif(POLICY CMP0020)
+    if(POLICY CMP0042)
+        cmake_policy(SET CMP0042 NEW)
+    endif(POLICY CMP0042)
+    if(POLICY CMP0054)
+        cmake_policy(SET CMP0054 NEW)
+    endif(POLICY CMP0054)
+    if(POLICY CMP0058)
+        cmake_policy(SET CMP0058 NEW)
+    endif(POLICY CMP0058)
+    if(POLICY CMP0063)
+        cmake_policy(SET CMP0063 NEW)
+    endif(POLICY CMP0063)
+    if(POLICY CMP0075)
+        cmake_policy(SET CMP0075 NEW)
+    endif(POLICY CMP0075)
+    if(POLICY CMP0092)
+        cmake_policy(SET CMP0092 NEW)
+    endif(POLICY CMP0092)
+    if(POLICY CMP0117)
+        cmake_policy(SET CMP0117 NEW)
+    endif(POLICY CMP0117)
+endif(COMMAND CMAKE_POLICY)
+
+project(OpenAL)
+
+if(NOT CMAKE_BUILD_TYPE)
+    set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING
+        "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel."
+        FORCE)
+endif()
+if(NOT CMAKE_DEBUG_POSTFIX)
+    set(CMAKE_DEBUG_POSTFIX "" CACHE STRING
+        "Library postfix for debug builds. Normally left blank."
+        FORCE)
+endif()
+
+set(DEFAULT_TARGET_PROPS
+    # Require C++14.
+    CXX_STANDARD 14
+    CXX_STANDARD_REQUIRED TRUE
+    # Prefer C11, but support C99 and earlier when possible.
+    C_STANDARD 11)
+
+set(CMAKE_MODULE_PATH "${OpenAL_SOURCE_DIR}/cmake")
+
+include(CheckFunctionExists)
+include(CheckLibraryExists)
+include(CheckIncludeFile)
+include(CheckIncludeFiles)
+include(CheckSymbolExists)
+include(CheckCCompilerFlag)
+include(CheckCXXCompilerFlag)
+include(CheckCSourceCompiles)
+include(CheckCXXSourceCompiles)
+include(CheckStructHasMember)
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+find_package(PkgConfig)
+find_package(SDL2 QUIET)
+
+
+option(ALSOFT_DLOPEN  "Check for the dlopen API for loading optional libs"  ON)
+
+option(ALSOFT_WERROR  "Treat compile warnings as errors"      OFF)
+
+option(ALSOFT_UTILS "Build utility programs"  ON)
+option(ALSOFT_NO_CONFIG_UTIL "Disable building the alsoft-config utility" OFF)
+
+option(ALSOFT_EXAMPLES  "Build example programs"  ON)
+
+option(ALSOFT_INSTALL "Install main library" ON)
+option(ALSOFT_INSTALL_CONFIG "Install alsoft.conf sample configuration file" ON)
+option(ALSOFT_INSTALL_HRTF_DATA "Install HRTF data files" ON)
+option(ALSOFT_INSTALL_AMBDEC_PRESETS "Install AmbDec preset files" ON)
+option(ALSOFT_INSTALL_EXAMPLES "Install example programs (alplay, alstream, ...)" ON)
+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})
+
+option(ALSOFT_SEARCH_INSTALL_DATADIR "Search the installation data directory" OFF)
+if(ALSOFT_SEARCH_INSTALL_DATADIR)
+    set(ALSOFT_INSTALL_DATADIR ${CMAKE_INSTALL_FULL_DATADIR})
+endif()
+
+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}")
+endif()
+
+if(DEFINED LIB_SUFFIX)
+    message(WARNING "LIB_SUFFIX is deprecated.  Use the variables provided by the GNUInstallDirs module instead")
+endif()
+if(DEFINED ALSOFT_CONFIG)
+    message(WARNING "ALSOFT_CONFIG is deprecated. Use ALSOFT_INSTALL_CONFIG instead")
+endif()
+if(DEFINED ALSOFT_HRTF_DEFS)
+    message(WARNING "ALSOFT_HRTF_DEFS is deprecated. Use ALSOFT_INSTALL_HRTF_DATA instead")
+endif()
+if(DEFINED ALSOFT_AMBDEC_PRESETS)
+    message(WARNING "ALSOFT_AMBDEC_PRESETS is deprecated. Use ALSOFT_INSTALL_AMBDEC_PRESETS instead")
+endif()
+
+
+set(CPP_DEFS ) # C pre-processor, not C++
+set(INC_PATHS )
+set(C_FLAGS )
+set(LINKER_FLAGS )
+set(EXTRA_LIBS )
+
+if(WIN32)
+    set(CPP_DEFS ${CPP_DEFS} _WIN32 NOMINMAX)
+    if(MINGW)
+        set(CPP_DEFS ${CPP_DEFS} __USE_MINGW_ANSI_STDIO)
+    endif()
+
+    option(ALSOFT_BUILD_ROUTER  "Build the router (EXPERIMENTAL; creates OpenAL32.dll and soft_oal.dll)"  OFF)
+    if(MINGW)
+        option(ALSOFT_BUILD_IMPORT_LIB "Build an import .lib using dlltool (requires sed)" ON)
+    endif()
+elseif(APPLE)
+    option(ALSOFT_OSX_FRAMEWORK "Build as macOS framework" OFF)
+endif()
+
+
+# QNX's gcc do not uses /usr/include and /usr/lib pathes by default
+if("${CMAKE_C_PLATFORM_ID}" STREQUAL "QNX")
+    set(INC_PATHS ${INC_PATHS} /usr/include)
+    set(LINKER_FLAGS ${LINKER_FLAGS} -L/usr/lib)
+endif()
+
+# When the library is built for static linking, apps should define
+# AL_LIBTYPE_STATIC when including the AL headers.
+if(NOT LIBTYPE)
+    set(LIBTYPE SHARED)
+endif()
+
+set(LIB_MAJOR_VERSION "1")
+set(LIB_MINOR_VERSION "23")
+set(LIB_REVISION "1")
+set(LIB_VERSION "${LIB_MAJOR_VERSION}.${LIB_MINOR_VERSION}.${LIB_REVISION}")
+set(LIB_VERSION_NUM ${LIB_MAJOR_VERSION},${LIB_MINOR_VERSION},${LIB_REVISION},0)
+
+set(EXPORT_DECL "")
+
+
+if(NOT WIN32)
+    # Check if _POSIX_C_SOURCE and _XOPEN_SOURCE needs to be set for POSIX functions
+    check_symbol_exists(posix_memalign stdlib.h HAVE_POSIX_MEMALIGN_DEFAULT)
+    if(NOT HAVE_POSIX_MEMALIGN_DEFAULT)
+        set(OLD_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
+        set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -D_POSIX_C_SOURCE=200112L -D_XOPEN_SOURCE=600")
+        check_symbol_exists(posix_memalign stdlib.h HAVE_POSIX_MEMALIGN_POSIX)
+        if(NOT HAVE_POSIX_MEMALIGN_POSIX)
+            set(CMAKE_REQUIRED_FLAGS ${OLD_REQUIRED_FLAGS})
+        else()
+            set(CPP_DEFS ${CPP_DEFS} _POSIX_C_SOURCE=200112L _XOPEN_SOURCE=600)
+        endif()
+    endif()
+    unset(OLD_REQUIRED_FLAGS)
+endif()
+
+# C99 has restrict, but C++ does not, so we can only utilize __restrict.
+check_cxx_source_compiles("int *__restrict foo;
+int main() { return 0; }" HAVE___RESTRICT)
+if(HAVE___RESTRICT)
+    set(CPP_DEFS ${CPP_DEFS} RESTRICT=__restrict)
+else()
+    set(CPP_DEFS ${CPP_DEFS} "RESTRICT=")
+endif()
+
+# Some systems may need libatomic for atomic functions to work
+set(OLD_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES})
+set(CMAKE_REQUIRED_LIBRARIES ${OLD_REQUIRED_LIBRARIES} atomic)
+check_cxx_source_compiles("#include <atomic>
+std::atomic<int> foo{0};
+int main() { return foo.fetch_add(2); }"
+HAVE_LIBATOMIC)
+if(NOT HAVE_LIBATOMIC)
+    set(CMAKE_REQUIRED_LIBRARIES "${OLD_REQUIRED_LIBRARIES}")
+else()
+    set(EXTRA_LIBS atomic ${EXTRA_LIBS})
+endif()
+unset(OLD_REQUIRED_LIBRARIES)
+
+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)
+    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 /wd4127 /wd4268 /wd4324 /wd5030 /wd5051)
+
+    if(NOT DXSDK_DIR)
+        string(REGEX REPLACE "\\\\" "/" DXSDK_DIR "$ENV{DXSDK_DIR}")
+    else()
+        string(REGEX REPLACE "\\\\" "/" DXSDK_DIR "${DXSDK_DIR}")
+    endif()
+    if(DXSDK_DIR)
+        message(STATUS "Using DirectX SDK directory: ${DXSDK_DIR}")
+    endif()
+
+    option(FORCE_STATIC_VCRT "Force /MT for static VC runtimes" OFF)
+    if(FORCE_STATIC_VCRT)
+        foreach(flag_var
+                CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE
+                CMAKE_C_FLAGS_MINSIZEREL CMAKE_C_FLAGS_RELWITHDEBINFO
+                CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
+                CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
+            if(${flag_var} MATCHES "/MD")
+                string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
+            endif()
+        endforeach(flag_var)
+    endif()
+else()
+    set(C_FLAGS ${C_FLAGS} -Winline -Wunused -Wall -Wextra -Wshadow -Wconversion -Wcast-align
+        -Wpedantic
+        $<$<COMPILE_LANGUAGE:CXX>:-Wold-style-cast -Wnon-virtual-dtor -Woverloaded-virtual>)
+
+    check_cxx_compiler_flag(-Wno-c++20-attribute-extensions HAVE_WNO_CXX20_ATTR_EXT)
+    if(HAVE_WNO_CXX20_ATTR_EXT)
+        set(C_FLAGS ${C_FLAGS} $<$<COMPILE_LANGUAGE:CXX>:-Wno-c++20-attribute-extensions>)
+    else()
+        check_cxx_compiler_flag(-Wno-c++20-extensions HAVE_WNO_CXX20_EXT)
+        if(HAVE_WNO_CXX20_EXT)
+            set(C_FLAGS ${C_FLAGS} $<$<COMPILE_LANGUAGE:CXX>:-Wno-c++20-extensions>)
+        endif()
+    endif()
+
+    if(ALSOFT_WERROR)
+        set(C_FLAGS ${C_FLAGS} -Werror)
+    endif()
+
+    # We want RelWithDebInfo to actually include debug stuff (define _DEBUG
+    # instead of NDEBUG)
+    foreach(flag_var  CMAKE_C_FLAGS_RELWITHDEBINFO CMAKE_CXX_FLAGS_RELWITHDEBINFO)
+        if(${flag_var} MATCHES "-DNDEBUG")
+            string(REGEX REPLACE "-DNDEBUG" "-D_DEBUG" ${flag_var} "${${flag_var}}")
+        endif()
+    endforeach()
+
+    check_c_compiler_flag(-fno-math-errno HAVE_FNO_MATH_ERRNO)
+    if(HAVE_FNO_MATH_ERRNO)
+        set(C_FLAGS ${C_FLAGS} -fno-math-errno)
+    endif()
+
+    option(ALSOFT_STATIC_LIBGCC "Force -static-libgcc for static GCC runtimes" OFF)
+    if(ALSOFT_STATIC_LIBGCC)
+        set(OLD_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES})
+        set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} -static-libgcc)
+        check_cxx_source_compiles("int main() { }" HAVE_STATIC_LIBGCC_SWITCH)
+        set(CMAKE_REQUIRED_LIBRARIES ${OLD_REQUIRED_LIBRARIES})
+        unset(OLD_REQUIRED_LIBRARIES)
+
+        if(NOT HAVE_STATIC_LIBGCC_SWITCH)
+            message(FATAL_ERROR "Cannot static link libgcc")
+        endif()
+        set(LINKER_FLAGS ${LINKER_FLAGS} -static-libgcc)
+    endif()
+
+    option(ALSOFT_STATIC_STDCXX "Static link libstdc++" OFF)
+    if(ALSOFT_STATIC_STDCXX)
+        set(OLD_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES})
+        set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} "-Wl,--push-state,-Bstatic,-lstdc++,--pop-state")
+        check_cxx_source_compiles("int main() { }" HAVE_STATIC_LIBSTDCXX_SWITCH)
+        set(CMAKE_REQUIRED_LIBRARIES ${OLD_REQUIRED_LIBRARIES})
+        unset(OLD_REQUIRED_LIBRARIES)
+
+        if(NOT HAVE_STATIC_LIBSTDCXX_SWITCH)
+            message(FATAL_ERROR "Cannot static link libstdc++")
+        endif()
+        set(LINKER_FLAGS ${LINKER_FLAGS} "-Wl,--push-state,-Bstatic,-lstdc++,--pop-state")
+    endif()
+
+    if(WIN32)
+        option(ALSOFT_STATIC_WINPTHREAD "Static link libwinpthread" OFF)
+        if(ALSOFT_STATIC_WINPTHREAD)
+            set(OLD_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES})
+            set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} "-Wl,--push-state,-Bstatic,-lwinpthread,--pop-state")
+            check_cxx_source_compiles("int main() { }" HAVE_STATIC_LIBWINPTHREAD_SWITCH)
+            set(CMAKE_REQUIRED_LIBRARIES ${OLD_REQUIRED_LIBRARIES})
+            unset(OLD_REQUIRED_LIBRARIES)
+
+            if(NOT HAVE_STATIC_LIBWINPTHREAD_SWITCH)
+                message(FATAL_ERROR "Cannot static link libwinpthread")
+            endif()
+            set(LINKER_FLAGS ${LINKER_FLAGS} "-Wl,--push-state,-Bstatic,-lwinpthread,--pop-state")
+        endif()
+    endif()
+endif()
+
+# Set visibility/export options if available
+if(WIN32)
+    if(NOT LIBTYPE STREQUAL "STATIC")
+        set(EXPORT_DECL "__declspec(dllexport)")
+    endif()
+else()
+    set(OLD_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
+    # Yes GCC, really don't accept visibility modes you don't support
+    set(CMAKE_REQUIRED_FLAGS "${OLD_REQUIRED_FLAGS} -Wattributes -Werror")
+
+    check_c_source_compiles("int foo() __attribute__((visibility(\"protected\")));
+                             int main() {return 0;}" HAVE_GCC_PROTECTED_VISIBILITY)
+    if(HAVE_GCC_PROTECTED_VISIBILITY)
+        if(NOT LIBTYPE STREQUAL "STATIC")
+            set(EXPORT_DECL "__attribute__((visibility(\"protected\")))")
+        endif()
+    else()
+        check_c_source_compiles("int foo() __attribute__((visibility(\"default\")));
+                                 int main() {return 0;}" HAVE_GCC_DEFAULT_VISIBILITY)
+        if(HAVE_GCC_DEFAULT_VISIBILITY)
+            if(NOT LIBTYPE STREQUAL "STATIC")
+                set(EXPORT_DECL "__attribute__((visibility(\"default\")))")
+            endif()
+        endif()
+    endif()
+
+    set(CMAKE_REQUIRED_FLAGS "${OLD_REQUIRED_FLAGS}")
+endif()
+
+
+set(SSE2_SWITCH "")
+
+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_include_file(xmmintrin.h HAVE_XMMINTRIN_H)
+check_include_file(emmintrin.h HAVE_EMMINTRIN_H)
+check_include_file(pmmintrin.h HAVE_PMMINTRIN_H)
+check_include_file(smmintrin.h HAVE_SMMINTRIN_H)
+check_include_file(arm_neon.h HAVE_ARM_NEON_H)
+
+set(HAVE_SSE        0)
+set(HAVE_SSE2       0)
+set(HAVE_SSE3       0)
+set(HAVE_SSE4_1     0)
+set(HAVE_NEON       0)
+
+# Check for SSE support
+option(ALSOFT_CPUEXT_SSE "Enable SSE support" ON)
+option(ALSOFT_REQUIRE_SSE "Require SSE support" OFF)
+if(ALSOFT_CPUEXT_SSE AND HAVE_XMMINTRIN_H)
+    set(HAVE_SSE 1)
+endif()
+if(ALSOFT_REQUIRE_SSE AND NOT HAVE_SSE)
+    message(FATAL_ERROR "Failed to enabled required SSE CPU extensions")
+endif()
+
+option(ALSOFT_CPUEXT_SSE2 "Enable SSE2 support" ON)
+option(ALSOFT_REQUIRE_SSE2 "Require SSE2 support" OFF)
+if(ALSOFT_CPUEXT_SSE2 AND HAVE_SSE AND HAVE_EMMINTRIN_H)
+    set(HAVE_SSE2 1)
+endif()
+if(ALSOFT_REQUIRE_SSE2 AND NOT HAVE_SSE2)
+    message(FATAL_ERROR "Failed to enable required SSE2 CPU extensions")
+endif()
+
+option(ALSOFT_CPUEXT_SSE3 "Enable SSE3 support" ON)
+option(ALSOFT_REQUIRE_SSE3 "Require SSE3 support" OFF)
+if(ALSOFT_CPUEXT_SSE3 AND HAVE_SSE2 AND HAVE_PMMINTRIN_H)
+    set(HAVE_SSE3 1)
+endif()
+if(ALSOFT_REQUIRE_SSE3 AND NOT HAVE_SSE3)
+    message(FATAL_ERROR "Failed to enable required SSE3 CPU extensions")
+endif()
+
+option(ALSOFT_CPUEXT_SSE4_1 "Enable SSE4.1 support" ON)
+option(ALSOFT_REQUIRE_SSE4_1 "Require SSE4.1 support" OFF)
+if(ALSOFT_CPUEXT_SSE4_1 AND HAVE_SSE3 AND HAVE_SMMINTRIN_H)
+    set(HAVE_SSE4_1 1)
+endif()
+if(ALSOFT_REQUIRE_SSE4_1 AND NOT HAVE_SSE4_1)
+    message(FATAL_ERROR "Failed to enable required SSE4.1 CPU extensions")
+endif()
+
+# Check for ARM Neon support
+option(ALSOFT_CPUEXT_NEON "Enable ARM NEON support" ON)
+option(ALSOFT_REQUIRE_NEON "Require ARM NEON support" OFF)
+if(ALSOFT_CPUEXT_NEON AND HAVE_ARM_NEON_H)
+    check_c_source_compiles("#include <arm_neon.h>
+        int main()
+        {
+            int32x4_t ret4 = vdupq_n_s32(0);
+            return vgetq_lane_s32(ret4, 0);
+        }" HAVE_NEON_INTRINSICS)
+    if(HAVE_NEON_INTRINSICS)
+        set(HAVE_NEON 1)
+    endif()
+endif()
+if(ALSOFT_REQUIRE_NEON AND NOT HAVE_NEON)
+    message(FATAL_ERROR "Failed to enabled required ARM NEON CPU extensions")
+endif()
+
+
+set(SSE_FLAGS )
+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(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)
+                set(C_FLAGS ${C_FLAGS} ${SSE_FLAGS})
+                set(FPMATH_SET 2)
+            endif()
+            # SSE depends on a 16-byte aligned stack, and by default, GCC
+            # assumes the stack is suitably aligned. Older Linux code or other
+            # 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))")
+        endif()
+    endif()
+endif()
+
+if(HAVE_SSE2)
+    set(OLD_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
+    foreach(flag_var ${SSE_FLAGS})
+        set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} ${flag_var}")
+    endforeach()
+
+    check_c_source_compiles("#include <emmintrin.h>
+        int main() {_mm_pause(); return 0;}" HAVE_SSE_INTRINSICS)
+
+    set(CMAKE_REQUIRED_FLAGS ${OLD_REQUIRED_FLAGS})
+endif()
+
+
+check_include_file(malloc.h HAVE_MALLOC_H)
+check_include_file(cpuid.h HAVE_CPUID_H)
+check_include_file(intrin.h HAVE_INTRIN_H)
+check_include_file(guiddef.h HAVE_GUIDDEF_H)
+if(NOT HAVE_GUIDDEF_H)
+    check_include_file(initguid.h HAVE_INITGUID_H)
+endif()
+
+# Some systems need libm for some math functions to work
+set(MATH_LIB )
+check_library_exists(m pow "" HAVE_LIBM)
+if(HAVE_LIBM)
+    set(MATH_LIB ${MATH_LIB} m)
+    set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} m)
+endif()
+
+# Some systems need to link with -lrt for clock_gettime as used by the common
+# eaxmple functions.
+set(RT_LIB )
+check_library_exists(rt clock_gettime "" HAVE_LIBRT)
+if(HAVE_LIBRT)
+    set(RT_LIB rt)
+endif()
+
+# Check for the dlopen API (for dynamicly loading backend libs)
+if(ALSOFT_DLOPEN)
+    check_include_file(dlfcn.h HAVE_DLFCN_H)
+    check_library_exists(dl dlopen "" HAVE_LIBDL)
+    if(HAVE_LIBDL)
+        set(EXTRA_LIBS dl ${EXTRA_LIBS})
+        set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} dl)
+    endif()
+endif()
+
+# Check for a cpuid intrinsic
+if(HAVE_CPUID_H)
+    check_c_source_compiles("#include <cpuid.h>
+        int main()
+        {
+            unsigned int eax, ebx, ecx, edx;
+            return __get_cpuid(0, &eax, &ebx, &ecx, &edx);
+        }" HAVE_GCC_GET_CPUID)
+endif()
+if(HAVE_INTRIN_H)
+    check_c_source_compiles("#include <intrin.h>
+        int main()
+        {
+            int regs[4];
+            __cpuid(regs, 0);
+            return regs[0];
+        }" HAVE_CPUID_INTRINSIC)
+endif()
+
+check_symbol_exists(posix_memalign   stdlib.h HAVE_POSIX_MEMALIGN)
+check_symbol_exists(_aligned_malloc  malloc.h HAVE__ALIGNED_MALLOC)
+check_symbol_exists(proc_pidpath     libproc.h HAVE_PROC_PIDPATH)
+
+if(NOT WIN32)
+    # We need pthreads outside of Windows, for semaphores. It's also used to
+    # set the priority and name of threads, when possible.
+    check_include_file(pthread.h HAVE_PTHREAD_H)
+    if(NOT HAVE_PTHREAD_H)
+        message(FATAL_ERROR "PThreads is required for non-Windows builds!")
+    endif()
+
+    check_c_compiler_flag(-pthread HAVE_PTHREAD)
+    if(HAVE_PTHREAD)
+        set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -pthread")
+        set(C_FLAGS ${C_FLAGS} -pthread)
+        set(LINKER_FLAGS ${LINKER_FLAGS} -pthread)
+    endif()
+
+    check_symbol_exists(pthread_setschedparam pthread.h HAVE_PTHREAD_SETSCHEDPARAM)
+
+    # Some systems need pthread_np.h to get pthread_setname_np
+    check_include_files("pthread.h;pthread_np.h" HAVE_PTHREAD_NP_H)
+    if(HAVE_PTHREAD_NP_H)
+        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)
+        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)
+        endif()
+    endif()
+endif()
+
+check_symbol_exists(getopt unistd.h HAVE_GETOPT)
+
+
+# Common sources used by both the OpenAL implementation library, the OpenAL
+# router, and certain tools and examples.
+set(COMMON_OBJS
+    common/albit.h
+    common/albyte.h
+    common/alcomplex.cpp
+    common/alcomplex.h
+    common/aldeque.h
+    common/alfstream.cpp
+    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/altraits.h
+    common/atomic.h
+    common/comptr.h
+    common/dynload.cpp
+    common/dynload.h
+    common/intrusive_ptr.h
+    common/opthelpers.h
+    common/phase_shifter.h
+    common/polyphase_resampler.cpp
+    common/polyphase_resampler.h
+    common/pragmadefs.h
+    common/ringbuffer.cpp
+    common/ringbuffer.h
+    common/strutils.cpp
+    common/strutils.h
+    common/threads.cpp
+    common/threads.h
+    common/vecmat.h
+    common/vector.h)
+
+# Core library routines
+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/cubic_defs.h
+    core/cubic_tables.cpp
+    core/cubic_tables.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
+    core/filters/biquad.cpp
+    core/filters/nfc.cpp
+    core/filters/nfc.h
+    core/filters/splitter.cpp
+    core/filters/splitter.h
+    core/fmt_traits.cpp
+    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)
+if(NOT WIN32)
+    option(ALSOFT_RTKIT "Enable RTKit support" ON)
+    option(ALSOFT_REQUIRE_RTKIT "Require RTKit/D-Bus support" FALSE)
+    if(ALSOFT_RTKIT)
+        find_package(DBus1 QUIET)
+        if(NOT DBus1_FOUND AND PkgConfig_FOUND)
+            pkg_check_modules(DBUS dbus-1)
+        endif()
+        if(DBus1_FOUND OR DBUS_FOUND)
+            set(HAVE_RTKIT 1)
+            set(CORE_OBJS ${CORE_OBJS} core/dbus_wrap.cpp core/dbus_wrap.h
+                core/rtkit.cpp core/rtkit.h)
+            if(NOT DBus1_FOUND)
+                set(INC_PATHS ${INC_PATHS} ${DBUS_INCLUDE_DIRS})
+                set(CPP_DEFS ${CPP_DEFS} ${DBUS_CFLAGS_OTHER})
+                if(NOT HAVE_DLFCN_H)
+                    set(EXTRA_LIBS ${EXTRA_LIBS} ${DBUS_LINK_LIBRARIES})
+                endif()
+            elseif(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()
+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
+    core/mixer/mixer_c.cpp)
+
+# AL and related routines
+set(OPENAL_OBJS
+    al/auxeffectslot.cpp
+    al/auxeffectslot.h
+    al/buffer.cpp
+    al/buffer.h
+    al/effect.cpp
+    al/effect.h
+    al/effects/autowah.cpp
+    al/effects/chorus.cpp
+    al/effects/compressor.cpp
+    al/effects/convolution.cpp
+    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
+    al/effects/modulator.cpp
+    al/effects/null.cpp
+    al/effects/pshifter.cpp
+    al/effects/reverb.cpp
+    al/effects/vmorpher.cpp
+    al/error.cpp
+    al/event.cpp
+    al/event.h
+    al/extension.cpp
+    al/filter.cpp
+    al/filter.h
+    al/listener.cpp
+    al/listener.h
+    al/source.cpp
+    al/source.h
+    al/state.cpp)
+
+# ALC and related routines
+set(ALC_OBJS
+    alc/alc.cpp
+    alc/alu.cpp
+    alc/alu.h
+    alc/alconfig.cpp
+    alc/alconfig.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
+    alc/effects/compressor.cpp
+    alc/effects/convolution.cpp
+    alc/effects/dedicated.cpp
+    alc/effects/distortion.cpp
+    alc/effects/echo.cpp
+    alc/effects/equalizer.cpp
+    alc/effects/fshifter.cpp
+    alc/effects/modulator.cpp
+    alc/effects/null.cpp
+    alc/effects/pshifter.cpp
+    alc/effects/reverb.cpp
+    alc/effects/vmorpher.cpp
+    alc/inprogext.h
+    alc/panning.cpp)
+
+if(ALSOFT_EAX)
+    set(OPENAL_OBJS
+        ${OPENAL_OBJS}
+        al/eax/api.cpp
+        al/eax/api.h
+        al/eax/call.cpp
+        al/eax/call.h
+        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.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_sse2.cpp)
+    set(CPU_EXTS "${CPU_EXTS}, SSE2")
+endif()
+if(HAVE_SSE3)
+    set(CORE_OBJS  ${CORE_OBJS} core/mixer/mixer_sse3.cpp)
+    set(CPU_EXTS "${CPU_EXTS}, SSE3")
+endif()
+if(HAVE_SSE4_1)
+    set(CORE_OBJS  ${CORE_OBJS} core/mixer/mixer_sse41.cpp)
+    set(CPU_EXTS "${CPU_EXTS}, SSE4.1")
+endif()
+if(HAVE_NEON)
+    set(CORE_OBJS  ${CORE_OBJS} core/mixer/mixer_neon.cpp)
+    set(CPU_EXTS "${CPU_EXTS}, Neon")
+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)
+set(HAVE_WASAPI     0)
+set(HAVE_WINMM      0)
+set(HAVE_PORTAUDIO  0)
+set(HAVE_PULSEAUDIO 0)
+set(HAVE_COREAUDIO  0)
+set(HAVE_OPENSL     0)
+set(HAVE_OBOE       0)
+set(HAVE_WAVE       0)
+set(HAVE_SDL2       0)
+
+if(WIN32 OR HAVE_DLFCN_H)
+    set(IS_LINKED "")
+    macro(ADD_BACKEND_LIBS _LIBS)
+    endmacro()
+else()
+    set(IS_LINKED " (linked)")
+    macro(ADD_BACKEND_LIBS _LIBS)
+        set(EXTRA_LIBS ${_LIBS} ${EXTRA_LIBS})
+    endmacro()
+endif()
+
+set(BACKENDS "")
+set(ALC_OBJS  ${ALC_OBJS}
+    alc/backends/base.cpp
+    alc/backends/base.h
+    # Default backends, always available
+    alc/backends/loopback.cpp
+    alc/backends/loopback.h
+    alc/backends/null.cpp
+    alc/backends/null.h
+)
+
+# Check PipeWire backend
+option(ALSOFT_BACKEND_PIPEWIRE "Enable PipeWire backend" ON)
+option(ALSOFT_REQUIRE_PIPEWIRE "Require PipeWire backend" OFF)
+if(ALSOFT_BACKEND_PIPEWIRE AND PkgConfig_FOUND)
+    pkg_check_modules(PIPEWIRE libpipewire-0.3>=0.3.23)
+    if(PIPEWIRE_FOUND)
+        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()
+if(ALSOFT_REQUIRE_PIPEWIRE AND NOT HAVE_PIPEWIRE)
+    message(FATAL_ERROR "Failed to enabled required PipeWire backend")
+endif()
+
+# Check PulseAudio backend
+option(ALSOFT_BACKEND_PULSEAUDIO "Enable PulseAudio backend" ON)
+option(ALSOFT_REQUIRE_PULSEAUDIO "Require PulseAudio backend" OFF)
+if(ALSOFT_BACKEND_PULSEAUDIO)
+    find_package(PulseAudio)
+    if(PULSEAUDIO_FOUND)
+        set(HAVE_PULSEAUDIO 1)
+        set(BACKENDS  "${BACKENDS} PulseAudio${IS_LINKED},")
+        set(ALC_OBJS  ${ALC_OBJS} alc/backends/pulseaudio.cpp alc/backends/pulseaudio.h)
+        add_backend_libs(${PULSEAUDIO_LIBRARY})
+        set(INC_PATHS ${INC_PATHS} ${PULSEAUDIO_INCLUDE_DIR})
+    endif()
+endif()
+if(ALSOFT_REQUIRE_PULSEAUDIO AND NOT HAVE_PULSEAUDIO)
+    message(FATAL_ERROR "Failed to enabled required PulseAudio backend")
+endif()
+
+if(NOT WIN32)
+    # Check ALSA backend
+    option(ALSOFT_BACKEND_ALSA "Enable ALSA backend" ON)
+    option(ALSOFT_REQUIRE_ALSA "Require ALSA backend" OFF)
+    if(ALSOFT_BACKEND_ALSA)
+        find_package(ALSA)
+        if(ALSA_FOUND)
+            set(HAVE_ALSA 1)
+            set(BACKENDS  "${BACKENDS} ALSA${IS_LINKED},")
+            set(ALC_OBJS  ${ALC_OBJS} alc/backends/alsa.cpp alc/backends/alsa.h)
+            add_backend_libs(${ALSA_LIBRARIES})
+            set(INC_PATHS ${INC_PATHS} ${ALSA_INCLUDE_DIRS})
+        endif()
+    endif()
+
+    # Check OSS backend
+    option(ALSOFT_BACKEND_OSS "Enable OSS backend" ON)
+    option(ALSOFT_REQUIRE_OSS "Require OSS backend" OFF)
+    if(ALSOFT_BACKEND_OSS)
+        find_package(OSS)
+        if(OSS_FOUND)
+            set(HAVE_OSS 1)
+            set(BACKENDS  "${BACKENDS} OSS,")
+            set(ALC_OBJS  ${ALC_OBJS} alc/backends/oss.cpp alc/backends/oss.h)
+            if(OSS_LIBRARIES)
+                set(EXTRA_LIBS ${OSS_LIBRARIES} ${EXTRA_LIBS})
+            endif()
+            set(INC_PATHS ${INC_PATHS} ${OSS_INCLUDE_DIRS})
+        endif()
+    endif()
+
+    # Check Solaris backend
+    option(ALSOFT_BACKEND_SOLARIS "Enable Solaris backend" ON)
+    option(ALSOFT_REQUIRE_SOLARIS "Require Solaris backend" OFF)
+    if(ALSOFT_BACKEND_SOLARIS)
+        find_package(AudioIO)
+        if(AUDIOIO_FOUND)
+            set(HAVE_SOLARIS 1)
+            set(BACKENDS  "${BACKENDS} Solaris,")
+            set(ALC_OBJS  ${ALC_OBJS} alc/backends/solaris.cpp alc/backends/solaris.h)
+            set(INC_PATHS ${INC_PATHS} ${AUDIOIO_INCLUDE_DIRS})
+        endif()
+    endif()
+
+    # Check SndIO backend
+    option(ALSOFT_BACKEND_SNDIO "Enable SndIO backend" ON)
+    option(ALSOFT_REQUIRE_SNDIO "Require SndIO backend" OFF)
+    if(ALSOFT_BACKEND_SNDIO)
+        find_package(SoundIO)
+        if(SOUNDIO_FOUND)
+            set(HAVE_SNDIO 1)
+            set(BACKENDS  "${BACKENDS} SndIO (linked),")
+            set(ALC_OBJS  ${ALC_OBJS} alc/backends/sndio.cpp alc/backends/sndio.h)
+            set(EXTRA_LIBS ${SOUNDIO_LIBRARIES} ${EXTRA_LIBS})
+            set(INC_PATHS ${INC_PATHS} ${SOUNDIO_INCLUDE_DIRS})
+        endif()
+    endif()
+endif()
+if(ALSOFT_REQUIRE_ALSA AND NOT HAVE_ALSA)
+    message(FATAL_ERROR "Failed to enabled required ALSA backend")
+endif()
+if(ALSOFT_REQUIRE_OSS AND NOT HAVE_OSS)
+    message(FATAL_ERROR "Failed to enabled required OSS backend")
+endif()
+if(ALSOFT_REQUIRE_SOLARIS AND NOT HAVE_SOLARIS)
+    message(FATAL_ERROR "Failed to enabled required Solaris backend")
+endif()
+if(ALSOFT_REQUIRE_SNDIO AND NOT HAVE_SNDIO)
+    message(FATAL_ERROR "Failed to enabled required SndIO backend")
+endif()
+
+# Check Windows-only backends
+if(WIN32)
+    # Check MMSystem backend
+    option(ALSOFT_BACKEND_WINMM "Enable Windows Multimedia backend" ON)
+    option(ALSOFT_REQUIRE_WINMM "Require Windows Multimedia backend" OFF)
+    if(ALSOFT_BACKEND_WINMM)
+        set(HAVE_WINMM 1)
+        set(BACKENDS "${BACKENDS} WinMM,")
+        set(ALC_OBJS ${ALC_OBJS} alc/backends/winmm.cpp alc/backends/winmm.h)
+        # There doesn't seem to be good way to search for winmm.lib for MSVC.
+        # find_library doesn't find it without being told to look in a specific
+        # place in the WindowsSDK, but it links anyway. If there ends up being
+        # Windows targets without this, another means to detect it is needed.
+        set(EXTRA_LIBS winmm ${EXTRA_LIBS})
+    endif()
+
+    # Check DSound backend
+    option(ALSOFT_BACKEND_DSOUND "Enable DirectSound backend" ON)
+    option(ALSOFT_REQUIRE_DSOUND "Require DirectSound backend" OFF)
+    if(ALSOFT_BACKEND_DSOUND)
+        check_include_file(dsound.h HAVE_DSOUND_H)
+        if(DXSDK_DIR)
+            find_path(DSOUND_INCLUDE_DIR NAMES "dsound.h"
+                PATHS "${DXSDK_DIR}" PATH_SUFFIXES include
+                DOC "The DirectSound include directory")
+        endif()
+        if(HAVE_DSOUND_H OR DSOUND_INCLUDE_DIR)
+            set(HAVE_DSOUND 1)
+            set(BACKENDS "${BACKENDS} DirectSound,")
+            set(ALC_OBJS ${ALC_OBJS} alc/backends/dsound.cpp alc/backends/dsound.h)
+
+            if(NOT HAVE_DSOUND_H)
+                set(INC_PATHS ${INC_PATHS} ${DSOUND_INCLUDE_DIR})
+            endif()
+        endif()
+    endif()
+
+    # Check for WASAPI backend
+    option(ALSOFT_BACKEND_WASAPI "Enable WASAPI backend" ON)
+    option(ALSOFT_REQUIRE_WASAPI "Require WASAPI backend" OFF)
+    if(ALSOFT_BACKEND_WASAPI)
+        check_include_file(mmdeviceapi.h HAVE_MMDEVICEAPI_H)
+        if(HAVE_MMDEVICEAPI_H)
+            set(HAVE_WASAPI 1)
+            set(BACKENDS  "${BACKENDS} WASAPI,")
+            set(ALC_OBJS  ${ALC_OBJS} alc/backends/wasapi.cpp alc/backends/wasapi.h)
+        endif()
+    endif()
+endif()
+if(ALSOFT_REQUIRE_WINMM AND NOT HAVE_WINMM)
+    message(FATAL_ERROR "Failed to enabled required WinMM backend")
+endif()
+if(ALSOFT_REQUIRE_DSOUND AND NOT HAVE_DSOUND)
+    message(FATAL_ERROR "Failed to enabled required DSound backend")
+endif()
+if(ALSOFT_REQUIRE_WASAPI AND NOT HAVE_WASAPI)
+    message(FATAL_ERROR "Failed to enabled required WASAPI backend")
+endif()
+
+# Check JACK backend
+option(ALSOFT_BACKEND_JACK "Enable JACK backend" ON)
+option(ALSOFT_REQUIRE_JACK "Require JACK backend" OFF)
+if(ALSOFT_BACKEND_JACK)
+    find_package(JACK)
+    if(JACK_FOUND)
+        set(HAVE_JACK 1)
+        set(BACKENDS  "${BACKENDS} JACK${IS_LINKED},")
+        set(ALC_OBJS  ${ALC_OBJS} alc/backends/jack.cpp alc/backends/jack.h)
+        add_backend_libs(${JACK_LIBRARIES})
+        set(INC_PATHS ${INC_PATHS} ${JACK_INCLUDE_DIRS})
+    endif()
+endif()
+if(ALSOFT_REQUIRE_JACK AND NOT HAVE_JACK)
+    message(FATAL_ERROR "Failed to enabled required JACK backend")
+endif()
+
+# Check CoreAudio backend
+option(ALSOFT_BACKEND_COREAUDIO "Enable CoreAudio backend" ON)
+option(ALSOFT_REQUIRE_COREAUDIO "Require CoreAudio backend" OFF)
+if(ALSOFT_BACKEND_COREAUDIO)
+    find_library(COREAUDIO_FRAMEWORK NAMES CoreAudio)
+    find_path(AUDIOUNIT_INCLUDE_DIR NAMES AudioUnit/AudioUnit.h)
+    if(COREAUDIO_FRAMEWORK AND AUDIOUNIT_INCLUDE_DIR)
+        set(HAVE_COREAUDIO 1)
+        set(ALC_OBJS  ${ALC_OBJS} alc/backends/coreaudio.cpp alc/backends/coreaudio.h)
+        set(BACKENDS  "${BACKENDS} CoreAudio,")
+
+        set(EXTRA_LIBS -Wl,-framework,CoreAudio ${EXTRA_LIBS})
+        if(CMAKE_SYSTEM_NAME MATCHES "^(iOS|tvOS)$")
+            find_library(COREFOUNDATION_FRAMEWORK NAMES CoreFoundation)
+            if(COREFOUNDATION_FRAMEWORK)
+                set(EXTRA_LIBS -Wl,-framework,CoreFoundation ${EXTRA_LIBS})
+            endif()
+        else()
+            set(EXTRA_LIBS -Wl,-framework,AudioUnit,-framework,ApplicationServices ${EXTRA_LIBS})
+        endif()
+
+        # Some versions of OSX may need the AudioToolbox framework. Add it if
+        # it's found.
+        find_library(AUDIOTOOLBOX_LIBRARY NAMES AudioToolbox)
+        if(AUDIOTOOLBOX_LIBRARY)
+            set(EXTRA_LIBS -Wl,-framework,AudioToolbox ${EXTRA_LIBS})
+        endif()
+
+        set(INC_PATHS ${INC_PATHS} ${AUDIOUNIT_INCLUDE_DIR})
+    endif()
+endif()
+if(ALSOFT_REQUIRE_COREAUDIO AND NOT HAVE_COREAUDIO)
+    message(FATAL_ERROR "Failed to enabled required CoreAudio backend")
+endif()
+
+# Check for Oboe (Android) backend
+option(ALSOFT_BACKEND_OBOE "Enable Oboe backend" ON)
+option(ALSOFT_REQUIRE_OBOE "Require Oboe backend" OFF)
+if(ALSOFT_BACKEND_OBOE)
+    set(OBOE_TARGET )
+    if(ANDROID)
+        set(OBOE_SOURCE "" CACHE STRING "Source directory for Oboe.")
+        if(OBOE_SOURCE)
+            add_subdirectory(${OBOE_SOURCE} ./oboe)
+            set(OBOE_TARGET oboe)
+        else()
+            find_package(oboe CONFIG)
+            if(NOT TARGET oboe::oboe)
+                find_package(Oboe)
+            endif()
+            if(TARGET oboe::oboe)
+                set(OBOE_TARGET "oboe::oboe")
+            endif()
+        endif()
+    endif()
+
+    if(OBOE_TARGET)
+        set(HAVE_OBOE 1)
+        set(ALC_OBJS  ${ALC_OBJS} alc/backends/oboe.cpp alc/backends/oboe.h)
+        set(BACKENDS  "${BACKENDS} Oboe,")
+        set(EXTRA_LIBS ${OBOE_TARGET} ${EXTRA_LIBS})
+    endif()
+endif()
+if(ALSOFT_REQUIRE_OBOE AND NOT HAVE_OBOE)
+    message(FATAL_ERROR "Failed to enabled required Oboe backend")
+endif()
+
+# Check for OpenSL (Android) backend
+option(ALSOFT_BACKEND_OPENSL "Enable OpenSL backend" ON)
+option(ALSOFT_REQUIRE_OPENSL "Require OpenSL backend" OFF)
+if(ALSOFT_BACKEND_OPENSL)
+    find_package(OpenSL)
+    if(OPENSL_FOUND)
+        set(HAVE_OPENSL 1)
+        set(ALC_OBJS  ${ALC_OBJS} alc/backends/opensl.cpp alc/backends/opensl.h)
+        set(BACKENDS  "${BACKENDS} OpenSL,")
+        set(EXTRA_LIBS "OpenSL::OpenSLES" ${EXTRA_LIBS})
+    endif()
+endif()
+if(ALSOFT_REQUIRE_OPENSL AND NOT HAVE_OPENSL)
+    message(FATAL_ERROR "Failed to enabled required OpenSL backend")
+endif()
+
+# Check PortAudio backend
+option(ALSOFT_BACKEND_PORTAUDIO "Enable PortAudio backend" ON)
+option(ALSOFT_REQUIRE_PORTAUDIO "Require PortAudio backend" OFF)
+if(ALSOFT_BACKEND_PORTAUDIO)
+    find_package(PortAudio)
+    if(PORTAUDIO_FOUND)
+        set(HAVE_PORTAUDIO 1)
+        set(BACKENDS  "${BACKENDS} PortAudio${IS_LINKED},")
+        set(ALC_OBJS  ${ALC_OBJS} alc/backends/portaudio.cpp alc/backends/portaudio.h)
+        add_backend_libs(${PORTAUDIO_LIBRARIES})
+        set(INC_PATHS ${INC_PATHS} ${PORTAUDIO_INCLUDE_DIRS})
+    endif()
+endif()
+if(ALSOFT_REQUIRE_PORTAUDIO AND NOT HAVE_PORTAUDIO)
+    message(FATAL_ERROR "Failed to enabled required PortAudio backend")
+endif()
+
+# Check for SDL2 backend
+# Off by default, since it adds a runtime dependency
+option(ALSOFT_BACKEND_SDL2 "Enable SDL2 backend" OFF)
+option(ALSOFT_REQUIRE_SDL2 "Require SDL2 backend" OFF)
+if(ALSOFT_BACKEND_SDL2)
+    if(SDL2_FOUND)
+        set(HAVE_SDL2 1)
+        set(ALC_OBJS  ${ALC_OBJS} alc/backends/sdl2.cpp alc/backends/sdl2.h)
+        set(BACKENDS  "${BACKENDS} SDL2,")
+        set(EXTRA_LIBS ${EXTRA_LIBS} SDL2::SDL2)
+    else()
+        message(STATUS "Could NOT find SDL2")
+    endif()
+endif()
+if(ALSOFT_REQUIRE_SDL2 AND NOT SDL2_FOUND)
+    message(FATAL_ERROR "Failed to enabled required SDL2 backend")
+endif()
+
+# Optionally enable the Wave Writer backend
+option(ALSOFT_BACKEND_WAVE "Enable Wave Writer backend" ON)
+if(ALSOFT_BACKEND_WAVE)
+    set(HAVE_WAVE 1)
+    set(ALC_OBJS  ${ALC_OBJS} alc/backends/wave.cpp alc/backends/wave.h)
+    set(BACKENDS  "${BACKENDS} WaveFile,")
+endif()
+
+# This is always available
+set(BACKENDS  "${BACKENDS} Null")
+
+
+find_package(Git)
+if(ALSOFT_UPDATE_BUILD_VERSION AND GIT_FOUND AND EXISTS "${OpenAL_SOURCE_DIR}/.git")
+    set(GIT_DIR "${OpenAL_SOURCE_DIR}/.git")
+
+    # Check if this is a submodule, if it is then find the .git directory
+    if(NOT IS_DIRECTORY "${OpenAL_SOURCE_DIR}/.git")
+        file(READ ${GIT_DIR} submodule)
+        string(REGEX REPLACE "gitdir: (.*)$" "\\1" GIT_DIR_RELATIVE ${submodule})
+        string(STRIP ${GIT_DIR_RELATIVE} GIT_DIR_RELATIVE)
+        get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH)
+        get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE)
+    endif()
+
+    # Get the current working branch and its latest abbreviated commit hash
+    add_custom_command(OUTPUT "${OpenAL_BINARY_DIR}/version_witness.txt"
+        BYPRODUCTS "${OpenAL_BINARY_DIR}/version.h"
+        COMMAND ${CMAKE_COMMAND} -D GIT_EXECUTABLE=${GIT_EXECUTABLE} -D LIB_VERSION=${LIB_VERSION}
+            -D LIB_VERSION_NUM=${LIB_VERSION_NUM} -D SRC=${OpenAL_SOURCE_DIR}/version.h.in
+            -D DST=${OpenAL_BINARY_DIR}/version.h -P ${OpenAL_SOURCE_DIR}/version.cmake
+        COMMAND ${CMAKE_COMMAND} -E touch "${OpenAL_BINARY_DIR}/version_witness.txt"
+        WORKING_DIRECTORY "${OpenAL_SOURCE_DIR}"
+        MAIN_DEPENDENCY "${OpenAL_SOURCE_DIR}/version.h.in"
+        DEPENDS "${GIT_DIR}/index" "${OpenAL_SOURCE_DIR}/version.cmake"
+        VERBATIM
+    )
+
+    add_custom_target(build_version DEPENDS "${OpenAL_BINARY_DIR}/version_witness.txt")
+else()
+    set(GIT_BRANCH "UNKNOWN")
+    set(GIT_COMMIT_HASH "unknown")
+    configure_file(
+        "${OpenAL_SOURCE_DIR}/version.h.in"
+        "${OpenAL_BINARY_DIR}/version.h")
+endif()
+
+
+option(ALSOFT_EMBED_HRTF_DATA "Embed the HRTF data files (increases library footprint)" ON)
+if(ALSOFT_EMBED_HRTF_DATA)
+    macro(make_hrtf_header FILENAME VARNAME)
+        set(infile  "${OpenAL_SOURCE_DIR}/hrtf/${FILENAME}")
+        set(outfile  "${OpenAL_BINARY_DIR}/${VARNAME}.txt")
+
+        add_custom_command(OUTPUT "${outfile}"
+            COMMAND ${CMAKE_COMMAND} -D "INPUT_FILE=${infile}" -D "OUTPUT_FILE=${outfile}"
+                -P "${CMAKE_MODULE_PATH}/bin2h.script.cmake"
+            WORKING_DIRECTORY "${OpenAL_SOURCE_DIR}"
+            DEPENDS "${infile}" "${CMAKE_MODULE_PATH}/bin2h.script.cmake"
+            VERBATIM
+        )
+        set(ALC_OBJS  ${ALC_OBJS} "${outfile}")
+    endmacro()
+
+    make_hrtf_header("Default HRTF.mhr" "default_hrtf")
+endif()
+
+
+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_UTILS OR ALSOFT_EXAMPLES)
+    find_package(SndFile)
+    if(SDL2_FOUND)
+        find_package(FFmpeg COMPONENTS AVFORMAT AVCODEC AVUTIL SWSCALE SWRESAMPLE)
+    endif()
+endif()
+
+if(NOT WIN32)
+    set(LIBNAME "openal")
+else()
+    set(LIBNAME "OpenAL32")
+endif()
+
+# Needed for openal.pc.in
+set(prefix ${CMAKE_INSTALL_PREFIX})
+set(exec_prefix "\${prefix}")
+set(libdir "${CMAKE_INSTALL_FULL_LIBDIR}")
+set(bindir "${CMAKE_INSTALL_FULL_BINDIR}")
+set(includedir "${CMAKE_INSTALL_FULL_INCLUDEDIR}")
+set(PACKAGE_VERSION "${LIB_VERSION}")
+set(PKG_CONFIG_CFLAGS )
+set(PKG_CONFIG_PRIVATE_LIBS )
+if(LIBTYPE STREQUAL "STATIC")
+    set(PKG_CONFIG_CFLAGS -DAL_LIBTYPE_STATIC)
+    foreach(FLAG  ${LINKER_FLAGS} ${EXTRA_LIBS} ${MATH_LIB})
+        # If this is already a linker flag, or is a full path+file, add it
+        # as-is. If it's an SDL2 target, add the link flag for it. Otherwise,
+        # it's a name intended to be dressed as -lname.
+        if(FLAG MATCHES "^-.*" OR EXISTS "${FLAG}")
+            set(PKG_CONFIG_PRIVATE_LIBS "${PKG_CONFIG_PRIVATE_LIBS} ${FLAG}")
+        elseif(FLAG MATCHES "^SDL2::SDL2")
+            set(PKG_CONFIG_PRIVATE_LIBS "${PKG_CONFIG_PRIVATE_LIBS} -lSDL2")
+        else()
+            set(PKG_CONFIG_PRIVATE_LIBS "${PKG_CONFIG_PRIVATE_LIBS} -l${FLAG}")
+        endif()
+    endforeach()
+endif()
+
+# End configuration
+configure_file(
+    "${OpenAL_SOURCE_DIR}/config.h.in"
+    "${OpenAL_BINARY_DIR}/config.h")
+configure_file(
+    "${OpenAL_SOURCE_DIR}/openal.pc.in"
+    "${OpenAL_BINARY_DIR}/openal.pc"
+    @ONLY)
+
+
+add_library(common STATIC EXCLUDE_FROM_ALL ${COMMON_OBJS})
+target_include_directories(common PRIVATE ${OpenAL_BINARY_DIR} ${OpenAL_SOURCE_DIR}/include)
+target_compile_definitions(common PRIVATE ${CPP_DEFS})
+target_compile_options(common PRIVATE ${C_FLAGS})
+set_target_properties(common PROPERTIES ${DEFAULT_TARGET_PROPS} POSITION_INDEPENDENT_CODE TRUE)
+
+
+unset(HAS_ROUTER)
+set(IMPL_TARGET OpenAL) # Either OpenAL or soft_oal.
+
+# Build main library
+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
+        # required IDs are defined.
+        option(ALSOFT_NO_UID_DEFS "Do not define GUIDs, IIDs, CLSIDs, or PropertyKeys" OFF)
+        if(ALSOFT_NO_UID_DEFS)
+            target_compile_definitions(${IMPL_TARGET} PRIVATE AL_NO_UID_DEFS)
+        endif()
+    endif()
+else()
+    set(RC_CONFIG resources/openal32.rc)
+    if(WIN32 AND ALSOFT_BUILD_ROUTER)
+        add_library(OpenAL SHARED
+            resources/router.rc
+            router/router.cpp
+            router/router.h
+            router/alc.cpp
+            router/al.cpp
+        )
+        target_compile_definitions(OpenAL
+            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:${CMAKE_INSTALL_INCLUDEDIR}>
+          PRIVATE
+            ${OpenAL_SOURCE_DIR}/common
+            ${OpenAL_BINARY_DIR}
+        )
+        set_target_properties(OpenAL PROPERTIES ${DEFAULT_TARGET_PROPS} PREFIX ""
+            OUTPUT_NAME ${LIBNAME})
+        if(TARGET build_version)
+            add_dependencies(OpenAL build_version)
+        endif()
+        set(HAS_ROUTER 1)
+
+        set(LIBNAME "soft_oal")
+        set(IMPL_TARGET soft_oal)
+        set(RC_CONFIG resources/soft_oal.rc)
+    endif()
+
+    # !important: for osx framework public header works, the headers must be in
+    # the project
+    set(TARGET_PUBLIC_HEADERS include/AL/al.h include/AL/alc.h include/AL/alext.h include/AL/efx.h
+        include/AL/efx-presets.h)
+
+    add_library(${IMPL_TARGET} SHARED ${OPENAL_OBJS} ${ALC_OBJS} ${CORE_OBJS} ${RC_CONFIG}
+        ${TARGET_PUBLIC_HEADERS})
+    if(WIN32)
+        set_target_properties(${IMPL_TARGET} PROPERTIES PREFIX "")
+    endif()
+    target_link_libraries(${IMPL_TARGET} PRIVATE common ${LINKER_FLAGS} ${EXTRA_LIBS} ${MATH_LIB})
+
+    if(NOT WIN32 AND NOT APPLE)
+        # FIXME: This doesn't put a dependency on the version script. Changing
+        # the version script will not cause a relink as it should.
+        set_property(TARGET ${IMPL_TARGET} APPEND_STRING PROPERTY
+            LINK_FLAGS " -Wl,--version-script=${OpenAL_SOURCE_DIR}/libopenal.version")
+    endif()
+
+    if(APPLE AND ALSOFT_OSX_FRAMEWORK)
+        # Sets framework name to soft_oal to avoid ambiguity with the system OpenAL.framework
+        set(LIBNAME "soft_oal")
+        if(GIT_FOUND)
+            execute_process(COMMAND ${GIT_EXECUTABLE} rev-list --count HEAD
+                TIMEOUT 5
+                OUTPUT_VARIABLE BUNDLE_VERSION
+                OUTPUT_STRIP_TRAILING_WHITESPACE)
+        else()
+            set(BUNDLE_VERSION 1)
+        endif()
+
+        # Build: Fix rpath in iOS shared libraries
+        # If this flag is not set, the final dylib binary ld path will be
+        # /User/xxx/*/soft_oal.framework/soft_oal, which can't be loaded by iOS devices.
+        # See also: https://github.com/libjpeg-turbo/libjpeg-turbo/commit/c80ddef7a4ce21ace9e3ca0fd190d320cc8cdaeb
+        if(NOT CMAKE_SHARED_LIBRARY_RUNTIME_C_FLAG)
+            set(CMAKE_SHARED_LIBRARY_RUNTIME_C_FLAG "-Wl,-rpath,")
+        endif()
+
+        set_target_properties(${IMPL_TARGET} PROPERTIES
+            FRAMEWORK TRUE
+            FRAMEWORK_VERSION C
+            MACOSX_FRAMEWORK_NAME "${IMPL_TARGET}"
+            MACOSX_FRAMEWORK_IDENTIFIER "org.openal-soft.openal"
+            MACOSX_FRAMEWORK_SHORT_VERSION_STRING "${LIB_VERSION}"
+            MACOSX_FRAMEWORK_BUNDLE_VERSION "${BUNDLE_VERSION}"
+            XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ""
+            XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
+            XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
+            PUBLIC_HEADER "${TARGET_PUBLIC_HEADERS}"
+            MACOSX_RPATH TRUE)
+    endif()
+endif()
+
+target_include_directories(${IMPL_TARGET}
+  PUBLIC
+    $<BUILD_INTERFACE:${OpenAL_SOURCE_DIR}/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}/common
+)
+
+set_target_properties(${IMPL_TARGET} PROPERTIES ${DEFAULT_TARGET_PROPS}
+    OUTPUT_NAME ${LIBNAME}
+    VERSION ${LIB_VERSION}
+    SOVERSION ${LIB_MAJOR_VERSION}
+)
+target_compile_definitions(${IMPL_TARGET}
+    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)
+    add_dependencies(${IMPL_TARGET} build_version)
+endif()
+
+if(WIN32 AND MINGW AND ALSOFT_BUILD_IMPORT_LIB AND NOT LIBTYPE STREQUAL "STATIC")
+    find_program(SED_EXECUTABLE NAMES sed DOC "sed executable")
+    if(NOT SED_EXECUTABLE OR NOT CMAKE_DLLTOOL)
+        message(STATUS "")
+        if(NOT SED_EXECUTABLE)
+            message(STATUS "WARNING: Cannot find sed, disabling .def/.lib generation")
+        endif()
+        if(NOT CMAKE_DLLTOOL)
+            message(STATUS "WARNING: Cannot find dlltool, disabling .def/.lib generation")
+        endif()
+    else()
+        set_property(TARGET OpenAL APPEND_STRING PROPERTY
+            LINK_FLAGS " -Wl,--output-def,OpenAL32.def")
+        add_custom_command(TARGET OpenAL POST_BUILD
+            COMMAND "${SED_EXECUTABLE}" -i -e "s/ @[^ ]*//" OpenAL32.def
+            COMMAND "${CMAKE_DLLTOOL}" -d OpenAL32.def -l OpenAL32.lib -D OpenAL32.dll
+            # Technically OpenAL32.def was created by the build, but cmake
+            # doesn't recognize it due to -Wl,--output-def,OpenAL32.def being
+            # manually specified. But declaring the file here allows it to be
+            # properly cleaned, e.g. during make clean.
+            BYPRODUCTS OpenAL32.def OpenAL32.lib
+            COMMENT "Stripping ordinals from OpenAL32.def and generating OpenAL32.lib..."
+            VERBATIM
+        )
+    endif()
+endif()
+
+if(HAS_ROUTER)
+    message(STATUS "")
+    message(STATUS "Building DLL router")
+endif()
+
+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 "")
+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 "")
+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}
+        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+        FRAMEWORK DESTINATION ${CMAKE_INSTALL_LIBDIR}
+        INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ${CMAKE_INSTALL_INCLUDEDIR}/AL)
+    export(TARGETS OpenAL
+        NAMESPACE OpenAL::
+        FILE OpenALTargets.cmake)
+    install(EXPORT OpenAL
+        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/OpenAL
+        NAMESPACE OpenAL::
+        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})
+    endif()
+    message(STATUS "Installing library and headers")
+else()
+    message(STATUS "NOT installing library and headers")
+endif()
+
+if(ALSOFT_INSTALL_CONFIG)
+    install(FILES alsoftrc.sample
+        DESTINATION ${CMAKE_INSTALL_DATADIR}/openal)
+    message(STATUS "Installing sample configuration")
+endif()
+
+if(ALSOFT_INSTALL_HRTF_DATA)
+    install(DIRECTORY hrtf
+        DESTINATION ${CMAKE_INSTALL_DATADIR}/openal)
+    message(STATUS "Installing HRTF data files")
+endif()
+
+if(ALSOFT_INSTALL_AMBDEC_PRESETS)
+    install(DIRECTORY presets
+        DESTINATION ${CMAKE_INSTALL_DATADIR}/openal)
+    message(STATUS "Installing AmbDec presets")
+endif()
+message(STATUS "")
+
+set(UNICODE_FLAG )
+if(MINGW)
+    set(UNICODE_FLAG ${UNICODE_FLAG} -municode)
+endif()
+set(EXTRA_INSTALLS )
+if(ALSOFT_UTILS)
+    add_executable(openal-info utils/openal-info.c)
+    target_include_directories(openal-info PRIVATE ${OpenAL_SOURCE_DIR}/common)
+    target_compile_options(openal-info PRIVATE ${C_FLAGS})
+    target_link_libraries(openal-info PRIVATE ${LINKER_FLAGS} OpenAL ${UNICODE_FLAG})
+    set_target_properties(openal-info PROPERTIES ${DEFAULT_TARGET_PROPS})
+    if(ALSOFT_INSTALL_EXAMPLES)
+        set(EXTRA_INSTALLS ${EXTRA_INSTALLS} openal-info)
+    endif()
+
+    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})
+        set_target_properties(uhjdecoder PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        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})
+        set_target_properties(uhjencoder PROPERTIES ${DEFAULT_TARGET_PROPS})
+    endif()
+
+    if(MYSOFA_FOUND)
+        set(SOFA_SUPPORT_SRCS
+            utils/sofa-support.cpp
+            utils/sofa-support.h)
+        add_library(sofa-support STATIC EXCLUDE_FROM_ALL ${SOFA_SUPPORT_SRCS})
+        target_compile_definitions(sofa-support PRIVATE ${CPP_DEFS})
+        target_include_directories(sofa-support PUBLIC ${OpenAL_SOURCE_DIR}/common)
+        target_compile_options(sofa-support PRIVATE ${C_FLAGS})
+        target_link_libraries(sofa-support PUBLIC common MySOFA::MySOFA PRIVATE ${LINKER_FLAGS})
+        set_target_properties(sofa-support PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        set(MAKEMHR_SRCS
+            utils/makemhr/loaddef.cpp
+            utils/makemhr/loaddef.h
+            utils/makemhr/loadsofa.cpp
+            utils/makemhr/loadsofa.h
+            utils/makemhr/makemhr.cpp
+            utils/makemhr/makemhr.h)
+        if(NOT HAVE_GETOPT)
+            set(MAKEMHR_SRCS  ${MAKEMHR_SRCS} utils/getopt.c utils/getopt.h)
+        endif()
+        add_executable(makemhr ${MAKEMHR_SRCS})
+        target_compile_definitions(makemhr PRIVATE ${CPP_DEFS})
+        target_include_directories(makemhr
+            PRIVATE ${OpenAL_BINARY_DIR} ${OpenAL_SOURCE_DIR}/utils)
+        target_compile_options(makemhr PRIVATE ${C_FLAGS})
+        target_link_libraries(makemhr PRIVATE ${LINKER_FLAGS} sofa-support ${UNICODE_FLAG})
+        set_target_properties(makemhr PROPERTIES ${DEFAULT_TARGET_PROPS})
+        if(ALSOFT_INSTALL_EXAMPLES)
+            set(EXTRA_INSTALLS ${EXTRA_INSTALLS} makemhr)
+        endif()
+
+        set(SOFAINFO_SRCS  utils/sofa-info.cpp)
+        add_executable(sofa-info ${SOFAINFO_SRCS})
+        target_compile_definitions(sofa-info PRIVATE ${CPP_DEFS})
+        target_include_directories(sofa-info PRIVATE ${OpenAL_SOURCE_DIR}/utils)
+        target_compile_options(sofa-info PRIVATE ${C_FLAGS})
+        target_link_libraries(sofa-info PRIVATE ${LINKER_FLAGS} sofa-support ${UNICODE_FLAG})
+        set_target_properties(sofa-info PROPERTIES ${DEFAULT_TARGET_PROPS})
+    endif()
+    message(STATUS "Building utility programs")
+
+    if(NOT ALSOFT_NO_CONFIG_UTIL)
+        add_subdirectory(utils/alsoft-config)
+    endif()
+    message(STATUS "")
+endif()
+
+
+# Add a static library with common functions used by multiple example targets
+add_library(ex-common STATIC EXCLUDE_FROM_ALL
+    examples/common/alhelpers.c
+    examples/common/alhelpers.h)
+target_compile_definitions(ex-common PUBLIC ${CPP_DEFS})
+target_include_directories(ex-common PUBLIC ${OpenAL_SOURCE_DIR}/common)
+target_compile_options(ex-common PUBLIC ${C_FLAGS})
+target_link_libraries(ex-common PUBLIC OpenAL PRIVATE ${RT_LIB})
+set_target_properties(ex-common PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+if(ALSOFT_EXAMPLES)
+    add_executable(altonegen examples/altonegen.c)
+    target_link_libraries(altonegen PRIVATE ${LINKER_FLAGS} ${MATH_LIB} ex-common ${UNICODE_FLAG})
+    set_target_properties(altonegen PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+    add_executable(alrecord examples/alrecord.c)
+    target_link_libraries(alrecord PRIVATE ${LINKER_FLAGS} ex-common ${UNICODE_FLAG})
+    set_target_properties(alrecord PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+    if(ALSOFT_INSTALL_EXAMPLES)
+        set(EXTRA_INSTALLS ${EXTRA_INSTALLS} altonegen alrecord)
+    endif()
+
+    message(STATUS "Building example programs")
+
+    if(SNDFILE_FOUND)
+        add_executable(alplay examples/alplay.c)
+        target_link_libraries(alplay PRIVATE ${LINKER_FLAGS} SndFile::SndFile ex-common
+            ${UNICODE_FLAG})
+        set_target_properties(alplay PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        add_executable(alstream examples/alstream.c)
+        target_link_libraries(alstream PRIVATE ${LINKER_FLAGS} SndFile::SndFile ex-common
+            ${UNICODE_FLAG})
+        set_target_properties(alstream PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        add_executable(alreverb examples/alreverb.c)
+        target_link_libraries(alreverb PRIVATE ${LINKER_FLAGS} SndFile::SndFile ex-common
+            ${UNICODE_FLAG})
+        set_target_properties(alreverb PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        add_executable(almultireverb examples/almultireverb.c)
+        target_link_libraries(almultireverb
+            PRIVATE ${LINKER_FLAGS} SndFile::SndFile ex-common ${MATH_LIB} ${UNICODE_FLAG})
+        set_target_properties(almultireverb PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        add_executable(allatency examples/allatency.c)
+        target_link_libraries(allatency PRIVATE ${LINKER_FLAGS} SndFile::SndFile ex-common
+            ${UNICODE_FLAG})
+        set_target_properties(allatency PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        add_executable(alhrtf examples/alhrtf.c)
+        target_link_libraries(alhrtf
+            PRIVATE ${LINKER_FLAGS} SndFile::SndFile ex-common ${MATH_LIB} ${UNICODE_FLAG})
+        set_target_properties(alhrtf PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        add_executable(alstreamcb examples/alstreamcb.cpp)
+        target_link_libraries(alstreamcb PRIVATE ${LINKER_FLAGS} SndFile::SndFile ex-common
+            ${UNICODE_FLAG})
+        set_target_properties(alstreamcb PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        add_executable(alconvolve examples/alconvolve.c)
+        target_link_libraries(alconvolve PRIVATE ${LINKER_FLAGS} common SndFile::SndFile ex-common
+            ${UNICODE_FLAG})
+        set_target_properties(alconvolve PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        if(ALSOFT_INSTALL_EXAMPLES)
+            set(EXTRA_INSTALLS ${EXTRA_INSTALLS} alplay alstream alreverb almultireverb allatency
+                alhrtf)
+        endif()
+
+        message(STATUS "Building SndFile example programs")
+    endif()
+
+    if(SDL2_FOUND)
+        add_executable(alloopback examples/alloopback.c)
+        target_link_libraries(alloopback
+            PRIVATE ${LINKER_FLAGS} SDL2::SDL2 ex-common ${MATH_LIB})
+        set_target_properties(alloopback PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+        if(ALSOFT_INSTALL_EXAMPLES)
+            set(EXTRA_INSTALLS ${EXTRA_INSTALLS} alloopback)
+        endif()
+
+        message(STATUS "Building SDL example programs")
+
+        set(FFVER_OK FALSE)
+        if(FFMPEG_FOUND)
+            set(FFVER_OK TRUE)
+            if(AVFORMAT_VERSION VERSION_LESS "59.27.100")
+                message(STATUS "libavformat is too old! (${AVFORMAT_VERSION}, wanted 59.27.100)")
+                set(FFVER_OK FALSE)
+            endif()
+            if(AVCODEC_VERSION VERSION_LESS "59.37.100")
+                message(STATUS "libavcodec is too old! (${AVCODEC_VERSION}, wanted 59.37.100)")
+                set(FFVER_OK FALSE)
+            endif()
+            if(AVUTIL_VERSION VERSION_LESS "57.28.100")
+                message(STATUS "libavutil is too old! (${AVUTIL_VERSION}, wanted 57.28.100)")
+                set(FFVER_OK FALSE)
+            endif()
+            if(SWSCALE_VERSION VERSION_LESS "6.7.100")
+                message(STATUS "libswscale is too old! (${SWSCALE_VERSION}, wanted 6.7.100)")
+                set(FFVER_OK FALSE)
+            endif()
+            if(SWRESAMPLE_VERSION VERSION_LESS "4.7.100")
+                message(STATUS "libswresample is too old! (${SWRESAMPLE_VERSION}, wanted 4.7.100)")
+                set(FFVER_OK FALSE)
+            endif()
+        endif()
+        if(FFVER_OK)
+            add_executable(alffplay examples/alffplay.cpp)
+            target_include_directories(alffplay PRIVATE ${FFMPEG_INCLUDE_DIRS})
+            target_link_libraries(alffplay
+                PRIVATE ${LINKER_FLAGS} SDL2::SDL2 ${FFMPEG_LIBRARIES} ex-common)
+            set_target_properties(alffplay PROPERTIES ${DEFAULT_TARGET_PROPS})
+
+            if(ALSOFT_INSTALL_EXAMPLES)
+                set(EXTRA_INSTALLS ${EXTRA_INSTALLS} alffplay)
+            endif()
+            message(STATUS "Building SDL+FFmpeg example programs")
+        endif()
+    endif()
+    message(STATUS "")
+endif()
+
+if(EXTRA_INSTALLS)
+    install(TARGETS ${EXTRA_INSTALLS}
+        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+        BUNDLE  DESTINATION ${CMAKE_INSTALL_BINDIR}
+        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
+endif()
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..8d5d000
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,437 @@
+                  GNU LIBRARY GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL.  It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it.  You can use it for
+your libraries, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library.  If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software.  To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+  Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs.  This
+license, the GNU Library General Public License, applies to certain
+designated libraries.  This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+  The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it.  Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program.  However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+  Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries.  We
+concluded that weaker conditions might promote sharing better.
+
+  However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves.  This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them.  (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.)  The hope is that this
+will lead to faster development of free libraries.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+  Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+
+                  GNU LIBRARY GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License").  Each licensee is
+addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    c) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    d) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Library General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..e4236f8
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,737 @@
+openal-soft-1.23.1:
+
+    Implemented the AL_SOFT_UHJ_ex extension.
+
+    Implemented the AL_SOFT_buffer_length_query extension.
+
+    Implemented the AL_SOFT_source_start_delay extension.
+
+    Implemented the AL_EXT_STATIC_BUFFER extension.
+
+    Fixed compiling with certain older versions of GCC.
+
+    Fixed compiling as a submodule.
+
+    Fixed compiling with newer versions of Oboe.
+
+    Improved EAX effect version switching.
+
+    Improved the quality of the reverb modulator.
+
+    Improved performance of the cubic resampler.
+
+    Added a compatibility option to restore AL_SOFT_buffer_sub_data. The option
+    disables AL_EXT_SOURCE_RADIUS due to incompatibility.
+
+    Reduced CPU usage when EAX is initialized and FXSlot0 or FXSlot1 are not
+    used.
+
+    Reduced memory usage for ADPCM buffer formats. They're no longer converted
+    to 16-bit samples on load.
+
+openal-soft-1.23.0:
+
+    Fixed CoreAudio capture support.
+
+    Fixed handling per-version EAX properties.
+
+    Fixed interpolating changes to the Super Stereo width source property.
+
+    Fixed detection of the update and buffer size from PipeWire.
+
+    Fixed resuming playback devices with OpenSL.
+
+    Fixed support for certain OpenAL implementations with the router.
+
+    Improved reverb environment transitions.
+
+    Improved performance of convolution reverb.
+
+    Improved quality and performance of the pitch shifter effect slightly.
+
+    Improved sub-sample precision for resampled sources.
+
+    Improved blending spatialized multi-channel sources that use the source
+    radius property.
+
+    Improved mixing 2D ambisonic sources for higher-order 3D ambisonic mixing.
+
+    Improved quadraphonic and 7.1 surround sound output slightly.
+
+    Added config options for UHJ encoding/decoding quality. Including Super
+    Stereo processing.
+
+    Added a config option for specifying the speaker distance.
+
+    Added a compatibility config option for specifying the NFC distance
+    scaling.
+
+    Added a config option for mixing on PipeWire's non-real-time thread.
+
+    Added support for virtual source nodes with PipeWire capture.
+
+    Added the ability for the WASAPI backend to use different playback rates.
+
+    Added support for SOFA files that define per-response delays in makemhr.
+
+    Changed the default fallback playback sample rate to 48khz. This doesn't
+    affect most backends, which can detect a default rate from the system.
+
+    Changed the default resampler to cubic.
+
+    Changed the default HRTF size from 32 to 64 points.
+
+openal-soft-1.22.2:
+
+    Fixed PipeWire version check.
+
+    Fixed building with PipeWire versions before 0.3.33.
+
+openal-soft-1.22.1:
+
+    Fixed CoreAudio capture.
+
+    Fixed air absorption strength.
+
+    Fixed handling 5.1 devices on Windows that use Rear channels instead of
+    Side channels.
+
+    Fixed some compilation issues on MinGW.
+
+    Fixed ALSA not being used on some systems without PipeWire and PulseAudio.
+
+    Fixed OpenSL capturing noise.
+
+    Fixed Oboe capture failing with some buffer sizes.
+
+    Added checks for the runtime PipeWire version. The same or newer version
+    than is used for building will be needed at runtime for the backend to
+    work.
+
+    Separated 3D7.1 into its own speaker configuration.
+
+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.
+
+    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.
+
+    Fixed CoreAudio capture when the requested sample rate doesn't match the
+    system configuration.
+
+    Fixed OpenSL capture desyncing from the internal capture buffer.
+
+    Fixed sources missing a batch update when applied after quickly restarting
+    the source.
+
+    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.
+
+    Implemented the AL_SOFT_effect_target extension.
+
+    Implemented the AL_SOFT_events extension.
+
+    Implemented the ALC_SOFT_loopback_bformat extension.
+
+    Improved memory use for mixing voices.
+
+    Improved detection of NEON capabilities.
+
+    Improved handling of PulseAudio devices that lack manual start control.
+
+    Improved mixing performance with PulseAudio.
+
+    Improved high-frequency scaling quality for the HRTF B-Format decoder.
+
+    Improved makemhr's HRIR delay calculation.
+
+    Improved WASAPI capture of mono formats with multichannel input.
+
+    Reimplemented the modulation stage for reverb.
+
+    Enabled real-time mixing priority by default, for backends that use the
+    setting. It can still be disabled in the config file.
+
+    Enabled dual-band processing for the built-in quad and 7.1 output decoders.
+
+    Fixed a potential crash when deleting an effect slot immediately after the
+    last source using it stops.
+
+    Fixed building with the static runtime on MSVC.
+
+    Fixed using source stereo angles outside of -pi...+pi.
+
+    Fixed the buffer processed event count for sources that start with empty
+    buffers.
+
+    Fixed trying to open an unopenable WASAPI device causing all devices to
+    stop working.
+
+    Fixed stale devices when re-enumerating WASAPI devices.
+
+    Fixed using unicode paths with the log file on Windows.
+
+    Fixed DirectSound capture reporting bad sample counts or erroring when
+    reading samples.
+
+    Added an in-progress extension for a callback-driven buffer type.
+
+    Added an in-progress extension for higher-order B-Format buffers.
+
+    Added an in-progress extension for convolution reverb.
+
+    Added an experimental Oboe backend for Android playback. This requires the
+    Oboe sources at build time, so that it's built as a static library included
+    in libopenal.
+
+    Added an option for auto-connecting JACK ports.
+
+    Added greater-than-stereo support to the SoundIO backend.
+
+    Modified the mixer to be fully asynchronous with the external API, and
+    should now be real-time safe. Although alcRenderSamplesSOFT is not due to
+    locking to check the device handle validity.
+
+    Modified the UHJ encoder to use an all-pass FIR filter that's less harmful
+    to non-filtered signal phase.
+
+    Converted examples from SDL_sound to libsndfile. To avoid issues when
+    combining SDL2 and SDL_sound.
+
+    Worked around a 32-bit GCC/MinGW bug with TLS destructors. See:
+    https://gcc.gnu.org/bugzilla/show_bug.cgi?id=83562
+
+    Reduced the maximum number of source sends from 16 to 6.
+
+    Removed the QSA backend. It's been broken for who knows how long.
+
+    Got rid of the compile-time native-tools targets, using cmake and global
+    initialization instead. This should make cross-compiling less troublesome.
+
+openal-soft-1.20.1:
+
+    Implemented the AL_SOFT_direct_channels_remix extension. This extends
+    AL_DIRECT_CHANNELS_SOFT to optionally remix input channels that don't have
+    a matching output channel.
+
+    Implemented the AL_SOFT_bformat_ex extension. This extends B-Format buffer
+    support for N3D or SN3D scaling, or ACN channel ordering.
+
+    Fixed a potential voice leak when a source is started and stopped or
+    restarted in quick succession.
+
+    Fixed a potential device reset failure with JACK.
+
+    Improved handling of unsupported channel configurations with WASAPI. Such
+    setups will now try to output at least a stereo mix.
+
+    Improved clarity a bit for the HRTF second-order ambisonic decoder.
+
+    Improved detection of compatible layouts for SOFA files in makemhr and
+    sofa-info.
+
+    Added the ability to resample HRTFs on load. MHR files no longer need to
+    match the device sample rate to be usable.
+
+    Added an option to limit the HRTF's filter length.
+
+openal-soft-1.20.0:
+
+    Converted the library codebase to C++11. A lot of hacks and custom
+    structures have been replaced with standard or cleaner implementations.
+
+    Partially implemented the Vocal Morpher effect.
+
+    Fixed the bsinc SSE resamplers on non-GCC compilers.
+
+    Fixed OpenSL capture.
+
+    Fixed support for extended capture formats with OpenSL.
+
+    Fixed handling of WASAPI not reporting a default device.
+
+    Fixed performance problems relating to semaphores on macOS.
+
+    Modified the bsinc12 resampler's transition band to better avoid aliasing
+    noise.
+
+    Modified alcResetDeviceSOFT to attempt recovery of disconnected devices.
+
+    Modified the virtual speaker layout for HRTF B-Format decoding.
+
+    Modified the PulseAudio backend to use a custom processing loop.
+
+    Renamed the makehrtf utility to makemhr.
+
+    Improved the efficiency of the bsinc resamplers when up-sampling.
+
+    Improved the quality of the bsinc resamplers slightly.
+
+    Improved the efficiency of the HRTF filters.
+
+    Improved the HRTF B-Format decoder coefficient generation.
+
+    Improved reverb feedback fading to be more consistent with pan fading.
+
+    Improved handling of sources that end prematurely, avoiding loud clicks.
+
+    Improved the performance of some reverb processing loops.
+
+    Added fast_bsinc12 and 24 resamplers that improve efficiency at the cost of
+    some quality. Notably, down-sampling has less smooth pitch ramping.
+
+    Added support for SOFA input files with makemhr.
+
+    Added a build option to use pre-built native tools. For cross-compiling,
+    use with caution and ensure the native tools' binaries are kept up-to-date.
+
+    Added an adjust-latency config option for the PulseAudio backend.
+
+    Added basic support for multi-field HRTFs.
+
+    Added an option for mixing first- or second-order B-Format with HRTF
+    output. This can improve HRTF performance given a number of sources.
+
+    Added an RC file for proper DLL version information.
+
+    Disabled some old KDE workarounds by default. Specifically, PulseAudio
+    streams can now be moved (KDE may try to move them after opening).
+
+openal-soft-1.19.1:
+
+    Implemented capture support for the SoundIO backend.
+
+    Fixed source buffer queues potentially not playing properly when a queue
+    entry completes.
+
+    Fixed possible unexpected failures when generating auxiliary effect slots.
+
+    Fixed a crash with certain reverb or device settings.
+
+    Fixed OpenSL capture.
+
+    Improved output limiter response, better ensuring the sample amplitude is
+    clamped for output.
+
+openal-soft-1.19.0:
+
+    Implemented the ALC_SOFT_device_clock extension.
+
+    Implemented the Pitch Shifter, Frequency Shifter, and Autowah effects.
+
+    Fixed compiling on FreeBSD systems that use freebsd-lib 9.1.
+
+    Fixed compiling on NetBSD.
+
+    Fixed the reverb effect's density scale and panning parameters.
+
+    Fixed use of the WASAPI backend with certain games, which caused odd COM
+    initialization errors.
+
+    Increased the number of virtual channels for decoding Ambisonics to HRTF
+    output.
+
+    Changed 32-bit x86 builds to use SSE2 math by default for performance.
+    Build-time options are available to use just SSE1 or x87 instead.
+
+    Replaced the 4-point Sinc resampler with a more efficient cubic resampler.
+
+    Renamed the MMDevAPI backend to WASAPI.
+
+    Added support for 24-bit, dual-ear HRTF data sets. The built-in data set
+    has been updated to 24-bit.
+
+    Added a 24- to 48-point band-limited Sinc resampler.
+
+    Added an SDL2 playback backend. Disabled by default to avoid a dependency
+    on SDL2.
+
+    Improved the performance and quality of the Chorus and Flanger effects.
+
+    Improved the efficiency of the band-limited Sinc resampler.
+
+    Improved the Sinc resampler's transition band to avoid over-attenuating
+    higher frequencies.
+
+    Improved the performance of some filter operations.
+
+    Improved the efficiency of object ID lookups.
+
+    Improved the efficienty of internal voice/source synchronization.
+
+    Improved AL call error logging with contextualized messages.
+
+    Removed the reverb effect's modulation stage. Due to the lack of reference
+    for its intended behavior and strength.
+
+openal-soft-1.18.2:
+
+    Fixed resetting the FPU rounding mode after certain function calls on
+    Windows.
+
+    Fixed use of SSE intrinsics when building with Clang on Windows.
+
+    Fixed a crash with the JACK backend when using JACK1.
+
+    Fixed use of pthread_setnane_np on NetBSD.
+
+    Fixed building on FreeBSD with an older freebsd-lib.
+
+    OSS now links with libossaudio if found at build time (for NetBSD).
+
+openal-soft-1.18.1:
+
+    Fixed an issue where resuming a source might not restart playing it.
+
+    Fixed PulseAudio playback when the configured stream length is much less
+    than the requested length.
+
+    Fixed MMDevAPI capture with sample rates not matching the backing device.
+
+    Fixed int32 output for the Wave Writer.
+
+    Fixed enumeration of OSS devices that are missing device files.
+
+    Added correct retrieval of the executable's path on FreeBSD.
+
+    Added a config option to specify the dithering depth.
+
+    Added a 5.1 decoder preset that excludes front-center output.
+
+openal-soft-1.18.0:
+
+    Implemented the AL_EXT_STEREO_ANGLES and AL_EXT_SOURCE_RADIUS extensions.
+
+    Implemented the AL_SOFT_gain_clamp_ex, AL_SOFT_source_resampler,
+    AL_SOFT_source_spatialize, and ALC_SOFT_output_limiter extensions.
+
+    Implemented 3D processing for some effects. Currently implemented for
+    Reverb, Compressor, Equalizer, and Ring Modulator.
+
+    Implemented 2-channel UHJ output encoding. This needs to be enabled with a
+    config option to be used.
+
+    Implemented dual-band processing for high-quality ambisonic decoding.
+
+    Implemented distance-compensation for surround sound output.
+
+    Implemented near-field emulation and compensation with ambisonic rendering.
+    Currently only applies when using the high-quality ambisonic decoder or
+    ambisonic output, with appropriate config options.
+
+    Implemented an output limiter to reduce the amount of distortion from
+    clipping.
+
+    Implemented dithering for 8-bit and 16-bit output.
+
+    Implemented a config option to select a preferred HRTF.
+
+    Implemented a run-time check for NEON extensions using /proc/cpuinfo.
+
+    Implemented experimental capture support for the OpenSL backend.
+
+    Fixed building on compilers with NEON support but don't default to having
+    NEON enabled.
+
+    Fixed support for JACK on Windows.
+
+    Fixed starting a source while alcSuspendContext is in effect.
+
+    Fixed detection of headsets as headphones, with MMDevAPI.
+
+    Added support for AmbDec config files, for custom ambisonic decoder
+    configurations. Version 3 files only.
+
+    Added backend-specific options to alsoft-config.
+
+    Added first-, second-, and third-order ambisonic output formats. Currently
+    only works with backends that don't rely on channel labels, like JACK,
+    ALSA, and OSS.
+
+    Added a build option to embed the default HRTFs into the lib.
+
+    Added AmbDec presets to enable high-quality ambisonic decoding.
+
+    Added an AmbDec preset for 3D7.1 speaker setups.
+
+    Added documentation regarding Ambisonics, 3D7.1, AmbDec config files, and
+    the provided ambdec presets.
+
+    Added the ability for MMDevAPI to open devices given a Device ID or GUID
+    string.
+
+    Added an option to the example apps to open a specific device.
+
+    Increased the maximum auxiliary send limit to 16 (up from 4). Requires
+    requesting them with the ALC_MAX_AUXILIARY_SENDS context creation
+    attribute.
+
+    Increased the default auxiliary effect slot count to 64 (up from 4).
+
+    Reduced the default period count to 3 (down from 4).
+
+    Slightly improved automatic naming for enumerated HRTFs.
+
+    Improved B-Format decoding with HRTF output.
+
+    Improved internal property handling for better batching behavior.
+
+    Improved performance of certain filter uses.
+
+    Removed support for the AL_SOFT_buffer_samples and AL_SOFT_buffer_sub_data
+    extensions. Due to conflicts with AL_EXT_SOURCE_RADIUS.
+
+openal-soft-1.17.2:
+
+    Implemented device enumeration for OSSv4.
+
+    Fixed building on OSX.
+
+    Fixed building on non-Windows systems without POSIX-2008.
+
+    Fixed Dedicated Dialog and Dedicated LFE effect output.
+
+    Added a build option to override the share install dir.
+
+    Added a build option to static-link libgcc for MinGW.
+
+openal-soft-1.17.1:
+
+    Fixed building with JACK and without PulseAudio.
+
+    Fixed building on FreeBSD.
+
+    Fixed the ALSA backend's allow-resampler option.
+
+    Fixed handling of inexact ALSA period counts.
+
+    Altered device naming scheme on Windows backends to better match other
+    drivers.
+
+    Updated the CoreAudio backend to use the AudioComponent API. This clears up
+    deprecation warnings for OSX 10.11, although requires OSX 10.6 or newer.
+
+openal-soft-1.17.0:
+
+    Implemented a JACK playback backend.
+
+    Implemented the AL_EXT_BFORMAT and AL_EXT_MULAW_BFORMAT extensions.
+
+    Implemented the ALC_SOFT_HRTF extension.
+
+    Implemented C, SSE3, and SSE4.1 based 4- and 8-point Sinc resamplers.
+
+    Implemented a C and SSE based band-limited Sinc resampler. This does 12- to
+    24-point Sinc resampling, and performs anti-aliasing.
+
+    Implemented B-Format output support for the wave file writer. This creates
+    FuMa-style first-order Ambisonics wave files (AMB format).
+
+    Implemented a stereo-mode config option for treating stereo modes as either
+    speakers or headphones.
+
+    Implemented per-device configuration options.
+
+    Fixed handling of PulseAudio and MMDevAPI devices that have identical
+    descriptions.
+
+    Fixed a potential lockup when stopping playback of suspended PulseAudio devices.
+
+    Fixed logging of Unicode characters on Windows.
+
+    Fixed 5.1 surround sound channels. By default it will now use the side
+    channels for the surround output. A configuration using rear channels is
+    still available.
+
+    Fixed the QSA backend potentially altering the capture format.
+
+    Fixed detecting MMDevAPI's default device.
+
+    Fixed returning the default capture device name.
+
+    Fixed mixing property calculations when deferring context updates.
+
+    Altered the behavior of alcSuspendContext and alcProcessContext to better
+    match certain Windows drivers.
+
+    Altered the panning algorithm, utilizing Ambisonics for better side and
+    back positioning cues with surround sound output.
+
+    Improved support for certain older Windows apps.
+
+    Improved the alffplay example to support surround sound streams.
+
+    Improved support for building as a sub-project.
+
+    Added an HRTF playback example.
+
+    Added a tone generator output test.
+
+    Added a toolchain to help with cross-compiling to Android.
+
+openal-soft-1.16.0:
+
+    Implemented EFX Chorus, Flanger, Distortion, Equalizer, and Compressor
+    effects.
+
+    Implemented high-pass and band-pass EFX filters.
+
+    Implemented the high-pass filter for the EAXReverb effect.
+
+    Implemented SSE2 and SSE4.1 linear resamplers.
+
+    Implemented Neon-enhanced non-HRTF mixers.
+
+    Implemented a QSA backend, for QNX.
+
+    Implemented the ALC_SOFT_pause_device, AL_SOFT_deferred_updates,
+    AL_SOFT_block_alignment, AL_SOFT_MSADPCM, and AL_SOFT_source_length
+    extensions.
+
+    Fixed resetting mmdevapi backend devices.
+
+    Fixed clamping when converting 32-bit float samples to integer.
+
+    Fixed modulation range in the Modulator effect.
+
+    Several fixes for the OpenSL playback backend.
+
+    Fixed device specifier names that have Unicode characters on Windows.
+
+    Added support for filenames and paths with Unicode (UTF-8) characters on
+    Windows.
+
+    Added support for alsoft.conf config files found in XDG Base Directory
+    Specification locations (XDG_CONFIG_DIRS and XDG_CONFIG_HOME, or their
+    defaults) on non-Windows systems.
+
+    Added a GUI configuration utility (requires Qt 4.8).
+
+    Added support for environment variable expansion in config options (not
+    keys or section names).
+
+    Added an example that uses SDL2 and ffmpeg.
+
+    Modified examples to use SDL_sound.
+
+    Modified CMake config option names for better sorting.
+
+    HRTF data sets specified in the hrtf_tables config option may now be
+    relative or absolute filenames.
+
+    Made the default HRTF data set an external file, and added a data set for
+    48khz playback in addition to 44.1khz.
+
+    Added support for C11 atomic methods.
+
+    Improved support for some non-GNU build systems.
+
+openal-soft-1.15.1:
+
+    Fixed a regression with retrieving the source's AL_GAIN property.
+
+openal-soft-1.15:
+
+    Fixed device enumeration with the OSS backend.
+
+    Reorganized internal mixing logic, so unneeded steps can potentially be
+    skipped for better performance.
+
+    Removed the lookup table for calculating the mixing pans. The panning is
+    now calculated directly for better precision.
+
+    Improved the panning of stereo source channels when using stereo output.
+
+    Improved source filter quality on send paths.
+
+    Added a config option to allow PulseAudio to move streams between devices.
+
+    The PulseAudio backend will now attempt to spawn a server by default.
+
+    Added a workaround for a DirectSound bug relating to float32 output.
+
+    Added SSE-based mixers, for HRTF and non-HRTF mixing.
+
+    Added support for the new AL_SOFT_source_latency extension.
+
+    Improved ALSA capture by avoiding an extra buffer when using sizes
+    supported by the underlying device.
+
+    Improved the makehrtf utility to support new options and input formats.
+
+    Modified the CFLAGS declared in the pkg-config file so the "AL/" portion of
+    the header includes can optionally be omitted.
+
+    Added a couple example code programs to show how to apply reverb, and
+    retrieve latency.
+
+    The configuration sample is now installed into the share/openal/ directory
+    instead of /etc/openal.
+
+    The configuration sample now gets installed by default.
diff --git a/OpenALConfig.cmake.in b/OpenALConfig.cmake.in
new file mode 100644 (file)
index 0000000..128c1a4
--- /dev/null
@@ -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@)
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..50b7bfb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,78 @@
+OpenAL Soft
+===========
+
+`master` branch CI status : [![GitHub Actions Status](https://github.com/kcat/openal-soft/actions/workflows/ci.yml/badge.svg)](https://github.com/kcat/openal-soft/actions) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/kcat/openal-soft?branch=master&svg=true)](https://ci.appveyor.com/api/projects/status/github/kcat/openal-soft?branch=master&svg=true)
+
+OpenAL Soft is an LGPL-licensed, cross-platform, software implementation of the OpenAL 3D audio API. It's forked from the open-sourced Windows version available originally from openal.org's SVN repository (now defunct).
+OpenAL provides capabilities for playing audio in a virtual 3D environment. Distance attenuation, doppler shift, and directional sound emitters are among the features handled by the API. More advanced effects, including air absorption, occlusion, and environmental reverb, are available through the EFX extension. It also facilitates streaming audio, multi-channel buffers, and audio capture.
+
+More information is available on the [official website](http://openal-soft.org/).
+
+Source Install
+-------------
+To install OpenAL Soft, use your favorite shell to go into the build/
+directory, and run:
+
+```bash
+cmake ..
+```
+
+Alternatively, you can use any available CMake front-end, like cmake-gui,
+ccmake, or your preferred IDE's CMake project parser.
+
+Assuming configuration went well, you can then build it. The command
+`cmake --build .` will instruct CMake to build the project with the toolchain
+chosen during configuration (often GNU Make or NMake, although others are
+possible).
+
+Please Note: Double check that the appropriate backends were detected. Often,
+complaints of no sound, crashing, and missing devices can be solved by making
+sure the correct backends are being used. CMake's output will identify which
+backends were enabled.
+
+For most systems, you will likely want to make sure PipeWire, PulseAudio, and
+ALSA were detected (if your target system uses them). For Windows, make sure
+WASAPI was detected.
+
+
+Building openal-soft - Using vcpkg
+----------------------------------
+
+You can download and install openal-soft using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager:
+
+    git clone https://github.com/Microsoft/vcpkg.git
+    cd vcpkg
+    ./bootstrap-vcpkg.sh
+    ./vcpkg integrate install
+    ./vcpkg install openal-soft
+
+The openal-soft port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository.
+
+Utilities
+---------
+The source package comes with an informational utility, openal-info, and is
+built by default. It prints out information provided by the ALC and AL sub-
+systems, including discovered devices, version information, and extensions.
+
+
+Configuration
+-------------
+
+OpenAL Soft can be configured on a per-user and per-system basis. This allows
+users and sysadmins to control information provided to applications, as well
+as application-agnostic behavior of the library. See alsoftrc.sample for
+available settings.
+
+
+Acknowledgements
+----------------
+
+Special thanks go to:
+
+ - Creative Labs for the original source code this is based off of.
+ - Christopher Fitzgerald for the current reverb effect implementation, and
+helping with the low-pass and HRTF filters.
+ - Christian Borss for the 3D panning code previous versions used as a base.
+ - Ben Davis for the idea behind a previous version of the click-removal code.
+ - Richard Furse for helping with my understanding of Ambisonics that is used by
+the various parts of the library.
diff --git a/XCompile-Android.txt b/XCompile-Android.txt
new file mode 100644 (file)
index 0000000..693f0ed
--- /dev/null
@@ -0,0 +1,16 @@
+# Cross-compiling for Android is handled by the NDK's own provided toolchain,
+# which as of this writing, should be in
+# ${ndk_root}/build/cmake/android.toolchain.cmake
+#
+# Certain older NDK versions may also need to explicitly pick the libc++
+# runtime. So for example:
+# 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")
diff --git a/XCompile.txt b/XCompile.txt
new file mode 100644 (file)
index 0000000..32706bc
--- /dev/null
@@ -0,0 +1,37 @@
+# Cross-compiling requires CMake 2.6 or newer. Example:
+# cmake .. -DCMAKE_TOOLCHAIN_FILE=../XCompile.txt -DHOST=i686-w64-mingw32
+# Where 'i686-w64-mingw32' is the host prefix for your cross-compiler. If you
+# already have a toolchain file setup, you may use that instead of this file.
+
+# the name of the target operating system
+SET(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+SET(CMAKE_C_COMPILER "${HOST}-gcc")
+SET(CMAKE_CXX_COMPILER "${HOST}-g++")
+SET(CMAKE_RC_COMPILER "${HOST}-windres")
+
+# here is the target environment located
+SET(CMAKE_FIND_ROOT_PATH "/usr/${HOST}")
+
+# here is where stuff gets installed to
+SET(CMAKE_INSTALL_PREFIX "${CMAKE_FIND_ROOT_PATH}" CACHE STRING "Install path prefix, prepended onto install directories." FORCE)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+
+# set env vars so that pkg-config will look in the appropriate directory for
+# .pc files (as there seems to be no way to force using ${HOST}-pkg-config)
+set(ENV{PKG_CONFIG_LIBDIR} "${CMAKE_INSTALL_PREFIX}/lib/pkgconfig")
+set(ENV{PKG_CONFIG_PATH} "")
+
+# Qt4 tools
+SET(QT_QMAKE_EXECUTABLE ${HOST}-qmake)
+SET(QT_MOC_EXECUTABLE ${HOST}-moc)
+SET(QT_RCC_EXECUTABLE ${HOST}-rcc)
+SET(QT_UIC_EXECUTABLE ${HOST}-uic)
+SET(QT_LRELEASE_EXECUTABLE ${HOST}-lrelease)
diff --git a/al/auxeffectslot.cpp b/al/auxeffectslot.cpp
new file mode 100644 (file)
index 0000000..285da1d
--- /dev/null
@@ -0,0 +1,1563 @@
+/**
+ * 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 "auxeffectslot.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <thread>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/efx.h"
+
+#include "albit.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 "buffer.h"
+#include "core/except.h"
+#include "core/fpu_ctrl.h"
+#include "core/logging.h"
+#include "effect.h"
+#include "opthelpers.h"
+
+namespace {
+
+struct FactoryItem {
+    EffectSlotType Type;
+    EffectStateFactory* (&GetFactory)(void);
+};
+constexpr FactoryItem FactoryList[] = {
+    { EffectSlotType::None, NullStateFactory_getFactory },
+    { EffectSlotType::EAXReverb, ReverbStateFactory_getFactory },
+    { EffectSlotType::Reverb, StdReverbStateFactory_getFactory },
+    { EffectSlotType::Autowah, AutowahStateFactory_getFactory },
+    { EffectSlotType::Chorus, ChorusStateFactory_getFactory },
+    { EffectSlotType::Compressor, CompressorStateFactory_getFactory },
+    { EffectSlotType::Distortion, DistortionStateFactory_getFactory },
+    { EffectSlotType::Echo, EchoStateFactory_getFactory },
+    { EffectSlotType::Equalizer, EqualizerStateFactory_getFactory },
+    { EffectSlotType::Flanger, FlangerStateFactory_getFactory },
+    { EffectSlotType::FrequencyShifter, FshifterStateFactory_getFactory },
+    { EffectSlotType::RingModulator, ModulatorStateFactory_getFactory },
+    { EffectSlotType::PitchShifter, PshifterStateFactory_getFactory },
+    { EffectSlotType::VocalMorpher, VmorpherStateFactory_getFactory },
+    { EffectSlotType::DedicatedDialog, DedicatedStateFactory_getFactory },
+    { EffectSlotType::DedicatedLFE, DedicatedStateFactory_getFactory },
+    { EffectSlotType::Convolution, ConvolutionStateFactory_getFactory },
+};
+
+EffectStateFactory *getFactoryByType(EffectSlotType type)
+{
+    auto iter = std::find_if(std::begin(FactoryList), std::end(FactoryList),
+        [type](const FactoryItem &item) noexcept -> bool
+        { return item.Type == type; });
+    return (iter != std::end(FactoryList)) ? iter->GetFactory() : nullptr;
+}
+
+
+inline ALeffectslot *LookupEffectSlot(ALCcontext *context, ALuint id) noexcept
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= context->mEffectSlotList.size()) UNLIKELY
+        return nullptr;
+    EffectSlotSubList &sublist{context->mEffectSlotList[lidx]};
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.EffectSlots + slidx;
+}
+
+inline ALeffect *LookupEffect(ALCdevice *device, ALuint id) noexcept
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= device->EffectList.size()) UNLIKELY
+        return nullptr;
+    EffectSubList &sublist = device->EffectList[lidx];
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Effects + slidx;
+}
+
+inline ALbuffer *LookupBuffer(ALCdevice *device, ALuint id) noexcept
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= device->BufferList.size()) UNLIKELY
+        return nullptr;
+    BufferSubList &sublist = device->BufferList[lidx];
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Buffers + slidx;
+}
+
+
+void AddActiveEffectSlots(const al::span<ALeffectslot*> auxslots, ALCcontext *context)
+{
+    if(auxslots.empty()) return;
+    EffectSlotArray *curarray{context->mActiveAuxSlots.load(std::memory_order_acquire)};
+    size_t newcount{curarray->size() + auxslots.size()};
+
+    /* Insert the new effect slots into the head of the array, followed by the
+     * existing ones.
+     */
+    EffectSlotArray *newarray = EffectSlot::CreatePtrArray(newcount);
+    auto slotiter = std::transform(auxslots.begin(), auxslots.end(), newarray->begin(),
+        [](ALeffectslot *auxslot) noexcept { return auxslot->mSlot; });
+    std::copy(curarray->begin(), curarray->end(), slotiter);
+
+    /* Remove any duplicates (first instance of each will be kept). */
+    auto last = newarray->end();
+    for(auto start=newarray->begin()+1;;)
+    {
+        last = std::remove(start, last, *(start-1));
+        if(start == last) break;
+        ++start;
+    }
+    newcount = static_cast<size_t>(std::distance(newarray->begin(), last));
+
+    /* Reallocate newarray if the new size ended up smaller from duplicate
+     * removal.
+     */
+    if(newcount < newarray->size()) UNLIKELY
+    {
+        curarray = newarray;
+        newarray = EffectSlot::CreatePtrArray(newcount);
+        std::copy_n(curarray->begin(), newcount, newarray->begin());
+        delete curarray;
+        curarray = nullptr;
+    }
+    std::uninitialized_fill_n(newarray->end(), newcount, nullptr);
+
+    curarray = context->mActiveAuxSlots.exchange(newarray, std::memory_order_acq_rel);
+    context->mDevice->waitForMix();
+
+    al::destroy_n(curarray->end(), curarray->size());
+    delete curarray;
+}
+
+void RemoveActiveEffectSlots(const al::span<ALeffectslot*> auxslots, ALCcontext *context)
+{
+    if(auxslots.empty()) return;
+    EffectSlotArray *curarray{context->mActiveAuxSlots.load(std::memory_order_acquire)};
+
+    /* Don't shrink the allocated array size since we don't know how many (if
+     * any) of the effect slots to remove are in the array.
+     */
+    EffectSlotArray *newarray = EffectSlot::CreatePtrArray(curarray->size());
+
+    auto new_end = std::copy(curarray->begin(), curarray->end(), newarray->begin());
+    /* Remove elements from newarray that match any ID in slotids. */
+    for(const ALeffectslot *auxslot : auxslots)
+    {
+        auto slot_match = [auxslot](EffectSlot *slot) noexcept -> bool
+        { return (slot == auxslot->mSlot); };
+        new_end = std::remove_if(newarray->begin(), new_end, slot_match);
+    }
+
+    /* Reallocate with the new size. */
+    auto newsize = static_cast<size_t>(std::distance(newarray->begin(), new_end));
+    if(newsize != newarray->size()) LIKELY
+    {
+        curarray = newarray;
+        newarray = EffectSlot::CreatePtrArray(newsize);
+        std::copy_n(curarray->begin(), newsize, newarray->begin());
+
+        delete curarray;
+        curarray = nullptr;
+    }
+    std::uninitialized_fill_n(newarray->end(), newsize, nullptr);
+
+    curarray = context->mActiveAuxSlots.exchange(newarray, std::memory_order_acq_rel);
+    context->mDevice->waitForMix();
+
+    al::destroy_n(curarray->end(), curarray->size());
+    delete curarray;
+}
+
+
+EffectSlotType EffectSlotTypeFromEnum(ALenum type)
+{
+    switch(type)
+    {
+    case AL_EFFECT_NULL: return EffectSlotType::None;
+    case AL_EFFECT_REVERB: return EffectSlotType::Reverb;
+    case AL_EFFECT_CHORUS: return EffectSlotType::Chorus;
+    case AL_EFFECT_DISTORTION: return EffectSlotType::Distortion;
+    case AL_EFFECT_ECHO: return EffectSlotType::Echo;
+    case AL_EFFECT_FLANGER: return EffectSlotType::Flanger;
+    case AL_EFFECT_FREQUENCY_SHIFTER: return EffectSlotType::FrequencyShifter;
+    case AL_EFFECT_VOCAL_MORPHER: return EffectSlotType::VocalMorpher;
+    case AL_EFFECT_PITCH_SHIFTER: return EffectSlotType::PitchShifter;
+    case AL_EFFECT_RING_MODULATOR: return EffectSlotType::RingModulator;
+    case AL_EFFECT_AUTOWAH: return EffectSlotType::Autowah;
+    case AL_EFFECT_COMPRESSOR: return EffectSlotType::Compressor;
+    case AL_EFFECT_EQUALIZER: return EffectSlotType::Equalizer;
+    case AL_EFFECT_EAXREVERB: return EffectSlotType::EAXReverb;
+    case AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT: return EffectSlotType::DedicatedLFE;
+    case AL_EFFECT_DEDICATED_DIALOGUE: return EffectSlotType::DedicatedDialog;
+    case AL_EFFECT_CONVOLUTION_REVERB_SOFT: return EffectSlotType::Convolution;
+    }
+    ERR("Unhandled effect enum: 0x%04x\n", type);
+    return EffectSlotType::None;
+}
+
+bool EnsureEffectSlots(ALCcontext *context, size_t needed)
+{
+    size_t count{std::accumulate(context->mEffectSlotList.cbegin(),
+        context->mEffectSlotList.cend(), size_t{0},
+        [](size_t cur, const EffectSlotSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<ALuint>(al::popcount(sublist.FreeMask)); })};
+
+    while(needed > count)
+    {
+        if(context->mEffectSlotList.size() >= 1<<25) UNLIKELY
+            return false;
+
+        context->mEffectSlotList.emplace_back();
+        auto sublist = context->mEffectSlotList.end() - 1;
+        sublist->FreeMask = ~0_u64;
+        sublist->EffectSlots = static_cast<ALeffectslot*>(
+            al_calloc(alignof(ALeffectslot), sizeof(ALeffectslot)*64));
+        if(!sublist->EffectSlots) UNLIKELY
+        {
+            context->mEffectSlotList.pop_back();
+            return false;
+        }
+        count += 64;
+    }
+    return true;
+}
+
+ALeffectslot *AllocEffectSlot(ALCcontext *context)
+{
+    auto sublist = std::find_if(context->mEffectSlotList.begin(), context->mEffectSlotList.end(),
+        [](const EffectSlotSubList &entry) noexcept -> bool
+        { 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{al::construct_at(sublist->EffectSlots + slidx, context)};
+    aluInitEffectPanning(slot->mSlot, context);
+
+    /* Add 1 to avoid ID 0. */
+    slot->id = ((lidx<<6) | slidx) + 1;
+
+    context->mNumEffectSlots += 1;
+    sublist->FreeMask &= ~(1_u64 << slidx);
+
+    return slot;
+}
+
+void FreeEffectSlot(ALCcontext *context, ALeffectslot *slot)
+{
+    const ALuint id{slot->id - 1};
+    const size_t lidx{id >> 6};
+    const ALuint slidx{id & 0x3f};
+
+    al::destroy_at(slot);
+
+    context->mEffectSlotList[lidx].FreeMask |= 1_u64 << slidx;
+    context->mNumEffectSlots--;
+}
+
+
+inline void UpdateProps(ALeffectslot *slot, ALCcontext *context)
+{
+    if(!context->mDeferUpdates && slot->mState == SlotState::Playing)
+    {
+        slot->updateProps(context);
+        return;
+    }
+    slot->mPropsDirty = true;
+}
+
+} // namespace
+
+
+AL_API void AL_APIENTRY alGenAuxiliaryEffectSlots(ALsizei n, ALuint *effectslots)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Generating %d effect slots", n);
+    if(n <= 0) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    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)",
+            device->AuxiliaryEffectSlotMax, context->mNumEffectSlots, n);
+        return;
+    }
+    if(!EnsureEffectSlots(context.get(), static_cast<ALuint>(n)))
+    {
+        context->setError(AL_OUT_OF_MEMORY, "Failed to allocate %d effectslot%s", n,
+            (n==1) ? "" : "s");
+        return;
+    }
+
+    if(n == 1)
+    {
+        ALeffectslot *slot{AllocEffectSlot(context.get())};
+        effectslots[0] = slot->id;
+    }
+    else
+    {
+        al::vector<ALuint> ids;
+        ALsizei count{n};
+        ids.reserve(static_cast<ALuint>(count));
+        do {
+            ALeffectslot *slot{AllocEffectSlot(context.get())};
+            ids.emplace_back(slot->id);
+        } while(--count);
+        std::copy(ids.cbegin(), ids.cend(), effectslots);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDeleteAuxiliaryEffectSlots(ALsizei n, const ALuint *effectslots)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Deleting %d effect slots", n);
+    if(n <= 0) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    if(n == 1)
+    {
+        ALeffectslot *slot{LookupEffectSlot(context.get(), effectslots[0])};
+        if(!slot) UNLIKELY
+        {
+            context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslots[0]);
+            return;
+        }
+        if(ReadRef(slot->ref) != 0) UNLIKELY
+        {
+            context->setError(AL_INVALID_OPERATION, "Deleting in-use effect slot %u",
+                effectslots[0]);
+            return;
+        }
+        RemoveActiveEffectSlots({&slot, 1u}, context.get());
+        FreeEffectSlot(context.get(), slot);
+    }
+    else
+    {
+        auto slots = al::vector<ALeffectslot*>(static_cast<ALuint>(n));
+        for(size_t i{0};i < slots.size();++i)
+        {
+            ALeffectslot *slot{LookupEffectSlot(context.get(), effectslots[i])};
+            if(!slot) UNLIKELY
+            {
+                context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslots[i]);
+                return;
+            }
+            if(ReadRef(slot->ref) != 0) UNLIKELY
+            {
+                context->setError(AL_INVALID_OPERATION, "Deleting in-use effect slot %u",
+                    effectslots[i]);
+                return;
+            }
+            slots[i] = slot;
+        }
+        /* Remove any duplicates. */
+        auto slots_end = slots.end();
+        for(auto start=slots.begin()+1;start != slots_end;++start)
+        {
+            slots_end = std::remove(start, slots_end, *(start-1));
+            if(start == slots_end) break;
+        }
+        slots.erase(slots_end, slots.end());
+
+        /* All effectslots are valid, remove and delete them */
+        RemoveActiveEffectSlots(slots, context.get());
+        for(ALeffectslot *slot : slots)
+            FreeEffectSlot(context.get(), slot);
+    }
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alIsAuxiliaryEffectSlot(ALuint effectslot)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(context) LIKELY
+    {
+        std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+        if(LookupEffectSlot(context.get(), effectslot) != nullptr)
+            return AL_TRUE;
+    }
+    return AL_FALSE;
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotPlaySOFT(ALuint slotid)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot{LookupEffectSlot(context.get(), slotid)};
+    if(!slot) UNLIKELY
+    {
+        context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", slotid);
+        return;
+    }
+    if(slot->mState == SlotState::Playing)
+        return;
+
+    slot->mPropsDirty = false;
+    slot->updateProps(context.get());
+
+    AddActiveEffectSlots({&slot, 1}, context.get());
+    slot->mState = SlotState::Playing;
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotPlayvSOFT(ALsizei n, const ALuint *slotids)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Playing %d effect slots", n);
+    if(n <= 0) UNLIKELY return;
+
+    auto slots = al::vector<ALeffectslot*>(static_cast<ALuint>(n));
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    for(size_t i{0};i < slots.size();++i)
+    {
+        ALeffectslot *slot{LookupEffectSlot(context.get(), slotids[i])};
+        if(!slot) UNLIKELY
+        {
+            context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", slotids[i]);
+            return;
+        }
+
+        if(slot->mState != SlotState::Playing)
+        {
+            slot->mPropsDirty = false;
+            slot->updateProps(context.get());
+        }
+        slots[i] = slot;
+    };
+
+    AddActiveEffectSlots(slots, context.get());
+    for(auto slot : slots)
+        slot->mState = SlotState::Playing;
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotStopSOFT(ALuint slotid)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot{LookupEffectSlot(context.get(), slotid)};
+    if(!slot) UNLIKELY
+    {
+        context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", slotid);
+        return;
+    }
+
+    RemoveActiveEffectSlots({&slot, 1}, context.get());
+    slot->mState = SlotState::Stopped;
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotStopvSOFT(ALsizei n, const ALuint *slotids)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Stopping %d effect slots", n);
+    if(n <= 0) UNLIKELY return;
+
+    auto slots = al::vector<ALeffectslot*>(static_cast<ALuint>(n));
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    for(size_t i{0};i < slots.size();++i)
+    {
+        ALeffectslot *slot{LookupEffectSlot(context.get(), slotids[i])};
+        if(!slot) UNLIKELY
+        {
+            context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", slotids[i]);
+            return;
+        }
+
+        slots[i] = slot;
+    };
+
+    RemoveActiveEffectSlots(slots, context.get());
+    for(auto slot : slots)
+        slot->mState = SlotState::Stopped;
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    ALeffectslot *target{};
+    ALCdevice *device{};
+    ALenum err{};
+    switch(param)
+    {
+    case AL_EFFECTSLOT_EFFECT:
+        device = context->mALDevice.get();
+
+        {
+            std::lock_guard<std::mutex> ___{device->EffectLock};
+            ALeffect *effect{value ? LookupEffect(device, static_cast<ALuint>(value)) : nullptr};
+            if(effect)
+                err = slot->initEffect(effect->type, effect->Props, context.get());
+            else
+            {
+                if(value != 0)
+                    return context->setError(AL_INVALID_VALUE, "Invalid effect ID %u", value);
+                err = slot->initEffect(AL_EFFECT_NULL, EffectProps{}, context.get());
+            }
+        }
+        if(err != AL_NO_ERROR) UNLIKELY
+        {
+            context->setError(err, "Effect initialization failed");
+            return;
+        }
+        if(slot->mState == SlotState::Initial) UNLIKELY
+        {
+            slot->mPropsDirty = false;
+            slot->updateProps(context.get());
+
+            AddActiveEffectSlots({&slot, 1}, context.get());
+            slot->mState = SlotState::Playing;
+            return;
+        }
+        break;
+
+    case AL_EFFECTSLOT_AUXILIARY_SEND_AUTO:
+        if(!(value == AL_TRUE || value == AL_FALSE))
+            return context->setError(AL_INVALID_VALUE,
+                "Effect slot auxiliary send auto out of range");
+        if(slot->AuxSendAuto == !!value) UNLIKELY
+            return;
+        slot->AuxSendAuto = !!value;
+        break;
+
+    case AL_EFFECTSLOT_TARGET_SOFT:
+        target = LookupEffectSlot(context.get(), static_cast<ALuint>(value));
+        if(value && !target)
+            return context->setError(AL_INVALID_VALUE, "Invalid effect slot target ID");
+        if(slot->Target == target) UNLIKELY
+            return;
+        if(target)
+        {
+            ALeffectslot *checker{target};
+            while(checker && checker != slot)
+                checker = checker->Target;
+            if(checker)
+                return context->setError(AL_INVALID_OPERATION,
+                    "Setting target of effect slot ID %u to %u creates circular chain", slot->id,
+                    target->id);
+        }
+
+        if(ALeffectslot *oldtarget{slot->Target})
+        {
+            /* We must force an update if there was an existing effect slot
+             * target, in case it's about to be deleted.
+             */
+            if(target) IncrementRef(target->ref);
+            DecrementRef(oldtarget->ref);
+            slot->Target = target;
+            slot->updateProps(context.get());
+            return;
+        }
+
+        if(target) IncrementRef(target->ref);
+        slot->Target = target;
+        break;
+
+    case AL_BUFFER:
+        device = context->mALDevice.get();
+
+        if(slot->mState == SlotState::Playing)
+            return context->setError(AL_INVALID_OPERATION,
+                "Setting buffer on playing effect slot %u", slot->id);
+
+        if(ALbuffer *buffer{slot->Buffer})
+        {
+            if(buffer->id == static_cast<ALuint>(value)) UNLIKELY
+                return;
+        }
+        else if(value == 0) UNLIKELY
+            return;
+
+        {
+            std::lock_guard<std::mutex> ___{device->BufferLock};
+            ALbuffer *buffer{};
+            if(value)
+            {
+                buffer = LookupBuffer(device, static_cast<ALuint>(value));
+                if(!buffer) return context->setError(AL_INVALID_VALUE, "Invalid buffer ID");
+                if(buffer->mCallback)
+                    return context->setError(AL_INVALID_OPERATION,
+                        "Callback buffer not valid for effects");
+
+                IncrementRef(buffer->ref);
+            }
+
+            if(ALbuffer *oldbuffer{slot->Buffer})
+                DecrementRef(oldbuffer->ref);
+            slot->Buffer = buffer;
+
+            FPUCtl mixer_mode{};
+            auto *state = slot->Effect.State.get();
+            state->deviceUpdate(device, buffer);
+        }
+        break;
+
+    case AL_EFFECTSLOT_STATE_SOFT:
+        return context->setError(AL_INVALID_OPERATION, "AL_EFFECTSLOT_STATE_SOFT is read-only");
+
+    default:
+        return context->setError(AL_INVALID_ENUM, "Invalid effect slot integer property 0x%04x",
+            param);
+    }
+    UpdateProps(slot, context.get());
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, const ALint *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_EFFECTSLOT_EFFECT:
+    case AL_EFFECTSLOT_AUXILIARY_SEND_AUTO:
+    case AL_EFFECTSLOT_TARGET_SOFT:
+    case AL_EFFECTSLOT_STATE_SOFT:
+    case AL_BUFFER:
+        alAuxiliaryEffectSloti(effectslot, param, values[0]);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    switch(param)
+    {
+    default:
+        return context->setError(AL_INVALID_ENUM,
+            "Invalid effect slot integer-vector property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    switch(param)
+    {
+    case AL_EFFECTSLOT_GAIN:
+        if(!(value >= 0.0f && value <= 1.0f))
+            return context->setError(AL_INVALID_VALUE, "Effect slot gain out of range");
+        if(slot->Gain == value) UNLIKELY
+            return;
+        slot->Gain = value;
+        break;
+
+    default:
+        return context->setError(AL_INVALID_ENUM, "Invalid effect slot float property 0x%04x",
+            param);
+    }
+    UpdateProps(slot, context.get());
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, const ALfloat *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_EFFECTSLOT_GAIN:
+        alAuxiliaryEffectSlotf(effectslot, param, values[0]);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    switch(param)
+    {
+    default:
+        return context->setError(AL_INVALID_ENUM,
+            "Invalid effect slot float-vector property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    switch(param)
+    {
+    case AL_EFFECTSLOT_AUXILIARY_SEND_AUTO:
+        *value = slot->AuxSendAuto ? AL_TRUE : AL_FALSE;
+        break;
+
+    case AL_EFFECTSLOT_TARGET_SOFT:
+        if(auto *target = slot->Target)
+            *value = static_cast<ALint>(target->id);
+        else
+            *value = 0;
+        break;
+
+    case AL_EFFECTSLOT_STATE_SOFT:
+        *value = static_cast<int>(slot->mState);
+        break;
+
+    case AL_BUFFER:
+        if(auto *buffer = slot->Buffer)
+            *value = static_cast<ALint>(buffer->id);
+        else
+            *value = 0;
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid effect slot integer property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, ALint *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_EFFECTSLOT_EFFECT:
+    case AL_EFFECTSLOT_AUXILIARY_SEND_AUTO:
+    case AL_EFFECTSLOT_TARGET_SOFT:
+    case AL_EFFECTSLOT_STATE_SOFT:
+    case AL_BUFFER:
+        alGetAuxiliaryEffectSloti(effectslot, param, values);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid effect slot integer-vector property 0x%04x",
+            param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    switch(param)
+    {
+    case AL_EFFECTSLOT_GAIN:
+        *value = slot->Gain;
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid effect slot float property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, ALfloat *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_EFFECTSLOT_GAIN:
+        alGetAuxiliaryEffectSlotf(effectslot, param, values);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    ALeffectslot *slot = LookupEffectSlot(context.get(), effectslot);
+    if(!slot) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid effect slot ID %u", effectslot);
+
+    switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid effect slot float-vector property 0x%04x",
+            param);
+    }
+}
+END_API_FUNC
+
+
+ALeffectslot::ALeffectslot(ALCcontext *context)
+{
+    EffectStateFactory *factory{getFactoryByType(EffectSlotType::None)};
+    if(!factory) throw std::runtime_error{"Failed to get null effect factory"};
+
+    al::intrusive_ptr<EffectState> state{factory->create()};
+    Effect.State = state;
+
+    mSlot = context->getEffectSlot();
+    mSlot->InUse = true;
+    mSlot->mEffectState = std::move(state);
+}
+
+ALeffectslot::~ALeffectslot()
+{
+    if(Target)
+        DecrementRef(Target->ref);
+    Target = nullptr;
+    if(Buffer)
+        DecrementRef(Buffer->ref);
+    Buffer = nullptr;
+
+    if(EffectSlotProps *props{mSlot->Update.exchange(nullptr)})
+    {
+        TRACE("Freed unapplied AuxiliaryEffectSlot update %p\n",
+            decltype(std::declval<void*>()){props});
+        delete props;
+    }
+
+    mSlot->mEffectState = nullptr;
+    mSlot->InUse = false;
+}
+
+ALenum ALeffectslot::initEffect(ALenum effectType, const EffectProps &effectProps,
+    ALCcontext *context)
+{
+    EffectSlotType newtype{EffectSlotTypeFromEnum(effectType)};
+    if(newtype != Effect.Type)
+    {
+        EffectStateFactory *factory{getFactoryByType(newtype)};
+        if(!factory)
+        {
+            ERR("Failed to find factory for effect slot type %d\n", static_cast<int>(newtype));
+            return AL_INVALID_ENUM;
+        }
+        al::intrusive_ptr<EffectState> state{factory->create()};
+
+        ALCdevice *device{context->mALDevice.get()};
+        std::unique_lock<std::mutex> statelock{device->StateLock};
+        state->mOutTarget = device->Dry.Buffer;
+        {
+            FPUCtl mixer_mode{};
+            state->deviceUpdate(device, Buffer);
+        }
+
+        Effect.Type = newtype;
+        Effect.Props = effectProps;
+
+        Effect.State = std::move(state);
+    }
+    else if(newtype != EffectSlotType::None)
+        Effect.Props = effectProps;
+
+    /* Remove state references from old effect slot property updates. */
+    EffectSlotProps *props{context->mFreeEffectslotProps.load()};
+    while(props)
+    {
+        props->State = nullptr;
+        props = props->next.load(std::memory_order_relaxed);
+    }
+
+    return AL_NO_ERROR;
+}
+
+void ALeffectslot::updateProps(ALCcontext *context)
+{
+    /* Get an unused property container, or allocate a new one as needed. */
+    EffectSlotProps *props{context->mFreeEffectslotProps.load(std::memory_order_relaxed)};
+    if(!props)
+        props = new EffectSlotProps{};
+    else
+    {
+        EffectSlotProps *next;
+        do {
+            next = props->next.load(std::memory_order_relaxed);
+        } while(context->mFreeEffectslotProps.compare_exchange_weak(props, next,
+                std::memory_order_seq_cst, std::memory_order_acquire) == 0);
+    }
+
+    /* Copy in current property values. */
+    props->Gain = Gain;
+    props->AuxSendAuto = AuxSendAuto;
+    props->Target = Target ? Target->mSlot : nullptr;
+
+    props->Type = Effect.Type;
+    props->Props = Effect.Props;
+    props->State = Effect.State;
+
+    /* Set the new container for updating internal parameters. */
+    props = mSlot->Update.exchange(props, std::memory_order_acq_rel);
+    if(props)
+    {
+        /* If there was an unused update container, put it back in the
+         * freelist.
+         */
+        props->State = nullptr;
+        AtomicReplaceHead(context->mFreeEffectslotProps, props);
+    }
+}
+
+void UpdateAllEffectSlotProps(ALCcontext *context)
+{
+    std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+    for(auto &sublist : context->mEffectSlotList)
+    {
+        uint64_t usemask{~sublist.FreeMask};
+        while(usemask)
+        {
+            const int idx{al::countr_zero(usemask)};
+            usemask &= ~(1_u64 << idx);
+            ALeffectslot *slot{sublist.EffectSlots + idx};
+
+            if(slot->mState != SlotState::Stopped && std::exchange(slot->mPropsDirty, false))
+                slot->updateProps(context);
+        }
+    }
+}
+
+EffectSlotSubList::~EffectSlotSubList()
+{
+    uint64_t usemask{~FreeMask};
+    while(usemask)
+    {
+        const int idx{al::countr_zero(usemask)};
+        al::destroy_at(EffectSlots+idx);
+        usemask &= ~(1_u64 << idx);
+    }
+    FreeMask = ~usemask;
+    al_free(EffectSlots);
+    EffectSlots = nullptr;
+}
+
+#ifdef ALSOFT_EAX
+void ALeffectslot::eax_initialize(ALCcontext& al_context, EaxFxSlotIndexValue index)
+{
+    if(index >= EAX_MAX_FXSLOTS)
+        eax_fail("Index out of range.");
+
+    eax_al_context_ = &al_context;
+    eax_fx_slot_index_ = index;
+    eax_fx_slot_set_defaults();
+
+    eax_effect_ = std::make_unique<EaxEffect>();
+    if(index == 0) eax_effect_->init<EaxReverbCommitter>();
+    else if(index == 1) eax_effect_->init<EaxChorusCommitter>();
+    else eax_effect_->init<EaxNullCommitter>();
+}
+
+void ALeffectslot::eax_commit()
+{
+    if(eax_df_ != EaxDirtyFlags{})
+    {
+        auto df = EaxDirtyFlags{};
+        switch(eax_version_)
+        {
+        case 1:
+        case 2:
+        case 3:
+            eax5_fx_slot_commit(eax123_, df);
+            break;
+        case 4:
+            eax4_fx_slot_commit(df);
+            break;
+        case 5:
+            eax5_fx_slot_commit(eax5_, df);
+            break;
+        }
+        eax_df_ = EaxDirtyFlags{};
+
+        if((df & eax_volume_dirty_bit) != EaxDirtyFlags{})
+            eax_fx_slot_set_volume();
+        if((df & eax_flags_dirty_bit) != EaxDirtyFlags{})
+            eax_fx_slot_set_flags();
+    }
+
+    if(eax_effect_->commit(eax_version_))
+        eax_set_efx_slot_effect(*eax_effect_);
+}
+
+[[noreturn]] void ALeffectslot::eax_fail(const char* message)
+{
+    throw Exception{message};
+}
+
+[[noreturn]] void ALeffectslot::eax_fail_unknown_effect_id()
+{
+    eax_fail("Unknown effect ID.");
+}
+
+[[noreturn]] void ALeffectslot::eax_fail_unknown_property_id()
+{
+    eax_fail("Unknown property ID.");
+}
+
+[[noreturn]] void ALeffectslot::eax_fail_unknown_version()
+{
+    eax_fail("Unknown version.");
+}
+
+void ALeffectslot::eax4_fx_slot_ensure_unlocked() const
+{
+    if(eax4_fx_slot_is_legacy())
+        eax_fail("Locked legacy slot.");
+}
+
+ALenum ALeffectslot::eax_get_efx_effect_type(const GUID& guid)
+{
+    if(guid == EAX_NULL_GUID)
+        return AL_EFFECT_NULL;
+    if(guid == EAX_AUTOWAH_EFFECT)
+        return AL_EFFECT_AUTOWAH;
+    if(guid == EAX_CHORUS_EFFECT)
+        return AL_EFFECT_CHORUS;
+    if(guid == EAX_AGCCOMPRESSOR_EFFECT)
+        return AL_EFFECT_COMPRESSOR;
+    if(guid == EAX_DISTORTION_EFFECT)
+        return AL_EFFECT_DISTORTION;
+    if(guid == EAX_REVERB_EFFECT)
+        return AL_EFFECT_EAXREVERB;
+    if(guid == EAX_ECHO_EFFECT)
+        return AL_EFFECT_ECHO;
+    if(guid == EAX_EQUALIZER_EFFECT)
+        return AL_EFFECT_EQUALIZER;
+    if(guid == EAX_FLANGER_EFFECT)
+        return AL_EFFECT_FLANGER;
+    if(guid == EAX_FREQUENCYSHIFTER_EFFECT)
+        return AL_EFFECT_FREQUENCY_SHIFTER;
+    if(guid == EAX_PITCHSHIFTER_EFFECT)
+        return AL_EFFECT_PITCH_SHIFTER;
+    if(guid == EAX_RINGMODULATOR_EFFECT)
+        return AL_EFFECT_RING_MODULATOR;
+    if(guid == EAX_VOCALMORPHER_EFFECT)
+        return AL_EFFECT_VOCAL_MORPHER;
+
+    eax_fail_unknown_effect_id();
+}
+
+const 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 eax4_fx_slot_is_legacy() ? EAXFXSLOT_LOCKED : EAXFXSLOT_UNLOCKED;
+}
+
+void ALeffectslot::eax4_fx_slot_set_defaults(Eax4Props& props) noexcept
+{
+    props.guidLoadEffect = eax_get_eax_default_effect_guid();
+    props.lVolume = EAXFXSLOT_DEFAULTVOLUME;
+    props.lLock = eax_get_eax_default_lock();
+    props.ulFlags = EAX40FXSLOT_DEFAULTFLAGS;
+}
+
+void ALeffectslot::eax5_fx_slot_set_defaults(Eax5Props& props) noexcept
+{
+    props.guidLoadEffect = eax_get_eax_default_effect_guid();
+    props.lVolume = EAXFXSLOT_DEFAULTVOLUME;
+    props.lLock = EAXFXSLOT_UNLOCKED;
+    props.ulFlags = EAX50FXSLOT_DEFAULTFLAGS;
+    props.lOcclusion = EAXFXSLOT_DEFAULTOCCLUSION;
+    props.flOcclusionLFRatio = EAXFXSLOT_DEFAULTOCCLUSIONLFRATIO;
+}
+
+void ALeffectslot::eax_fx_slot_set_defaults()
+{
+    eax5_fx_slot_set_defaults(eax123_.i);
+    eax4_fx_slot_set_defaults(eax4_.i);
+    eax5_fx_slot_set_defaults(eax5_.i);
+    eax_ = eax5_.i;
+    eax_df_ = EaxDirtyFlags{};
+}
+
+void ALeffectslot::eax4_fx_slot_get(const EaxCall& call, const Eax4Props& props) const
+{
+    switch(call.get_property_id())
+    {
+    case EAXFXSLOT_ALLPARAMETERS:
+        call.set_value<Exception>(props);
+        break;
+    case EAXFXSLOT_LOADEFFECT:
+        call.set_value<Exception>(props.guidLoadEffect);
+        break;
+    case EAXFXSLOT_VOLUME:
+        call.set_value<Exception>(props.lVolume);
+        break;
+    case EAXFXSLOT_LOCK:
+        call.set_value<Exception>(props.lLock);
+        break;
+    case EAXFXSLOT_FLAGS:
+        call.set_value<Exception>(props.ulFlags);
+        break;
+    default:
+        eax_fail_unknown_property_id();
+    }
+}
+
+void ALeffectslot::eax5_fx_slot_get(const EaxCall& call, const Eax5Props& props) const
+{
+    switch(call.get_property_id())
+    {
+    case EAXFXSLOT_ALLPARAMETERS:
+        call.set_value<Exception>(props);
+        break;
+    case EAXFXSLOT_LOADEFFECT:
+        call.set_value<Exception>(props.guidLoadEffect);
+        break;
+    case EAXFXSLOT_VOLUME:
+        call.set_value<Exception>(props.lVolume);
+        break;
+    case EAXFXSLOT_LOCK:
+        call.set_value<Exception>(props.lLock);
+        break;
+    case EAXFXSLOT_FLAGS:
+        call.set_value<Exception>(props.ulFlags);
+        break;
+    case EAXFXSLOT_OCCLUSION:
+        call.set_value<Exception>(props.lOcclusion);
+        break;
+    case EAXFXSLOT_OCCLUSIONLFRATIO:
+        call.set_value<Exception>(props.flOcclusionLFRatio);
+        break;
+    default:
+        eax_fail_unknown_property_id();
+    }
+}
+
+void ALeffectslot::eax_fx_slot_get(const EaxCall& call) const
+{
+    switch(call.get_version())
+    {
+    case 4: eax4_fx_slot_get(call, eax4_.i); break;
+    case 5: eax5_fx_slot_get(call, eax5_.i); break;
+    default: eax_fail_unknown_version();
+    }
+}
+
+bool ALeffectslot::eax_get(const EaxCall& call)
+{
+    switch(call.get_property_set_id())
+    {
+    case EaxCallPropertySetId::fx_slot:
+        eax_fx_slot_get(call);
+        break;
+    case EaxCallPropertySetId::fx_slot_effect:
+        eax_effect_->get(call);
+        break;
+    default:
+        eax_fail_unknown_property_id();
+    }
+
+    return false;
+}
+
+void ALeffectslot::eax_fx_slot_load_effect(int version, ALenum altype)
+{
+    if(!IsValidEffectType(altype))
+        altype = AL_EFFECT_NULL;
+    eax_effect_->set_defaults(version, altype);
+}
+
+void ALeffectslot::eax_fx_slot_set_volume()
+{
+    const auto volume = clamp(eax_.lVolume, EAXFXSLOT_MINVOLUME, EAXFXSLOT_MAXVOLUME);
+    const auto gain = level_mb_to_gain(static_cast<float>(volume));
+    eax_set_efx_slot_gain(gain);
+}
+
+void ALeffectslot::eax_fx_slot_set_environment_flag()
+{
+    eax_set_efx_slot_send_auto((eax_.ulFlags & EAXFXSLOTFLAGS_ENVIRONMENT) != 0u);
+}
+
+void ALeffectslot::eax_fx_slot_set_flags()
+{
+    eax_fx_slot_set_environment_flag();
+}
+
+void ALeffectslot::eax4_fx_slot_set_all(const EaxCall& call)
+{
+    eax4_fx_slot_ensure_unlocked();
+    const auto& src = call.get_value<Exception, const EAX40FXSLOTPROPERTIES>();
+    Eax4AllValidator{}(src);
+    auto& dst = eax4_.i;
+    eax_df_ |= eax_load_effect_dirty_bit; // Always reset the effect.
+    eax_df_ |= (dst.lVolume != src.lVolume ? eax_volume_dirty_bit : EaxDirtyFlags{});
+    eax_df_ |= (dst.lLock != src.lLock ? eax_lock_dirty_bit : EaxDirtyFlags{});
+    eax_df_ |= (dst.ulFlags != src.ulFlags ? eax_flags_dirty_bit : EaxDirtyFlags{});
+    dst = src;
+}
+
+void ALeffectslot::eax5_fx_slot_set_all(const EaxCall& call)
+{
+    const auto& src = call.get_value<Exception, const EAX50FXSLOTPROPERTIES>();
+    Eax5AllValidator{}(src);
+    auto& dst = eax5_.i;
+    eax_df_ |= eax_load_effect_dirty_bit; // Always reset the effect.
+    eax_df_ |= (dst.lVolume != src.lVolume ? eax_volume_dirty_bit : EaxDirtyFlags{});
+    eax_df_ |= (dst.lLock != src.lLock ? eax_lock_dirty_bit : EaxDirtyFlags{});
+    eax_df_ |= (dst.ulFlags != src.ulFlags ? eax_flags_dirty_bit : EaxDirtyFlags{});
+    eax_df_ |= (dst.lOcclusion != src.lOcclusion ? eax_flags_dirty_bit : EaxDirtyFlags{});
+    eax_df_ |= (dst.flOcclusionLFRatio != src.flOcclusionLFRatio ? eax_flags_dirty_bit : EaxDirtyFlags{});
+    dst = src;
+}
+
+bool ALeffectslot::eax_fx_slot_should_update_sources() const noexcept
+{
+    const auto dirty_bits =
+        eax_occlusion_dirty_bit |
+        eax_occlusion_lf_ratio_dirty_bit |
+        eax_flags_dirty_bit;
+
+    if((eax_df_ & dirty_bits) != EaxDirtyFlags{})
+        return true;
+
+    return false;
+}
+
+// Returns `true` if all sources should be updated, or `false` otherwise.
+bool ALeffectslot::eax4_fx_slot_set(const EaxCall& call)
+{
+    auto& dst = eax4_.i;
+
+    switch(call.get_property_id())
+    {
+    case EAXFXSLOT_NONE:
+        break;
+    case EAXFXSLOT_ALLPARAMETERS:
+        eax4_fx_slot_set_all(call);
+        if((eax_df_ & eax_load_effect_dirty_bit))
+            eax_fx_slot_load_effect(4, eax_get_efx_effect_type(dst.guidLoadEffect));
+        break;
+    case EAXFXSLOT_LOADEFFECT:
+        eax4_fx_slot_ensure_unlocked();
+        eax_fx_slot_set_dirty<Eax4GuidLoadEffectValidator, eax_load_effect_dirty_bit>(call, dst.guidLoadEffect, eax_df_);
+        if((eax_df_ & eax_load_effect_dirty_bit))
+            eax_fx_slot_load_effect(4, eax_get_efx_effect_type(dst.guidLoadEffect));
+        break;
+    case EAXFXSLOT_VOLUME:
+        eax_fx_slot_set<Eax4VolumeValidator, eax_volume_dirty_bit>(call, dst.lVolume, eax_df_);
+        break;
+    case EAXFXSLOT_LOCK:
+        eax4_fx_slot_ensure_unlocked();
+        eax_fx_slot_set<Eax4LockValidator, eax_lock_dirty_bit>(call, dst.lLock, eax_df_);
+        break;
+    case EAXFXSLOT_FLAGS:
+        eax_fx_slot_set<Eax4FlagsValidator, eax_flags_dirty_bit>(call, dst.ulFlags, eax_df_);
+        break;
+    default:
+        eax_fail_unknown_property_id();
+    }
+
+    return eax_fx_slot_should_update_sources();
+}
+
+// Returns `true` if all sources should be updated, or `false` otherwise.
+bool ALeffectslot::eax5_fx_slot_set(const EaxCall& call)
+{
+    auto& dst = eax5_.i;
+
+    switch(call.get_property_id())
+    {
+    case EAXFXSLOT_NONE:
+        break;
+    case EAXFXSLOT_ALLPARAMETERS:
+        eax5_fx_slot_set_all(call);
+        if((eax_df_ & eax_load_effect_dirty_bit))
+            eax_fx_slot_load_effect(5, eax_get_efx_effect_type(dst.guidLoadEffect));
+        break;
+    case EAXFXSLOT_LOADEFFECT:
+        eax_fx_slot_set_dirty<Eax4GuidLoadEffectValidator, eax_load_effect_dirty_bit>(call, dst.guidLoadEffect, eax_df_);
+        if((eax_df_ & eax_load_effect_dirty_bit))
+            eax_fx_slot_load_effect(5, eax_get_efx_effect_type(dst.guidLoadEffect));
+        break;
+    case EAXFXSLOT_VOLUME:
+        eax_fx_slot_set<Eax4VolumeValidator, eax_volume_dirty_bit>(call, dst.lVolume, eax_df_);
+        break;
+    case EAXFXSLOT_LOCK:
+        eax_fx_slot_set<Eax4LockValidator, eax_lock_dirty_bit>(call, dst.lLock, eax_df_);
+        break;
+    case EAXFXSLOT_FLAGS:
+        eax_fx_slot_set<Eax5FlagsValidator, eax_flags_dirty_bit>(call, dst.ulFlags, eax_df_);
+        break;
+    case EAXFXSLOT_OCCLUSION:
+        eax_fx_slot_set<Eax5OcclusionValidator, eax_occlusion_dirty_bit>(call, dst.lOcclusion, eax_df_);
+        break;
+    case EAXFXSLOT_OCCLUSIONLFRATIO:
+        eax_fx_slot_set<Eax5OcclusionLfRatioValidator, eax_occlusion_lf_ratio_dirty_bit>(call, dst.flOcclusionLFRatio, eax_df_);
+        break;
+    default:
+        eax_fail_unknown_property_id();
+    }
+
+    return eax_fx_slot_should_update_sources();
+}
+
+// Returns `true` if all sources should be updated, or `false` otherwise.
+bool ALeffectslot::eax_fx_slot_set(const EaxCall& call)
+{
+    switch (call.get_version())
+    {
+    case 4: return eax4_fx_slot_set(call);
+    case 5: return eax5_fx_slot_set(call);
+    default: eax_fail_unknown_version();
+    }
+}
+
+// Returns `true` if all sources should be updated, or `false` otherwise.
+bool ALeffectslot::eax_set(const EaxCall& call)
+{
+    bool ret{false};
+
+    switch(call.get_property_set_id())
+    {
+    case EaxCallPropertySetId::fx_slot: ret = eax_fx_slot_set(call); break;
+    case EaxCallPropertySetId::fx_slot_effect: eax_effect_->set(call); break;
+    default: eax_fail_unknown_property_id();
+    }
+
+    const auto version = call.get_version();
+    if(eax_version_ != version)
+        eax_df_ = ~EaxDirtyFlags{};
+    eax_version_ = version;
+
+    return ret;
+}
+
+void ALeffectslot::eax4_fx_slot_commit(EaxDirtyFlags& dst_df)
+{
+    eax_fx_slot_commit_property<eax_load_effect_dirty_bit>(eax4_, dst_df, &EAX40FXSLOTPROPERTIES::guidLoadEffect);
+    eax_fx_slot_commit_property<eax_volume_dirty_bit>(eax4_, dst_df, &EAX40FXSLOTPROPERTIES::lVolume);
+    eax_fx_slot_commit_property<eax_lock_dirty_bit>(eax4_, dst_df, &EAX40FXSLOTPROPERTIES::lLock);
+    eax_fx_slot_commit_property<eax_flags_dirty_bit>(eax4_, dst_df, &EAX40FXSLOTPROPERTIES::ulFlags);
+
+    auto& dst_i = eax_;
+
+    if(dst_i.lOcclusion != EAXFXSLOT_DEFAULTOCCLUSION) {
+        dst_df |= eax_occlusion_dirty_bit;
+        dst_i.lOcclusion = EAXFXSLOT_DEFAULTOCCLUSION;
+    }
+
+    if(dst_i.flOcclusionLFRatio != EAXFXSLOT_DEFAULTOCCLUSIONLFRATIO) {
+        dst_df |= eax_occlusion_lf_ratio_dirty_bit;
+        dst_i.flOcclusionLFRatio = EAXFXSLOT_DEFAULTOCCLUSIONLFRATIO;
+    }
+}
+
+void ALeffectslot::eax5_fx_slot_commit(Eax5State& state, EaxDirtyFlags& dst_df)
+{
+    eax_fx_slot_commit_property<eax_load_effect_dirty_bit>(state, dst_df, &EAX50FXSLOTPROPERTIES::guidLoadEffect);
+    eax_fx_slot_commit_property<eax_volume_dirty_bit>(state, dst_df, &EAX50FXSLOTPROPERTIES::lVolume);
+    eax_fx_slot_commit_property<eax_lock_dirty_bit>(state, dst_df, &EAX50FXSLOTPROPERTIES::lLock);
+    eax_fx_slot_commit_property<eax_flags_dirty_bit>(state, dst_df, &EAX50FXSLOTPROPERTIES::ulFlags);
+    eax_fx_slot_commit_property<eax_occlusion_dirty_bit>(state, dst_df, &EAX50FXSLOTPROPERTIES::lOcclusion);
+    eax_fx_slot_commit_property<eax_occlusion_lf_ratio_dirty_bit>(state, dst_df, &EAX50FXSLOTPROPERTIES::flOcclusionLFRatio);
+}
+
+void ALeffectslot::eax_set_efx_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;
+    }
+
+    mPropsDirty = true;
+
+#undef EAX_PREFIX
+}
+
+void ALeffectslot::eax_set_efx_slot_send_auto(bool is_send_auto)
+{
+    if(AuxSendAuto == is_send_auto)
+        return;
+
+    AuxSendAuto = is_send_auto;
+    mPropsDirty = true;
+}
+
+void ALeffectslot::eax_set_efx_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);
+    mPropsDirty = true;
+
+#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;
+    }
+
+    return EaxAlEffectSlotUPtr{AllocEffectSlot(&context)};
+
+#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
diff --git a/al/auxeffectslot.h b/al/auxeffectslot.h
new file mode 100644 (file)
index 0000000..3e9a2a4
--- /dev/null
@@ -0,0 +1,368 @@
+#ifndef AL_AUXEFFECTSLOT_H
+#define AL_AUXEFFECTSLOT_H
+
+#include <atomic>
+#include <cstddef>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/efx.h"
+
+#include "alc/device.h"
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "atomic.h"
+#include "core/effectslot.h"
+#include "intrusive_ptr.h"
+#include "vector.h"
+
+#ifdef ALSOFT_EAX
+#include <memory>
+#include "eax/call.h"
+#include "eax/effect.h"
+#include "eax/exception.h"
+#include "eax/fx_slot_index.h"
+#include "eax/utils.h"
+#endif // ALSOFT_EAX
+
+struct ALbuffer;
+struct ALeffect;
+struct WetBuffer;
+
+#ifdef ALSOFT_EAX
+class EaxFxSlotException : public EaxException {
+public:
+       explicit EaxFxSlotException(const char* message)
+               : EaxException{"EAX_FX_SLOT", message}
+       {}
+};
+#endif // ALSOFT_EAX
+
+enum class SlotState : ALenum {
+    Initial = AL_INITIAL,
+    Playing = AL_PLAYING,
+    Stopped = AL_STOPPED,
+};
+
+struct ALeffectslot {
+    float Gain{1.0f};
+    bool  AuxSendAuto{true};
+    ALeffectslot *Target{nullptr};
+    ALbuffer *Buffer{nullptr};
+
+    struct {
+        EffectSlotType Type{EffectSlotType::None};
+        EffectProps Props{};
+
+        al::intrusive_ptr<EffectState> State;
+    } Effect;
+
+    bool mPropsDirty{true};
+
+    SlotState mState{SlotState::Initial};
+
+    RefCount ref{0u};
+
+    EffectSlot *mSlot{nullptr};
+
+    /* Self ID */
+    ALuint id{};
+
+    ALeffectslot(ALCcontext *context);
+    ALeffectslot(const ALeffectslot&) = delete;
+    ALeffectslot& operator=(const ALeffectslot&) = delete;
+    ~ALeffectslot();
+
+    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);
+
+    EaxFxSlotIndexValue eax_get_index() const noexcept { return eax_fx_slot_index_; }
+    const EAX50FXSLOTPROPERTIES& eax_get_eax_fx_slot() const noexcept
+    { return eax_; }
+
+    // Returns `true` if all sources should be updated, or `false` otherwise.
+    bool eax_dispatch(const EaxCall& call)
+    { return call.is_get() ? eax_get(call) : eax_set(call); }
+
+    void eax_commit();
+
+private:
+    static constexpr auto eax_load_effect_dirty_bit = EaxDirtyFlags{1} << 0;
+    static constexpr auto eax_volume_dirty_bit = EaxDirtyFlags{1} << 1;
+    static constexpr auto eax_lock_dirty_bit = EaxDirtyFlags{1} << 2;
+    static constexpr auto eax_flags_dirty_bit = EaxDirtyFlags{1} << 3;
+    static constexpr auto eax_occlusion_dirty_bit = EaxDirtyFlags{1} << 4;
+    static constexpr auto eax_occlusion_lf_ratio_dirty_bit = EaxDirtyFlags{1} << 5;
+
+    using Exception = EaxFxSlotException;
+
+    using Eax4Props = EAX40FXSLOTPROPERTIES;
+
+    struct Eax4State {
+        Eax4Props i; // Immediate.
+    };
+
+    using Eax5Props = EAX50FXSLOTPROPERTIES;
+
+    struct Eax5State {
+        Eax5Props i; // Immediate.
+    };
+
+    struct EaxRangeValidator {
+        template<typename TValue>
+        void operator()(
+            const char* name,
+            const TValue& value,
+            const TValue& min_value,
+            const TValue& max_value) const
+        {
+            eax_validate_range<Exception>(name, value, min_value, max_value);
+        }
+    };
+
+    struct Eax4GuidLoadEffectValidator {
+        void operator()(const GUID& guidLoadEffect) const
+        {
+            if (guidLoadEffect != EAX_NULL_GUID &&
+                guidLoadEffect != EAX_REVERB_EFFECT &&
+                guidLoadEffect != EAX_AGCCOMPRESSOR_EFFECT &&
+                guidLoadEffect != EAX_AUTOWAH_EFFECT &&
+                guidLoadEffect != EAX_CHORUS_EFFECT &&
+                guidLoadEffect != EAX_DISTORTION_EFFECT &&
+                guidLoadEffect != EAX_ECHO_EFFECT &&
+                guidLoadEffect != EAX_EQUALIZER_EFFECT &&
+                guidLoadEffect != EAX_FLANGER_EFFECT &&
+                guidLoadEffect != EAX_FREQUENCYSHIFTER_EFFECT &&
+                guidLoadEffect != EAX_VOCALMORPHER_EFFECT &&
+                guidLoadEffect != EAX_PITCHSHIFTER_EFFECT &&
+                guidLoadEffect != EAX_RINGMODULATOR_EFFECT)
+            {
+                eax_fail_unknown_effect_id();
+            }
+        }
+    };
+
+    struct Eax4VolumeValidator {
+        void operator()(long lVolume) const
+        {
+            EaxRangeValidator{}(
+                "Volume",
+                lVolume,
+                EAXFXSLOT_MINVOLUME,
+                EAXFXSLOT_MAXVOLUME);
+        }
+    };
+
+    struct Eax4LockValidator {
+        void operator()(long lLock) const
+        {
+            EaxRangeValidator{}(
+                "Lock",
+                lLock,
+                EAXFXSLOT_MINLOCK,
+                EAXFXSLOT_MAXLOCK);
+        }
+    };
+
+    struct Eax4FlagsValidator {
+        void operator()(unsigned long ulFlags) const
+        {
+            EaxRangeValidator{}(
+                "Flags",
+                ulFlags,
+                0UL,
+                ~EAX40FXSLOTFLAGS_RESERVED);
+        }
+    };
+
+    struct Eax4AllValidator {
+        void operator()(const EAX40FXSLOTPROPERTIES& all) const
+        {
+            Eax4GuidLoadEffectValidator{}(all.guidLoadEffect);
+            Eax4VolumeValidator{}(all.lVolume);
+            Eax4LockValidator{}(all.lLock);
+            Eax4FlagsValidator{}(all.ulFlags);
+        }
+    };
+
+    struct Eax5OcclusionValidator {
+        void operator()(long lOcclusion) const
+        {
+            EaxRangeValidator{}(
+                "Occlusion",
+                lOcclusion,
+                EAXFXSLOT_MINOCCLUSION,
+                EAXFXSLOT_MAXOCCLUSION);
+        }
+    };
+
+    struct Eax5OcclusionLfRatioValidator {
+        void operator()(float flOcclusionLFRatio) const
+        {
+            EaxRangeValidator{}(
+                "Occlusion LF Ratio",
+                flOcclusionLFRatio,
+                EAXFXSLOT_MINOCCLUSIONLFRATIO,
+                EAXFXSLOT_MAXOCCLUSIONLFRATIO);
+        }
+    };
+
+    struct Eax5FlagsValidator {
+        void operator()(unsigned long ulFlags) const
+        {
+            EaxRangeValidator{}(
+                "Flags",
+                ulFlags,
+                0UL,
+                ~EAX50FXSLOTFLAGS_RESERVED);
+        }
+    };
+
+    struct Eax5AllValidator {
+        void operator()(const EAX50FXSLOTPROPERTIES& all) const
+        {
+            Eax4AllValidator{}(static_cast<const EAX40FXSLOTPROPERTIES&>(all));
+            Eax5OcclusionValidator{}(all.lOcclusion);
+            Eax5OcclusionLfRatioValidator{}(all.flOcclusionLFRatio);
+        }
+    };
+
+    ALCcontext* eax_al_context_{};
+    EaxFxSlotIndexValue eax_fx_slot_index_{};
+    int eax_version_{}; // Current EAX version.
+    EaxDirtyFlags eax_df_{}; // Dirty flags for the current EAX version.
+    EaxEffectUPtr eax_effect_{};
+    Eax5State eax123_{}; // EAX1/EAX2/EAX3 state.
+    Eax4State eax4_{}; // EAX4 state.
+    Eax5State eax5_{}; // EAX5 state.
+    Eax5Props eax_{}; // Current EAX state.
+
+    [[noreturn]] static void eax_fail(const char* message);
+    [[noreturn]] static void eax_fail_unknown_effect_id();
+    [[noreturn]] static void eax_fail_unknown_property_id();
+    [[noreturn]] static void eax_fail_unknown_version();
+
+    // Gets a new value from EAX call,
+    // validates it,
+    // sets a dirty flag only if the new value differs form the old one,
+    // and assigns the new value.
+    template<typename TValidator, EaxDirtyFlags TDirtyBit, typename TProperties>
+    static void eax_fx_slot_set(const EaxCall& call, TProperties& dst, EaxDirtyFlags& dirty_flags)
+    {
+        const auto& src = call.get_value<Exception, const TProperties>();
+        TValidator{}(src);
+        dirty_flags |= (dst != src ? TDirtyBit : EaxDirtyFlags{});
+        dst = src;
+    }
+
+    // Gets a new value from EAX call,
+    // validates it,
+    // sets a dirty flag without comparing the values,
+    // and assigns the new value.
+    template<typename TValidator, EaxDirtyFlags TDirtyBit, typename TProperties>
+    static void eax_fx_slot_set_dirty(const EaxCall& call, TProperties& dst,
+        EaxDirtyFlags& dirty_flags)
+    {
+        const auto& src = call.get_value<Exception, const TProperties>();
+        TValidator{}(src);
+        dirty_flags |= TDirtyBit;
+        dst = src;
+    }
+
+    constexpr bool eax4_fx_slot_is_legacy() const noexcept
+    { return eax_fx_slot_index_ < 2; }
+
+    void eax4_fx_slot_ensure_unlocked() const;
+
+    static ALenum eax_get_efx_effect_type(const GUID& guid);
+    const GUID& eax_get_eax_default_effect_guid() const noexcept;
+    long eax_get_eax_default_lock() const noexcept;
+
+    void eax4_fx_slot_set_defaults(Eax4Props& props) noexcept;
+    void eax5_fx_slot_set_defaults(Eax5Props& props) noexcept;
+    void eax4_fx_slot_set_current_defaults(const Eax4Props& props) noexcept;
+    void eax5_fx_slot_set_current_defaults(const Eax5Props& props) noexcept;
+    void eax_fx_slot_set_current_defaults();
+    void eax_fx_slot_set_defaults();
+
+    void eax4_fx_slot_get(const EaxCall& call, const Eax4Props& props) const;
+    void eax5_fx_slot_get(const EaxCall& call, const Eax5Props& props) const;
+    void eax_fx_slot_get(const EaxCall& call) const;
+    // Returns `true` if all sources should be updated, or `false` otherwise.
+    bool eax_get(const EaxCall& call);
+
+    void eax_fx_slot_load_effect(int version, ALenum altype);
+    void eax_fx_slot_set_volume();
+    void eax_fx_slot_set_environment_flag();
+    void eax_fx_slot_set_flags();
+
+    void eax4_fx_slot_set_all(const EaxCall& call);
+    void eax5_fx_slot_set_all(const EaxCall& call);
+
+    bool eax_fx_slot_should_update_sources() const noexcept;
+
+    // Returns `true` if all sources should be updated, or `false` otherwise.
+    bool eax4_fx_slot_set(const EaxCall& call);
+    // Returns `true` if all sources should be updated, or `false` otherwise.
+    bool eax5_fx_slot_set(const EaxCall& call);
+    // Returns `true` if all sources should be updated, or `false` otherwise.
+    bool eax_fx_slot_set(const EaxCall& call);
+    // Returns `true` if all sources should be updated, or `false` otherwise.
+    bool eax_set(const EaxCall& call);
+
+    template<
+        EaxDirtyFlags TDirtyBit,
+        typename TMemberResult,
+        typename TProps,
+        typename TState>
+    void eax_fx_slot_commit_property(TState& state, EaxDirtyFlags& dst_df,
+        TMemberResult TProps::*member) noexcept
+    {
+        auto& src_i = state.i;
+        auto& dst_i = eax_;
+
+        if((eax_df_ & TDirtyBit) != EaxDirtyFlags{})
+        {
+            dst_df |= TDirtyBit;
+            dst_i.*member = src_i.*member;
+        }
+    }
+
+    void eax4_fx_slot_commit(EaxDirtyFlags& dst_df);
+    void eax5_fx_slot_commit(Eax5State& state, EaxDirtyFlags& dst_df);
+
+    // `alAuxiliaryEffectSloti(effect_slot, AL_EFFECTSLOT_EFFECT, effect)`
+    void eax_set_efx_slot_effect(EaxEffect &effect);
+
+    // `alAuxiliaryEffectSloti(effect_slot, AL_EFFECTSLOT_AUXILIARY_SEND_AUTO, value)`
+    void eax_set_efx_slot_send_auto(bool is_send_auto);
+
+    // `alAuxiliaryEffectSlotf(effect_slot, AL_EFFECTSLOT_GAIN, gain)`
+    void eax_set_efx_slot_gain(ALfloat gain);
+
+public:
+    class EaxDeleter {
+    public:
+        void operator()(ALeffectslot *effect_slot);
+    };
+#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
diff --git a/al/buffer.cpp b/al/buffer.cpp
new file mode 100644 (file)
index 0000000..ee50659
--- /dev/null
@@ -0,0 +1,1692 @@
+/**
+ * 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 "buffer.h"
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cassert>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <mutex>
+#include <new>
+#include <numeric>
+#include <stdexcept>
+#include <utility>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "albit.h"
+#include "albyte.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 "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 {
+
+al::optional<AmbiLayout> AmbiLayoutFromEnum(ALenum layout)
+{
+    switch(layout)
+    {
+    case AL_FUMA_SOFT: return AmbiLayout::FuMa;
+    case AL_ACN_SOFT: return AmbiLayout::ACN;
+    }
+    return al::nullopt;
+}
+ALenum EnumFromAmbiLayout(AmbiLayout layout)
+{
+    switch(layout)
+    {
+    case AmbiLayout::FuMa: return AL_FUMA_SOFT;
+    case AmbiLayout::ACN: return AL_ACN_SOFT;
+    }
+    throw std::runtime_error{"Invalid AmbiLayout: "+std::to_string(int(layout))};
+}
+
+al::optional<AmbiScaling> AmbiScalingFromEnum(ALenum scale)
+{
+    switch(scale)
+    {
+    case AL_FUMA_SOFT: return AmbiScaling::FuMa;
+    case AL_SN3D_SOFT: return AmbiScaling::SN3D;
+    case AL_N3D_SOFT: return AmbiScaling::N3D;
+    }
+    return al::nullopt;
+}
+ALenum EnumFromAmbiScaling(AmbiScaling scale)
+{
+    switch(scale)
+    {
+    case AmbiScaling::FuMa: return AL_FUMA_SOFT;
+    case AmbiScaling::SN3D: 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))};
+}
+
+#ifdef ALSOFT_EAX
+al::optional<EaxStorage> EaxStorageFromEnum(ALenum scale)
+{
+    switch(scale)
+    {
+    case AL_STORAGE_AUTOMATIC: return EaxStorage::Automatic;
+    case AL_STORAGE_ACCESSIBLE: return EaxStorage::Accessible;
+    case AL_STORAGE_HARDWARE: return EaxStorage::Hardware;
+    }
+    return al::nullopt;
+}
+ALenum EnumFromEaxStorage(EaxStorage storage)
+{
+    switch(storage)
+    {
+    case EaxStorage::Automatic: return AL_STORAGE_AUTOMATIC;
+    case EaxStorage::Accessible: return AL_STORAGE_ACCESSIBLE;
+    case EaxStorage::Hardware: return AL_STORAGE_HARDWARE;
+    }
+    throw std::runtime_error{"Invalid EaxStorage: "+std::to_string(int(storage))};
+}
+
+
+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)};
+constexpr ALbitfieldSOFT MAP_READ_WRITE_FLAGS{AL_MAP_READ_BIT_SOFT | AL_MAP_WRITE_BIT_SOFT};
+constexpr ALbitfieldSOFT INVALID_MAP_FLAGS{~unsigned(AL_MAP_READ_BIT_SOFT | AL_MAP_WRITE_BIT_SOFT |
+    AL_MAP_PERSISTENT_BIT_SOFT)};
+
+
+bool EnsureBuffers(ALCdevice *device, size_t needed)
+{
+    size_t count{std::accumulate(device->BufferList.cbegin(), device->BufferList.cend(), size_t{0},
+        [](size_t cur, const BufferSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<ALuint>(al::popcount(sublist.FreeMask)); })};
+
+    while(needed > count)
+    {
+        if(device->BufferList.size() >= 1<<25) UNLIKELY
+            return false;
+
+        device->BufferList.emplace_back();
+        auto sublist = device->BufferList.end() - 1;
+        sublist->FreeMask = ~0_u64;
+        sublist->Buffers = static_cast<ALbuffer*>(al_calloc(alignof(ALbuffer), sizeof(ALbuffer)*64));
+        if(!sublist->Buffers) UNLIKELY
+        {
+            device->BufferList.pop_back();
+            return false;
+        }
+        count += 64;
+    }
+    return true;
+}
+
+ALbuffer *AllocBuffer(ALCdevice *device)
+{
+    auto sublist = std::find_if(device->BufferList.begin(), device->BufferList.end(),
+        [](const BufferSubList &entry) noexcept -> bool
+        { 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{al::construct_at(sublist->Buffers + slidx)};
+
+    /* Add 1 to avoid buffer ID 0. */
+    buffer->id = ((lidx<<6) | slidx) + 1;
+
+    sublist->FreeMask &= ~(1_u64 << slidx);
+
+    return buffer;
+}
+
+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};
+
+    al::destroy_at(buffer);
+
+    device->BufferList[lidx].FreeMask |= 1_u64 << slidx;
+}
+
+inline ALbuffer *LookupBuffer(ALCdevice *device, ALuint id)
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= device->BufferList.size()) UNLIKELY
+        return nullptr;
+    BufferSubList &sublist = device->BufferList[lidx];
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Buffers + slidx;
+}
+
+
+ALuint SanitizeAlignment(FmtType type, ALuint align)
+{
+    if(align == 0)
+    {
+        if(type == FmtIMA4)
+        {
+            /* Here is where things vary:
+             * nVidia and Apple use 64+1 sample frames per block -> block_size=36 bytes per channel
+             * Most PC sound software uses 2040+1 sample frames per block -> block_size=1024 bytes per channel
+             */
+            return 65;
+        }
+        if(type == FmtMSADPCM)
+            return 64;
+        return 1;
+    }
+
+    if(type == FmtIMA4)
+    {
+        /* IMA4 block alignment must be a multiple of 8, plus 1. */
+        if((align&7) == 1) return static_cast<ALuint>(align);
+        return 0;
+    }
+    if(type == FmtMSADPCM)
+    {
+        /* MSADPCM block alignment must be a multiple of 2. */
+        if((align&1) == 0) return static_cast<ALuint>(align);
+        return 0;
+    }
+
+    return static_cast<ALuint>(align);
+}
+
+
+/** Loads the specified data into the buffer, using the specified format. */
+void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
+    const FmtChannels DstChannels, const FmtType DstType, const al::byte *SrcData,
+    ALbitfieldSOFT access)
+{
+    if(ReadRef(ALBuf->ref) != 0 || ALBuf->MappedAccess != 0) UNLIKELY
+        return context->setError(AL_INVALID_OPERATION, "Modifying storage for in-use buffer %u",
+            ALBuf->id);
+
+    const ALuint unpackalign{ALBuf->UnpackAlign};
+    const ALuint align{SanitizeAlignment(DstType, unpackalign)};
+    if(align < 1) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Invalid unpack alignment %u for %s samples",
+            unpackalign, NameFromFormat(DstType));
+
+    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(ALBuf->mChannels != DstChannels || ALBuf->mType != DstType) UNLIKELY
+            return context->setError(AL_INVALID_VALUE, "Preserving data of mismatched format");
+        if(ALBuf->mBlockAlign != align) UNLIKELY
+            return context->setError(AL_INVALID_VALUE, "Preserving data of mismatched alignment");
+        if(ALBuf->mAmbiOrder != ambiorder) UNLIKELY
+            return context->setError(AL_INVALID_VALUE, "Preserving data of mismatched order");
+    }
+
+    /* Convert the size in bytes to blocks using the unpack block alignment. */
+    const ALuint NumChannels{ChannelsFromFmt(DstChannels, ambiorder)};
+    const ALuint BlockSize{NumChannels *
+        ((DstType == FmtIMA4) ? (align-1)/2 + 4 :
+        (DstType == FmtMSADPCM) ? (align-2)/2 + 7 :
+        (align * BytesFromFmt(DstType)))};
+    if((size%BlockSize) != 0) UNLIKELY
+        return context->setError(AL_INVALID_VALUE,
+            "Data size %d is not a multiple of frame size %d (%d unpack alignment)",
+            size, BlockSize, align);
+    const ALuint blocks{size / BlockSize};
+
+    if(blocks > std::numeric_limits<ALsizei>::max()/align) UNLIKELY
+        return context->setError(AL_OUT_OF_MEMORY,
+            "Buffer size overflow, %d blocks x %d samples per block", blocks, align);
+    if(blocks > std::numeric_limits<size_t>::max()/BlockSize) UNLIKELY
+        return context->setError(AL_OUT_OF_MEMORY,
+            "Buffer size overflow, %d frames x %d bytes per frame", blocks, BlockSize);
+
+    const size_t newsize{static_cast<size_t>(blocks) * BlockSize};
+
+#ifdef ALSOFT_EAX
+    if(ALBuf->eax_x_ram_mode == EaxStorage::Hardware)
+    {
+        ALCdevice &device = *context->mALDevice;
+        if(!eax_x_ram_check_availability(device, *ALBuf, size))
+            return context->setError(AL_OUT_OF_MEMORY,
+                "Out of X-RAM memory (avail: %u, needed: %u)", device.eax_x_ram_free_size, size);
+    }
+#endif
+
+    /* This could reallocate only when increasing the size 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 usage, and reporting the real
+     * size could cause problems for apps that use AL_SIZE to try to get the
+     * buffer's play length.
+     */
+    if(newsize != ALBuf->mDataStorage.size())
+    {
+        auto newdata = al::vector<al::byte,16>(newsize, al::byte{});
+        if((access&AL_PRESERVE_DATA_BIT_SOFT))
+        {
+            const size_t tocopy{minz(newdata.size(), ALBuf->mDataStorage.size())};
+            std::copy_n(ALBuf->mDataStorage.begin(), tocopy, newdata.begin());
+        }
+        newdata.swap(ALBuf->mDataStorage);
+    }
+    ALBuf->mData = ALBuf->mDataStorage;
+#ifdef ALSOFT_EAX
+    eax_x_ram_clear(*context->mALDevice, *ALBuf);
+#endif
+
+    if(SrcData != nullptr && !ALBuf->mData.empty())
+        std::copy_n(SrcData, blocks*BlockSize, ALBuf->mData.begin());
+    ALBuf->mBlockAlign = (DstType == FmtIMA4 || DstType == FmtMSADPCM) ? align : 1;
+
+    ALBuf->OriginalSize = size;
+
+    ALBuf->Access = access;
+
+    ALBuf->mSampleRate = static_cast<ALuint>(freq);
+    ALBuf->mChannels = DstChannels;
+    ALBuf->mType = DstType;
+    ALBuf->mAmbiOrder = ambiorder;
+
+    ALBuf->mCallback = nullptr;
+    ALBuf->mUserData = nullptr;
+
+    ALBuf->mSampleLen = blocks * align;
+    ALBuf->mLoopStart = 0;
+    ALBuf->mLoopEnd = ALBuf->mSampleLen;
+
+#ifdef ALSOFT_EAX
+    if(eax_g_is_enabled && ALBuf->eax_x_ram_mode == EaxStorage::Hardware)
+        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,
+    const FmtChannels DstChannels, const FmtType DstType, ALBUFFERCALLBACKTYPESOFT callback,
+    void *userptr)
+{
+    if(ReadRef(ALBuf->ref) != 0 || ALBuf->MappedAccess != 0) UNLIKELY
+        return context->setError(AL_INVALID_OPERATION, "Modifying callback for in-use buffer %u",
+            ALBuf->id);
+
+    const ALuint ambiorder{IsBFormat(DstChannels) ? ALBuf->UnpackAmbiOrder :
+        (IsUHJ(DstChannels) ? 1 : 0)};
+
+    const ALuint unpackalign{ALBuf->UnpackAlign};
+    const ALuint align{SanitizeAlignment(DstType, unpackalign)};
+    const ALuint BlockSize{ChannelsFromFmt(DstChannels, ambiorder) *
+        ((DstType == FmtIMA4) ? (align-1)/2 + 4 :
+        (DstType == FmtMSADPCM) ? (align-2)/2 + 7 :
+        (align * BytesFromFmt(DstType)))};
+
+    /* The maximum number of samples a callback buffer may need to store is a
+     * full mixing line * max pitch * channel count, since it may need to hold
+     * a full line's worth of sample frames before downsampling. An additional
+     * MaxResamplerEdge is needed for "future" samples during resampling (the
+     * voice will hold a history for the past samples).
+     */
+    static constexpr size_t line_size{DeviceBase::MixerLineSize*MaxPitch + MaxResamplerEdge};
+    const size_t line_blocks{(line_size + align-1) / align};
+
+    using BufferVectorType = decltype(ALBuf->mDataStorage);
+    BufferVectorType(line_blocks*BlockSize).swap(ALBuf->mDataStorage);
+    ALBuf->mData = ALBuf->mDataStorage;
+
+#ifdef ALSOFT_EAX
+    eax_x_ram_clear(*context->mALDevice, *ALBuf);
+#endif
+
+    ALBuf->mCallback = callback;
+    ALBuf->mUserData = userptr;
+
+    ALBuf->OriginalSize = 0;
+    ALBuf->Access = 0;
+
+    ALBuf->mBlockAlign = (DstType == FmtIMA4 || DstType == FmtMSADPCM) ? align : 1;
+    ALBuf->mSampleRate = static_cast<ALuint>(freq);
+    ALBuf->mChannels = DstChannels;
+    ALBuf->mType = DstType;
+    ALBuf->mAmbiOrder = ambiorder;
+
+    ALBuf->mSampleLen = 0;
+    ALBuf->mLoopStart = 0;
+    ALBuf->mLoopEnd = ALBuf->mSampleLen;
+}
+
+/** Prepares the buffer to use caller-specified storage. */
+void PrepareUserPtr(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq,
+    const FmtChannels DstChannels, const FmtType DstType, al::byte *sdata, const ALuint sdatalen)
+{
+    if(ReadRef(ALBuf->ref) != 0 || ALBuf->MappedAccess != 0) UNLIKELY
+        return context->setError(AL_INVALID_OPERATION, "Modifying storage for in-use buffer %u",
+            ALBuf->id);
+
+    const ALuint unpackalign{ALBuf->UnpackAlign};
+    const ALuint align{SanitizeAlignment(DstType, unpackalign)};
+    if(align < 1) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Invalid unpack alignment %u for %s samples",
+            unpackalign, NameFromFormat(DstType));
+
+    auto get_type_alignment = [](const FmtType type) noexcept -> ALuint
+    {
+        /* NOTE: This only needs to be the required alignment for the CPU to
+         * read/write the given sample type in the mixer.
+         */
+        switch(type)
+        {
+        case FmtUByte: return alignof(ALubyte);
+        case FmtShort: return alignof(ALshort);
+        case FmtFloat: return alignof(ALfloat);
+        case FmtDouble: return alignof(ALdouble);
+        case FmtMulaw: return alignof(ALubyte);
+        case FmtAlaw: return alignof(ALubyte);
+        case FmtIMA4: break;
+        case FmtMSADPCM: break;
+        }
+        return 1;
+    };
+    const auto typealign = get_type_alignment(DstType);
+    if((reinterpret_cast<uintptr_t>(sdata) & (typealign-1)) != 0)
+        return context->setError(AL_INVALID_VALUE, "Pointer %p is misaligned for %s samples (%u)",
+            static_cast<void*>(sdata), NameFromFormat(DstType), typealign);
+
+    const ALuint ambiorder{IsBFormat(DstChannels) ? ALBuf->UnpackAmbiOrder :
+        (IsUHJ(DstChannels) ? 1 : 0)};
+
+    /* Convert the size in bytes to blocks using the unpack block alignment. */
+    const ALuint NumChannels{ChannelsFromFmt(DstChannels, ambiorder)};
+    const ALuint BlockSize{NumChannels *
+        ((DstType == FmtIMA4) ? (align-1)/2 + 4 :
+        (DstType == FmtMSADPCM) ? (align-2)/2 + 7 :
+        (align * BytesFromFmt(DstType)))};
+    if((sdatalen%BlockSize) != 0) UNLIKELY
+        return context->setError(AL_INVALID_VALUE,
+            "Data size %u is not a multiple of frame size %u (%u unpack alignment)",
+            sdatalen, BlockSize, align);
+    const ALuint blocks{sdatalen / BlockSize};
+
+    if(blocks > std::numeric_limits<ALsizei>::max()/align) UNLIKELY
+        return context->setError(AL_OUT_OF_MEMORY,
+            "Buffer size overflow, %d blocks x %d samples per block", blocks, align);
+    if(blocks > std::numeric_limits<size_t>::max()/BlockSize) UNLIKELY
+        return context->setError(AL_OUT_OF_MEMORY,
+            "Buffer size overflow, %d frames x %d bytes per frame", blocks, BlockSize);
+
+#ifdef ALSOFT_EAX
+    if(ALBuf->eax_x_ram_mode == EaxStorage::Hardware)
+    {
+        ALCdevice &device = *context->mALDevice;
+        if(!eax_x_ram_check_availability(device, *ALBuf, sdatalen))
+            return context->setError(AL_OUT_OF_MEMORY,
+                "Out of X-RAM memory (avail: %u, needed: %u)", device.eax_x_ram_free_size,
+                sdatalen);
+    }
+#endif
+
+    decltype(ALBuf->mDataStorage){}.swap(ALBuf->mDataStorage);
+    ALBuf->mData = {static_cast<al::byte*>(sdata), sdatalen};
+
+#ifdef ALSOFT_EAX
+    eax_x_ram_clear(*context->mALDevice, *ALBuf);
+#endif
+
+    ALBuf->mCallback = nullptr;
+    ALBuf->mUserData = nullptr;
+
+    ALBuf->OriginalSize = sdatalen;
+    ALBuf->Access = 0;
+
+    ALBuf->mBlockAlign = (DstType == FmtIMA4 || DstType == FmtMSADPCM) ? align : 1;
+    ALBuf->mSampleRate = static_cast<ALuint>(freq);
+    ALBuf->mChannels = DstChannels;
+    ALBuf->mType = DstType;
+    ALBuf->mAmbiOrder = ambiorder;
+
+    ALBuf->mSampleLen = blocks * align;
+    ALBuf->mLoopStart = 0;
+    ALBuf->mLoopEnd = ALBuf->mSampleLen;
+
+#ifdef ALSOFT_EAX
+    if(ALBuf->eax_x_ram_mode == EaxStorage::Hardware)
+        eax_x_ram_apply(*context->mALDevice, *ALBuf);
+#endif
+}
+
+
+struct DecompResult { FmtChannels channels; FmtType type; };
+al::optional<DecompResult> DecomposeUserFormat(ALenum format)
+{
+    struct FormatMap {
+        ALenum format;
+        FmtChannels channels;
+        FmtType type;
+    };
+    static const std::array<FormatMap,63> UserFmtList{{
+        { AL_FORMAT_MONO8,             FmtMono, FmtUByte   },
+        { AL_FORMAT_MONO16,            FmtMono, FmtShort   },
+        { AL_FORMAT_MONO_FLOAT32,      FmtMono, FmtFloat   },
+        { AL_FORMAT_MONO_DOUBLE_EXT,   FmtMono, FmtDouble  },
+        { AL_FORMAT_MONO_IMA4,         FmtMono, FmtIMA4    },
+        { AL_FORMAT_MONO_MSADPCM_SOFT, FmtMono, FmtMSADPCM },
+        { AL_FORMAT_MONO_MULAW,        FmtMono, FmtMulaw   },
+        { AL_FORMAT_MONO_ALAW_EXT,     FmtMono, FmtAlaw    },
+
+        { AL_FORMAT_STEREO8,             FmtStereo, FmtUByte   },
+        { AL_FORMAT_STEREO16,            FmtStereo, FmtShort   },
+        { AL_FORMAT_STEREO_FLOAT32,      FmtStereo, FmtFloat   },
+        { AL_FORMAT_STEREO_DOUBLE_EXT,   FmtStereo, FmtDouble  },
+        { AL_FORMAT_STEREO_IMA4,         FmtStereo, FmtIMA4    },
+        { AL_FORMAT_STEREO_MSADPCM_SOFT, FmtStereo, FmtMSADPCM },
+        { AL_FORMAT_STEREO_MULAW,        FmtStereo, FmtMulaw   },
+        { AL_FORMAT_STEREO_ALAW_EXT,     FmtStereo, FmtAlaw    },
+
+        { AL_FORMAT_REAR8,      FmtRear, FmtUByte },
+        { AL_FORMAT_REAR16,     FmtRear, FmtShort },
+        { AL_FORMAT_REAR32,     FmtRear, FmtFloat },
+        { AL_FORMAT_REAR_MULAW, FmtRear, FmtMulaw },
+
+        { AL_FORMAT_QUAD8_LOKI,  FmtQuad, FmtUByte },
+        { AL_FORMAT_QUAD16_LOKI, FmtQuad, FmtShort },
+
+        { AL_FORMAT_QUAD8,      FmtQuad, FmtUByte },
+        { AL_FORMAT_QUAD16,     FmtQuad, FmtShort },
+        { AL_FORMAT_QUAD32,     FmtQuad, FmtFloat },
+        { AL_FORMAT_QUAD_MULAW, FmtQuad, FmtMulaw },
+
+        { AL_FORMAT_51CHN8,      FmtX51, FmtUByte },
+        { AL_FORMAT_51CHN16,     FmtX51, FmtShort },
+        { AL_FORMAT_51CHN32,     FmtX51, FmtFloat },
+        { AL_FORMAT_51CHN_MULAW, FmtX51, FmtMulaw },
+
+        { AL_FORMAT_61CHN8,      FmtX61, FmtUByte },
+        { AL_FORMAT_61CHN16,     FmtX61, FmtShort },
+        { AL_FORMAT_61CHN32,     FmtX61, FmtFloat },
+        { AL_FORMAT_61CHN_MULAW, FmtX61, FmtMulaw },
+
+        { AL_FORMAT_71CHN8,      FmtX71, FmtUByte },
+        { AL_FORMAT_71CHN16,     FmtX71, FmtShort },
+        { AL_FORMAT_71CHN32,     FmtX71, FmtFloat },
+        { AL_FORMAT_71CHN_MULAW, FmtX71, FmtMulaw },
+
+        { AL_FORMAT_BFORMAT2D_8,       FmtBFormat2D, FmtUByte },
+        { AL_FORMAT_BFORMAT2D_16,      FmtBFormat2D, FmtShort },
+        { AL_FORMAT_BFORMAT2D_FLOAT32, FmtBFormat2D, FmtFloat },
+        { AL_FORMAT_BFORMAT2D_MULAW,   FmtBFormat2D, FmtMulaw },
+
+        { AL_FORMAT_BFORMAT3D_8,       FmtBFormat3D, FmtUByte },
+        { AL_FORMAT_BFORMAT3D_16,      FmtBFormat3D, FmtShort },
+        { AL_FORMAT_BFORMAT3D_FLOAT32, FmtBFormat3D, FmtFloat },
+        { AL_FORMAT_BFORMAT3D_MULAW,   FmtBFormat3D, FmtMulaw },
+
+        { AL_FORMAT_UHJ2CHN8_SOFT,        FmtUHJ2, FmtUByte   },
+        { AL_FORMAT_UHJ2CHN16_SOFT,       FmtUHJ2, FmtShort   },
+        { AL_FORMAT_UHJ2CHN_FLOAT32_SOFT, FmtUHJ2, FmtFloat   },
+        { AL_FORMAT_UHJ2CHN_MULAW_SOFT,   FmtUHJ2, FmtMulaw   },
+        { AL_FORMAT_UHJ2CHN_ALAW_SOFT,    FmtUHJ2, FmtAlaw    },
+        { AL_FORMAT_UHJ2CHN_IMA4_SOFT,    FmtUHJ2, FmtIMA4    },
+        { AL_FORMAT_UHJ2CHN_MSADPCM_SOFT, FmtUHJ2, FmtMSADPCM },
+
+        { AL_FORMAT_UHJ3CHN8_SOFT,        FmtUHJ3, FmtUByte },
+        { AL_FORMAT_UHJ3CHN16_SOFT,       FmtUHJ3, FmtShort },
+        { AL_FORMAT_UHJ3CHN_FLOAT32_SOFT, FmtUHJ3, FmtFloat },
+        { AL_FORMAT_UHJ3CHN_MULAW_SOFT,   FmtUHJ3, FmtMulaw },
+        { AL_FORMAT_UHJ3CHN_ALAW_SOFT,    FmtUHJ3, FmtAlaw  },
+
+        { AL_FORMAT_UHJ4CHN8_SOFT,        FmtUHJ4, FmtUByte },
+        { AL_FORMAT_UHJ4CHN16_SOFT,       FmtUHJ4, FmtShort },
+        { AL_FORMAT_UHJ4CHN_FLOAT32_SOFT, FmtUHJ4, FmtFloat },
+        { AL_FORMAT_UHJ4CHN_MULAW_SOFT,   FmtUHJ4, FmtMulaw },
+        { AL_FORMAT_UHJ4CHN_ALAW_SOFT,    FmtUHJ4, FmtAlaw  },
+    }};
+
+    for(const auto &fmt : UserFmtList)
+    {
+        if(fmt.format == format)
+            return al::make_optional<DecompResult>({fmt.channels, fmt.type});
+    }
+    return al::nullopt;
+}
+
+} // namespace
+
+
+AL_API void AL_APIENTRY alGenBuffers(ALsizei n, ALuint *buffers)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Generating %d buffers", n);
+    if(n <= 0) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+    if(!EnsureBuffers(device, static_cast<ALuint>(n)))
+    {
+        context->setError(AL_OUT_OF_MEMORY, "Failed to allocate %d buffer%s", n, (n==1)?"":"s");
+        return;
+    }
+
+    if(n == 1) LIKELY
+    {
+        /* Special handling for the easy and normal case. */
+        ALbuffer *buffer{AllocBuffer(device)};
+        buffers[0] = buffer->id;
+    }
+    else
+    {
+        /* Store the allocated buffer IDs in a separate local list, to avoid
+         * modifying the user storage in case of failure.
+         */
+        al::vector<ALuint> ids;
+        ids.reserve(static_cast<ALuint>(n));
+        do {
+            ALbuffer *buffer{AllocBuffer(device)};
+            ids.emplace_back(buffer->id);
+        } while(--n);
+        std::copy(ids.begin(), ids.end(), buffers);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDeleteBuffers(ALsizei n, const ALuint *buffers)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Deleting %d buffers", n);
+    if(n <= 0) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    /* First try to find any buffers that are invalid or in-use. */
+    auto validate_buffer = [device, &context](const ALuint bid) -> bool
+    {
+        if(!bid) return true;
+        ALbuffer *ALBuf{LookupBuffer(device, bid)};
+        if(!ALBuf) UNLIKELY
+        {
+            context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", bid);
+            return false;
+        }
+        if(ReadRef(ALBuf->ref) != 0) UNLIKELY
+        {
+            context->setError(AL_INVALID_OPERATION, "Deleting in-use buffer %u", bid);
+            return false;
+        }
+        return true;
+    };
+    const ALuint *buffers_end = buffers + n;
+    auto invbuf = std::find_if_not(buffers, buffers_end, validate_buffer);
+    if(invbuf != buffers_end) UNLIKELY return;
+
+    /* All good. Delete non-0 buffer IDs. */
+    auto delete_buffer = [device](const ALuint bid) -> void
+    {
+        ALbuffer *buffer{bid ? LookupBuffer(device, bid) : nullptr};
+        if(buffer) FreeBuffer(device, buffer);
+    };
+    std::for_each(buffers, buffers_end, delete_buffer);
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alIsBuffer(ALuint buffer)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(context) LIKELY
+    {
+        ALCdevice *device{context->mALDevice.get()};
+        std::lock_guard<std::mutex> _{device->BufferLock};
+        if(!buffer || LookupBuffer(device, buffer))
+            return AL_TRUE;
+    }
+    return AL_FALSE;
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alBufferData(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei freq)
+START_API_FUNC
+{ alBufferStorageSOFT(buffer, format, data, size, freq, 0); }
+END_API_FUNC
+
+AL_API void AL_APIENTRY alBufferStorageSOFT(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei freq, ALbitfieldSOFT flags)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(size < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Negative storage size %d", size);
+    else if(freq < 1) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Invalid sample rate %d", freq);
+    else if((flags&INVALID_STORAGE_MASK) != 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Invalid storage flags 0x%x",
+            flags&INVALID_STORAGE_MASK);
+    else if((flags&AL_MAP_PERSISTENT_BIT_SOFT) && !(flags&MAP_READ_WRITE_FLAGS)) UNLIKELY
+        context->setError(AL_INVALID_VALUE,
+            "Declaring persistently mapped storage without read or write access");
+    else
+    {
+        auto usrfmt = DecomposeUserFormat(format);
+        if(!usrfmt) UNLIKELY
+            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
+
+void AL_APIENTRY alBufferDataStatic(const ALuint buffer, ALenum format, ALvoid *data, ALsizei size,
+    ALsizei freq)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    if(size < 0) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Negative storage size %d", size);
+    if(freq < 1) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Invalid sample rate %d", freq);
+
+    auto usrfmt = DecomposeUserFormat(format);
+    if(!usrfmt) UNLIKELY
+        return context->setError(AL_INVALID_ENUM, "Invalid format 0x%04x", format);
+
+    PrepareUserPtr(context.get(), albuf, freq, usrfmt->channels, usrfmt->type,
+        static_cast<al::byte*>(data), static_cast<ALuint>(size));
+}
+END_API_FUNC
+
+AL_API void* AL_APIENTRY alMapBufferSOFT(ALuint buffer, ALsizei offset, ALsizei length, ALbitfieldSOFT access)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return nullptr;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if((access&INVALID_MAP_FLAGS) != 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Invalid map flags 0x%x", access&INVALID_MAP_FLAGS);
+    else if(!(access&MAP_READ_WRITE_FLAGS)) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Mapping buffer %u without read or write access",
+            buffer);
+    else
+    {
+        ALbitfieldSOFT unavailable = (albuf->Access^access) & access;
+        if(ReadRef(albuf->ref) != 0 && !(access&AL_MAP_PERSISTENT_BIT_SOFT)) UNLIKELY
+            context->setError(AL_INVALID_OPERATION,
+                "Mapping in-use buffer %u without persistent mapping", buffer);
+        else if(albuf->MappedAccess != 0) UNLIKELY
+            context->setError(AL_INVALID_OPERATION, "Mapping already-mapped buffer %u", buffer);
+        else if((unavailable&AL_MAP_READ_BIT_SOFT)) UNLIKELY
+            context->setError(AL_INVALID_VALUE,
+                "Mapping buffer %u for reading without read access", buffer);
+        else if((unavailable&AL_MAP_WRITE_BIT_SOFT)) UNLIKELY
+            context->setError(AL_INVALID_VALUE,
+                "Mapping buffer %u for writing without write access", buffer);
+        else if((unavailable&AL_MAP_PERSISTENT_BIT_SOFT)) UNLIKELY
+            context->setError(AL_INVALID_VALUE,
+                "Mapping buffer %u persistently without persistent access", buffer);
+        else if(offset < 0 || length <= 0
+            || static_cast<ALuint>(offset) >= albuf->OriginalSize
+            || static_cast<ALuint>(length) > albuf->OriginalSize - static_cast<ALuint>(offset))
+            UNLIKELY
+            context->setError(AL_INVALID_VALUE, "Mapping invalid range %d+%d for buffer %u",
+                offset, length, buffer);
+        else
+        {
+            void *retval{albuf->mData.data() + offset};
+            albuf->MappedAccess = access;
+            albuf->MappedOffset = offset;
+            albuf->MappedSize = length;
+            return retval;
+        }
+    }
+
+    return nullptr;
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alUnmapBufferSOFT(ALuint buffer)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(albuf->MappedAccess == 0) UNLIKELY
+        context->setError(AL_INVALID_OPERATION, "Unmapping unmapped buffer %u", buffer);
+    else
+    {
+        albuf->MappedAccess = 0;
+        albuf->MappedOffset = 0;
+        albuf->MappedSize = 0;
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alFlushMappedBufferSOFT(ALuint buffer, ALsizei offset, ALsizei length)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!(albuf->MappedAccess&AL_MAP_WRITE_BIT_SOFT)) UNLIKELY
+        context->setError(AL_INVALID_OPERATION, "Flushing buffer %u while not mapped for writing",
+            buffer);
+    else if(offset < albuf->MappedOffset || length <= 0
+        || offset >= albuf->MappedOffset+albuf->MappedSize
+        || length > albuf->MappedOffset+albuf->MappedSize-offset) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Flushing invalid range %d+%d on buffer %u", offset,
+            length, buffer);
+    else
+    {
+        /* FIXME: Need to use some method of double-buffering for the mixer and
+         * app to hold separate memory, which can be safely transfered
+         * asynchronously. Currently we just say the app shouldn't write where
+         * OpenAL's reading, and hope for the best...
+         */
+        std::atomic_thread_fence(std::memory_order_seq_cst);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alBufferSubDataSOFT(ALuint buffer, ALenum format, const ALvoid *data, ALsizei offset, ALsizei length)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+
+    auto usrfmt = DecomposeUserFormat(format);
+    if(!usrfmt) UNLIKELY
+        return context->setError(AL_INVALID_ENUM, "Invalid format 0x%04x", format);
+
+    const ALuint unpack_align{albuf->UnpackAlign};
+    const ALuint align{SanitizeAlignment(usrfmt->type, unpack_align)};
+    if(align < 1) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Invalid unpack alignment %u", unpack_align);
+    if(usrfmt->channels != albuf->mChannels || usrfmt->type != albuf->mType) UNLIKELY
+        return context->setError(AL_INVALID_ENUM, "Unpacking data with mismatched format");
+    if(align != albuf->mBlockAlign) UNLIKELY
+        return context->setError(AL_INVALID_VALUE,
+            "Unpacking data with alignment %u does not match original alignment %u", align,
+            albuf->mBlockAlign);
+    if(albuf->isBFormat() && albuf->UnpackAmbiOrder != albuf->mAmbiOrder) UNLIKELY
+        return context->setError(AL_INVALID_VALUE,
+            "Unpacking data with mismatched ambisonic order");
+    if(albuf->MappedAccess != 0) UNLIKELY
+        return context->setError(AL_INVALID_OPERATION, "Unpacking data into mapped buffer %u",
+            buffer);
+
+    const ALuint num_chans{albuf->channelsFromFmt()};
+    const ALuint byte_align{
+        (albuf->mType == FmtIMA4) ? ((align-1)/2 + 4) * num_chans :
+        (albuf->mType == FmtMSADPCM) ? ((align-2)/2 + 7) * num_chans :
+        (align * albuf->bytesFromFmt() * num_chans)};
+
+    if(offset < 0 || length < 0 || static_cast<ALuint>(offset) > albuf->OriginalSize
+        || static_cast<ALuint>(length) > albuf->OriginalSize-static_cast<ALuint>(offset))
+        UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Invalid data sub-range %d+%d on buffer %u",
+            offset, length, buffer);
+    if((static_cast<ALuint>(offset)%byte_align) != 0) UNLIKELY
+        return context->setError(AL_INVALID_VALUE,
+            "Sub-range offset %d is not a multiple of frame size %d (%d unpack alignment)",
+            offset, byte_align, align);
+    if((static_cast<ALuint>(length)%byte_align) != 0) UNLIKELY
+        return context->setError(AL_INVALID_VALUE,
+            "Sub-range length %d is not a multiple of frame size %d (%d unpack alignment)",
+            length, byte_align, align);
+
+    assert(al::to_underlying(usrfmt->type) == al::to_underlying(albuf->mType));
+    memcpy(albuf->mData.data()+offset, data, static_cast<ALuint>(length));
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alBufferSamplesSOFT(ALuint /*buffer*/, ALuint /*samplerate*/,
+    ALenum /*internalformat*/, ALsizei /*samples*/, ALenum /*channels*/, ALenum /*type*/,
+    const ALvoid* /*data*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    context->setError(AL_INVALID_OPERATION, "alBufferSamplesSOFT not supported");
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alBufferSubSamplesSOFT(ALuint /*buffer*/, ALsizei /*offset*/,
+    ALsizei /*samples*/, ALenum /*channels*/, ALenum /*type*/, const ALvoid* /*data*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    context->setError(AL_INVALID_OPERATION, "alBufferSubSamplesSOFT not supported");
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBufferSamplesSOFT(ALuint /*buffer*/, ALsizei /*offset*/,
+    ALsizei /*samples*/, ALenum /*channels*/, ALenum /*type*/, ALvoid* /*data*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    context->setError(AL_INVALID_OPERATION, "alGetBufferSamplesSOFT not supported");
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alIsBufferFormatSupportedSOFT(ALenum /*format*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return AL_FALSE;
+
+    context->setError(AL_INVALID_OPERATION, "alIsBufferFormatSupportedSOFT not supported");
+    return AL_FALSE;
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alBufferf(ALuint buffer, ALenum param, ALfloat /*value*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer float property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alBuffer3f(ALuint buffer, ALenum param,
+    ALfloat /*value1*/, ALfloat /*value2*/, ALfloat /*value3*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer 3-float property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alBufferfv(ALuint buffer, ALenum param, const ALfloat *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!values) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer float-vector property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alBufferi(ALuint buffer, ALenum param, ALint value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else switch(param)
+    {
+    case AL_UNPACK_BLOCK_ALIGNMENT_SOFT:
+        if(value < 0) UNLIKELY
+            context->setError(AL_INVALID_VALUE, "Invalid unpack block alignment %d", value);
+        else
+            albuf->UnpackAlign = static_cast<ALuint>(value);
+        break;
+
+    case AL_PACK_BLOCK_ALIGNMENT_SOFT:
+        if(value < 0) UNLIKELY
+            context->setError(AL_INVALID_VALUE, "Invalid pack block alignment %d", value);
+        else
+            albuf->PackAlign = static_cast<ALuint>(value);
+        break;
+
+    case AL_AMBISONIC_LAYOUT_SOFT:
+        if(ReadRef(albuf->ref) != 0) UNLIKELY
+            context->setError(AL_INVALID_OPERATION, "Modifying in-use buffer %u's ambisonic layout",
+                buffer);
+        else if(const auto layout = AmbiLayoutFromEnum(value))
+            albuf->mAmbiLayout = layout.value();
+        else UNLIKELY
+            context->setError(AL_INVALID_VALUE, "Invalid unpack ambisonic layout %04x", value);
+        break;
+
+    case AL_AMBISONIC_SCALING_SOFT:
+        if(ReadRef(albuf->ref) != 0) UNLIKELY
+            context->setError(AL_INVALID_OPERATION, "Modifying in-use buffer %u's ambisonic scaling",
+                buffer);
+        else if(const auto scaling = AmbiScalingFromEnum(value))
+            albuf->mAmbiScaling = scaling.value();
+        else UNLIKELY
+            context->setError(AL_INVALID_VALUE, "Invalid unpack ambisonic scaling %04x", value);
+        break;
+
+    case AL_UNPACK_AMBISONIC_ORDER_SOFT:
+        if(value < 1 || value > 14) UNLIKELY
+            context->setError(AL_INVALID_VALUE, "Invalid unpack ambisonic order %d", value);
+        else
+            albuf->UnpackAmbiOrder = static_cast<ALuint>(value);
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer integer property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alBuffer3i(ALuint buffer, ALenum param,
+    ALint /*value1*/, ALint /*value2*/, ALint /*value3*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer 3-integer property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alBufferiv(ALuint buffer, ALenum param, const ALint *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(param)
+        {
+        case AL_UNPACK_BLOCK_ALIGNMENT_SOFT:
+        case AL_PACK_BLOCK_ALIGNMENT_SOFT:
+        case AL_AMBISONIC_LAYOUT_SOFT:
+        case AL_AMBISONIC_SCALING_SOFT:
+        case AL_UNPACK_AMBISONIC_ORDER_SOFT:
+            alBufferi(buffer, param, values[0]);
+            return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!values) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_LOOP_POINTS_SOFT:
+        if(ReadRef(albuf->ref) != 0) UNLIKELY
+            context->setError(AL_INVALID_OPERATION, "Modifying in-use buffer %u's loop points",
+                buffer);
+        else if(values[0] < 0 || values[0] >= values[1]
+            || static_cast<ALuint>(values[1]) > albuf->mSampleLen) UNLIKELY
+            context->setError(AL_INVALID_VALUE, "Invalid loop point range %d -> %d on buffer %u",
+                values[0], values[1], buffer);
+        else
+        {
+            albuf->mLoopStart = static_cast<ALuint>(values[0]);
+            albuf->mLoopEnd = static_cast<ALuint>(values[1]);
+        }
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer integer-vector property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetBufferf(ALuint buffer, ALenum param, ALfloat *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!value) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_SEC_LENGTH_SOFT:
+        *value = (albuf->mSampleRate < 1) ? 0.0f :
+            (static_cast<float>(albuf->mSampleLen) / static_cast<float>(albuf->mSampleRate));
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer float property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBuffer3f(ALuint buffer, ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!value1 || !value2 || !value3) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer 3-float property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBufferfv(ALuint buffer, ALenum param, ALfloat *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_SEC_LENGTH_SOFT:
+        alGetBufferf(buffer, param, values);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!values) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer float-vector property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetBufferi(ALuint buffer, ALenum param, ALint *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!value) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_FREQUENCY:
+        *value = static_cast<ALint>(albuf->mSampleRate);
+        break;
+
+    case AL_BITS:
+        *value = (albuf->mType == FmtIMA4 || albuf->mType == FmtMSADPCM) ? 4
+            : static_cast<ALint>(albuf->bytesFromFmt() * 8);
+        break;
+
+    case AL_CHANNELS:
+        *value = static_cast<ALint>(albuf->channelsFromFmt());
+        break;
+
+    case AL_SIZE:
+        *value = albuf->mCallback ? 0 : static_cast<ALint>(albuf->mData.size());
+        break;
+
+    case AL_BYTE_LENGTH_SOFT:
+        *value = static_cast<ALint>(albuf->mSampleLen / albuf->mBlockAlign
+            * albuf->blockSizeFromFmt());
+        break;
+
+    case AL_SAMPLE_LENGTH_SOFT:
+        *value = static_cast<ALint>(albuf->mSampleLen);
+        break;
+
+    case AL_UNPACK_BLOCK_ALIGNMENT_SOFT:
+        *value = static_cast<ALint>(albuf->UnpackAlign);
+        break;
+
+    case AL_PACK_BLOCK_ALIGNMENT_SOFT:
+        *value = static_cast<ALint>(albuf->PackAlign);
+        break;
+
+    case AL_AMBISONIC_LAYOUT_SOFT:
+        *value = EnumFromAmbiLayout(albuf->mAmbiLayout);
+        break;
+
+    case AL_AMBISONIC_SCALING_SOFT:
+        *value = EnumFromAmbiScaling(albuf->mAmbiScaling);
+        break;
+
+    case AL_UNPACK_AMBISONIC_ORDER_SOFT:
+        *value = static_cast<int>(albuf->UnpackAmbiOrder);
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer integer property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBuffer3i(ALuint buffer, ALenum param, ALint *value1, ALint *value2, ALint *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!value1 || !value2 || !value3) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer 3-integer property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBufferiv(ALuint buffer, ALenum param, ALint *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_FREQUENCY:
+    case AL_BITS:
+    case AL_CHANNELS:
+    case AL_SIZE:
+    case AL_INTERNAL_FORMAT_SOFT:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_UNPACK_BLOCK_ALIGNMENT_SOFT:
+    case AL_PACK_BLOCK_ALIGNMENT_SOFT:
+    case AL_AMBISONIC_LAYOUT_SOFT:
+    case AL_AMBISONIC_SCALING_SOFT:
+    case AL_UNPACK_AMBISONIC_ORDER_SOFT:
+        alGetBufferi(buffer, param, values);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!values) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_LOOP_POINTS_SOFT:
+        values[0] = static_cast<ALint>(albuf->mLoopStart);
+        values[1] = static_cast<ALint>(albuf->mLoopEnd);
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer integer-vector property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alBufferCallbackSOFT(ALuint buffer, ALenum format, ALsizei freq,
+    ALBUFFERCALLBACKTYPESOFT callback, ALvoid *userptr)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(freq < 1) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Invalid sample rate %d", freq);
+    else if(callback == nullptr) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL callback");
+    else
+    {
+        auto usrfmt = DecomposeUserFormat(format);
+        if(!usrfmt) UNLIKELY
+            context->setError(AL_INVALID_ENUM, "Invalid format 0x%04x", format);
+        else
+            PrepareCallback(context.get(), albuf, freq, usrfmt->channels, usrfmt->type, callback,
+                userptr);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBufferPtrSOFT(ALuint buffer, ALenum param, ALvoid **value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+    ALbuffer *albuf = LookupBuffer(device, buffer);
+    if(!albuf) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!value) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_BUFFER_CALLBACK_FUNCTION_SOFT:
+        *value = reinterpret_cast<void*>(albuf->mCallback);
+        break;
+    case AL_BUFFER_CALLBACK_USER_PARAM_SOFT:
+        *value = albuf->mUserData;
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer pointer property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBuffer3PtrSOFT(ALuint buffer, ALenum param, ALvoid **value1, ALvoid **value2, ALvoid **value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!value1 || !value2 || !value3) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer 3-pointer property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBufferPtrvSOFT(ALuint buffer, ALenum param, ALvoid **values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_BUFFER_CALLBACK_FUNCTION_SOFT:
+    case AL_BUFFER_CALLBACK_USER_PARAM_SOFT:
+        alGetBufferPtrSOFT(buffer, param, values);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->BufferLock};
+    if(LookupBuffer(device, buffer) == nullptr) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
+    else if(!values) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid buffer pointer-vector property 0x%04x", param);
+    }
+}
+END_API_FUNC
+
+
+BufferSubList::~BufferSubList()
+{
+    uint64_t usemask{~FreeMask};
+    while(usemask)
+    {
+        const int idx{al::countr_zero(usemask)};
+        al::destroy_at(Buffers+idx);
+        usemask &= ~(1_u64 << idx);
+    }
+    FreeMask = ~usemask;
+    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;
+    }
+
+    const auto storage = EaxStorageFromEnum(value);
+    if(!storage)
+    {
+        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 bufid = buffers[i];
+        if(bufid == AL_NONE)
+            continue;
+
+        const auto buffer = LookupBuffer(device, bufid);
+        if(!buffer) UNLIKELY
+        {
+            ERR(EAX_PREFIX "Invalid buffer ID %u.\n", bufid);
+            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(*storage == EaxStorage::Hardware && !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(std::numeric_limits<size_t>::max()-buffer->OriginalSize < total_needed) UNLIKELY
+            {
+                context->setError(AL_OUT_OF_MEMORY, EAX_PREFIX "Size overflow (%u + %zu)\n",
+                    buffer->OriginalSize, total_needed);
+                return ALC_FALSE;
+            }
+            total_needed += buffer->OriginalSize;
+        }
+    }
+    if(total_needed > device->eax_x_ram_free_size)
+    {
+        context->setError(AL_OUT_OF_MEMORY,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 bufid = buffers[i];
+        if(bufid == AL_NONE)
+            continue;
+
+        const auto buffer = LookupBuffer(device, bufid);
+        assert(buffer);
+
+        if(*storage == EaxStorage::Hardware)
+            eax_x_ram_apply(*device, *buffer);
+        else
+            eax_x_ram_clear(*device, *buffer);
+        buffer->eax_x_ram_mode = *storage;
+    }
+
+    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 EnumFromEaxStorage(al_buffer->eax_x_ram_mode);
+
+#undef EAX_PREFIX
+}
+END_API_FUNC
+
+#endif // ALSOFT_EAX
diff --git a/al/buffer.h b/al/buffer.h
new file mode 100644 (file)
index 0000000..64ebe1f
--- /dev/null
@@ -0,0 +1,58 @@
+#ifndef AL_BUFFER_H
+#define AL_BUFFER_H
+
+#include <atomic>
+
+#include "AL/al.h"
+
+#include "albyte.h"
+#include "alc/inprogext.h"
+#include "almalloc.h"
+#include "atomic.h"
+#include "core/buffer_storage.h"
+#include "vector.h"
+
+#ifdef ALSOFT_EAX
+#include "eax/x_ram.h"
+
+enum class EaxStorage : uint8_t {
+    Automatic,
+    Accessible,
+    Hardware
+};
+#endif // ALSOFT_EAX
+
+
+struct ALbuffer : public BufferStorage {
+    ALbitfieldSOFT Access{0u};
+
+    al::vector<al::byte,16> mDataStorage;
+
+    ALuint OriginalSize{0};
+
+    ALuint UnpackAlign{0};
+    ALuint PackAlign{0};
+    ALuint UnpackAmbiOrder{1};
+
+    ALbitfieldSOFT MappedAccess{0u};
+    ALsizei MappedOffset{0};
+    ALsizei MappedSize{0};
+
+    ALuint mLoopStart{0u};
+    ALuint mLoopEnd{0u};
+
+    /* Number of times buffer was attached to a source (deletion can only occur when 0) */
+    RefCount ref{0u};
+
+    /* Self ID */
+    ALuint id{0};
+
+    DISABLE_ALLOC()
+
+#ifdef ALSOFT_EAX
+    EaxStorage eax_x_ram_mode{EaxStorage::Automatic};
+    bool eax_x_ram_is_hardware{};
+#endif // ALSOFT_EAX
+};
+
+#endif
diff --git a/al/eax/api.cpp b/al/eax/api.cpp
new file mode 100644 (file)
index 0000000..f0809df
--- /dev/null
@@ -0,0 +1,1621 @@
+//
+// 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 "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}
+};
+
+
+const GUID EAX40CONTEXT_DEFAULTPRIMARYFXSLOTID = EAXPROPERTYID_EAX40_FXSlot0;
+const GUID EAX50CONTEXT_DEFAULTPRIMARYFXSLOTID = EAXPROPERTYID_EAX50_FXSlot0;
+
+const EAX40ACTIVEFXSLOTS EAX40SOURCE_DEFAULTACTIVEFXSLOTID = EAX40ACTIVEFXSLOTS
+{{
+    EAX_NULL_GUID,
+    EAXPROPERTYID_EAX40_FXSlot0,
+}};
+
+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,
+}};
+
+
+// EAX1 =====================================================================
+
+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,
+}};
+
+// EAX2 =====================================================================
+
+namespace {
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_GENERIC{
+    EAX2LISTENER_DEFAULTROOM,
+    EAX2LISTENER_DEFAULTROOMHF,
+    EAX2LISTENER_DEFAULTROOMROLLOFFFACTOR,
+    EAX2LISTENER_DEFAULTDECAYTIME,
+    EAX2LISTENER_DEFAULTDECAYHFRATIO,
+    EAX2LISTENER_DEFAULTREFLECTIONS,
+    EAX2LISTENER_DEFAULTREFLECTIONSDELAY,
+    EAX2LISTENER_DEFAULTREVERB,
+    EAX2LISTENER_DEFAULTREVERBDELAY,
+    EAX2LISTENER_DEFAULTENVIRONMENT,
+    EAX2LISTENER_DEFAULTENVIRONMENTSIZE,
+    EAX2LISTENER_DEFAULTENVIRONMENTDIFFUSION,
+    EAX2LISTENER_DEFAULTAIRABSORPTIONHF,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_PADDEDCELL{
+    -1'000L,
+    -6'000L,
+    0.0F,
+    0.17F,
+    0.1F,
+    -1'204L,
+    0.001F,
+    207L,
+    0.002F,
+    EAX2_ENVIRONMENT_PADDEDCELL,
+    1.4F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_ROOM{
+    -1'000L,
+    -454L,
+    0.0F,
+    0.4F,
+    0.83F,
+    -1'646L,
+    0.002F,
+    53L,
+    0.003F,
+    EAX2_ENVIRONMENT_ROOM,
+    1.9F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_BATHROOM{
+    -1'000L,
+    -1'200L,
+    0.0F,
+    1.49F,
+    0.54F,
+    -370L,
+    0.007F,
+    1'030L,
+    0.011F,
+    EAX2_ENVIRONMENT_BATHROOM,
+    1.4F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_LIVINGROOM{
+    -1'000L,
+    -6'000L,
+    0.0F,
+    0.5F,
+    0.1F,
+    -1'376L,
+    0.003F,
+    -1'104L,
+    0.004F,
+    EAX2_ENVIRONMENT_LIVINGROOM,
+    2.5F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_STONEROOM{
+    -1'000L,
+    -300L,
+    0.0F,
+    2.31F,
+    0.64F,
+    -711L,
+    0.012F,
+    83L,
+    0.017F,
+    EAX2_ENVIRONMENT_STONEROOM,
+    11.6F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_AUDITORIUM{
+    -1'000L,
+    -476L,
+    0.0F,
+    4.32F,
+    0.59F,
+    -789L,
+    0.02F,
+    -289L,
+    0.03F,
+    EAX2_ENVIRONMENT_AUDITORIUM,
+    21.6F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_CONCERTHALL{
+    -1'000L,
+    -500L,
+    0.0F,
+    3.92F,
+    0.7F,
+    -1'230L,
+    0.02F,
+    -2L,
+    0.029F,
+    EAX2_ENVIRONMENT_CONCERTHALL,
+    19.6F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_CAVE{
+    -1'000L,
+    0L,
+    0.0F,
+    2.91F,
+    1.3F,
+    -602L,
+    0.015F,
+    -302L,
+    0.022F,
+    EAX2_ENVIRONMENT_CAVE,
+    14.6F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENERFLAGS_DECAYTIMESCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSSCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE |
+        EAX2LISTENERFLAGS_REVERBSCALE |
+        EAX2LISTENERFLAGS_REVERBDELAYSCALE,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_ARENA{
+    -1'000L,
+    -698L,
+    0.0F,
+    7.24F,
+    0.33F,
+    -1'166L,
+    0.02F,
+    16L,
+    0.03F,
+    EAX2_ENVIRONMENT_ARENA,
+    36.2F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_HANGAR{
+    -1'000L,
+    -1'000L,
+    0.0F,
+    10.05F,
+    0.23F,
+    -602L,
+    0.02F,
+    198L,
+    0.03F,
+    EAX2_ENVIRONMENT_HANGAR,
+    50.3F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_CARPETTEDHALLWAY{
+    -1'000L,
+    -4'000L,
+    0.0F,
+    0.3F,
+    0.1F,
+    -1'831L,
+    0.002F,
+    -1'630L,
+    0.03F,
+    EAX2_ENVIRONMENT_CARPETEDHALLWAY,
+    1.9F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_HALLWAY{
+    -1'000L,
+    -300L,
+    0.0F,
+    1.49F,
+    0.59F,
+    -1'219L,
+    0.007F,
+    441L,
+    0.011F,
+    EAX2_ENVIRONMENT_HALLWAY,
+    1.8F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_STONECORRIDOR{
+    -1'000L,
+    -237L,
+    0.0F,
+    2.7F,
+    0.79F,
+    -1'214L,
+    0.013F,
+    395L,
+    0.02F,
+    EAX2_ENVIRONMENT_STONECORRIDOR,
+    13.5F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_ALLEY{
+    -1'000L,
+    -270L,
+    0.0F,
+    1.49F,
+    0.86F,
+    -1'204L,
+    0.007F,
+    -4L,
+    0.011F,
+    EAX2_ENVIRONMENT_ALLEY,
+    7.5F,
+    0.3F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_FOREST{
+    -1'000L,
+    -3'300L,
+    0.0F,
+    1.49F,
+    0.54F,
+    -2'560L,
+    0.162F,
+    -229L,
+    0.088F,
+    EAX2_ENVIRONMENT_FOREST,
+    38.0F,
+    0.3F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_CITY{
+    -1'000L,
+    -800L,
+    0.0F,
+    1.49F,
+    0.67F,
+    -2'273L,
+    0.007F,
+    -1'691L,
+    0.011F,
+    EAX2_ENVIRONMENT_CITY,
+    7.5F,
+    0.5F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_MOUNTAINS{
+    -1'000L,
+    -2'500L,
+    0.0F,
+    1.49F,
+    0.21F,
+    -2'780L,
+    0.3F,
+    -1'434L,
+    0.1F,
+    EAX2_ENVIRONMENT_MOUNTAINS,
+    100.0F,
+    0.27F,
+    -5.0F,
+    EAX2LISTENERFLAGS_DECAYTIMESCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSSCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE |
+        EAX2LISTENERFLAGS_REVERBSCALE |
+        EAX2LISTENERFLAGS_REVERBDELAYSCALE,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_QUARRY{
+    -1'000L,
+    -1'000L,
+    0.0F,
+    1.49F,
+    0.83F,
+    -10'000L,
+    0.061F,
+    500L,
+    0.025F,
+    EAX2_ENVIRONMENT_QUARRY,
+    17.5F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_PLAIN{
+    -1'000L,
+    -2'000L,
+    0.0F,
+    1.49F,
+    0.5F,
+    -2'466L,
+    0.179F,
+    -1'926L,
+    0.1F,
+    EAX2_ENVIRONMENT_PLAIN,
+    42.5F,
+    0.21F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_PARKINGLOT{
+    -1'000L,
+    0L,
+    0.0F,
+    1.65F,
+    1.5F,
+    -1'363L,
+    0.008F,
+    -1'153L,
+    0.012F,
+    EAX2_ENVIRONMENT_PARKINGLOT,
+    8.3F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENERFLAGS_DECAYTIMESCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSSCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE |
+        EAX2LISTENERFLAGS_REVERBSCALE |
+        EAX2LISTENERFLAGS_REVERBDELAYSCALE,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_SEWERPIPE{
+    -1'000L,
+    -1'000L,
+    0.0F,
+    2.81F,
+    0.14F,
+    429L,
+    0.014F,
+    1'023L,
+    0.021F,
+    EAX2_ENVIRONMENT_SEWERPIPE,
+    1.7F,
+    0.8F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_UNDERWATER{
+    -1'000L,
+    -4'000L,
+    0.0F,
+    1.49F,
+    0.1F,
+    -449L,
+    0.007F,
+    1'700L,
+    0.011F,
+    EAX2_ENVIRONMENT_UNDERWATER,
+    1.8F,
+    1.0F,
+    -5.0F,
+    EAX2LISTENER_DEFAULTFLAGS,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_DRUGGED{
+    -1'000L,
+    0L,
+    0.0F,
+    8.39F,
+    1.39F,
+    -115L,
+    0.002F,
+    985L,
+    0.03F,
+    EAX2_ENVIRONMENT_DRUGGED,
+    1.9F,
+    0.5F,
+    -5.0F,
+    EAX2LISTENERFLAGS_DECAYTIMESCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSSCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE |
+        EAX2LISTENERFLAGS_REVERBSCALE |
+        EAX2LISTENERFLAGS_REVERBDELAYSCALE,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_DIZZY{
+    -1'000L,
+    -400L,
+    0.0F,
+    17.23F,
+    0.56F,
+    -1'713L,
+    0.02F,
+    -613L,
+    0.03F,
+    EAX2_ENVIRONMENT_DIZZY,
+    1.8F,
+    0.6F,
+    -5.0F,
+    EAX2LISTENERFLAGS_DECAYTIMESCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSSCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE |
+        EAX2LISTENERFLAGS_REVERBSCALE |
+        EAX2LISTENERFLAGS_REVERBDELAYSCALE,
+};
+
+constexpr EAX20LISTENERPROPERTIES EAX2REVERB_PRESET_PSYCHOTIC{
+    -1'000L,
+    -151L,
+    0.0F,
+    7.56F,
+    0.91F,
+    -626L,
+    0.02F,
+    774L,
+    0.03F,
+    EAX2_ENVIRONMENT_PSYCHOTIC,
+    1.0F,
+    0.5F,
+    -5.0F,
+    EAX2LISTENERFLAGS_DECAYTIMESCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSSCALE |
+        EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE |
+        EAX2LISTENERFLAGS_REVERBSCALE |
+        EAX2LISTENERFLAGS_REVERBDELAYSCALE,
+};
+
+} // namespace
+
+const Eax2ReverbPresets EAX2REVERB_PRESETS{
+    EAX2REVERB_PRESET_GENERIC,
+    EAX2REVERB_PRESET_PADDEDCELL,
+    EAX2REVERB_PRESET_ROOM,
+    EAX2REVERB_PRESET_BATHROOM,
+    EAX2REVERB_PRESET_LIVINGROOM,
+    EAX2REVERB_PRESET_STONEROOM,
+    EAX2REVERB_PRESET_AUDITORIUM,
+    EAX2REVERB_PRESET_CONCERTHALL,
+    EAX2REVERB_PRESET_CAVE,
+    EAX2REVERB_PRESET_ARENA,
+    EAX2REVERB_PRESET_HANGAR,
+    EAX2REVERB_PRESET_CARPETTEDHALLWAY,
+    EAX2REVERB_PRESET_HALLWAY,
+    EAX2REVERB_PRESET_STONECORRIDOR,
+    EAX2REVERB_PRESET_ALLEY,
+    EAX2REVERB_PRESET_FOREST,
+    EAX2REVERB_PRESET_CITY,
+    EAX2REVERB_PRESET_MOUNTAINS,
+    EAX2REVERB_PRESET_QUARRY,
+    EAX2REVERB_PRESET_PLAIN,
+    EAX2REVERB_PRESET_PARKINGLOT,
+    EAX2REVERB_PRESET_SEWERPIPE,
+    EAX2REVERB_PRESET_UNDERWATER,
+    EAX2REVERB_PRESET_DRUGGED,
+    EAX2REVERB_PRESET_DIZZY,
+    EAX2REVERB_PRESET_PSYCHOTIC,
+};
+
+// EAX3+ ====================================================================
+
+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,
+}};
diff --git a/al/eax/api.h b/al/eax/api.h
new file mode 100644 (file)
index 0000000..d254da1
--- /dev/null
@@ -0,0 +1,1493 @@
+#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;
+
+#ifndef _SYS_GUID_OPERATOR_EQ_
+#define _SYS_GUID_OPERATOR_EQ_
+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  // _SYS_GUID_OPERATOR_EQ_
+#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
+
+
+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;
+};
+
+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
+
+enum : unsigned long {
+    EAX2_ENVIRONMENT_GENERIC,
+    EAX2_ENVIRONMENT_PADDEDCELL,
+    EAX2_ENVIRONMENT_ROOM,
+    EAX2_ENVIRONMENT_BATHROOM,
+    EAX2_ENVIRONMENT_LIVINGROOM,
+    EAX2_ENVIRONMENT_STONEROOM,
+    EAX2_ENVIRONMENT_AUDITORIUM,
+    EAX2_ENVIRONMENT_CONCERTHALL,
+    EAX2_ENVIRONMENT_CAVE,
+    EAX2_ENVIRONMENT_ARENA,
+    EAX2_ENVIRONMENT_HANGAR,
+    EAX2_ENVIRONMENT_CARPETEDHALLWAY,
+    EAX2_ENVIRONMENT_HALLWAY,
+    EAX2_ENVIRONMENT_STONECORRIDOR,
+    EAX2_ENVIRONMENT_ALLEY,
+    EAX2_ENVIRONMENT_FOREST,
+    EAX2_ENVIRONMENT_CITY,
+    EAX2_ENVIRONMENT_MOUNTAINS,
+    EAX2_ENVIRONMENT_QUARRY,
+    EAX2_ENVIRONMENT_PLAIN,
+    EAX2_ENVIRONMENT_PARKINGLOT,
+    EAX2_ENVIRONMENT_SEWERPIPE,
+    EAX2_ENVIRONMENT_UNDERWATER,
+    EAX2_ENVIRONMENT_DRUGGED,
+    EAX2_ENVIRONMENT_DIZZY,
+    EAX2_ENVIRONMENT_PSYCHOTIC,
+
+    EAX2_ENVIRONMENT_COUNT,
+};
+
+constexpr auto EAX2LISTENERFLAGS_DECAYTIMESCALE = 0x00000001UL;
+constexpr auto EAX2LISTENERFLAGS_REFLECTIONSSCALE = 0x00000002UL;
+constexpr auto EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE = 0x00000004UL;
+constexpr auto EAX2LISTENERFLAGS_REVERBSCALE = 0x00000008UL;
+constexpr auto EAX2LISTENERFLAGS_REVERBDELAYSCALE = 0x00000010UL;
+constexpr auto EAX2LISTENERFLAGS_DECAYHFLIMIT = 0x00000020UL;
+constexpr auto EAX2LISTENERFLAGS_RESERVED = 0xFFFFFFC0UL;
+
+constexpr auto EAX2LISTENER_MINROOM = -10'000L;
+constexpr auto EAX2LISTENER_MAXROOM = 0L;
+constexpr auto EAX2LISTENER_DEFAULTROOM = -1'000L;
+
+constexpr auto EAX2LISTENER_MINROOMHF = -10'000L;
+constexpr auto EAX2LISTENER_MAXROOMHF = 0L;
+constexpr auto EAX2LISTENER_DEFAULTROOMHF = -100L;
+
+constexpr auto EAX2LISTENER_MINROOMROLLOFFFACTOR = 0.0F;
+constexpr auto EAX2LISTENER_MAXROOMROLLOFFFACTOR = 10.0F;
+constexpr auto EAX2LISTENER_DEFAULTROOMROLLOFFFACTOR = 0.0F;
+
+constexpr auto EAX2LISTENER_MINDECAYTIME = 0.1F;
+constexpr auto EAX2LISTENER_MAXDECAYTIME = 20.0F;
+constexpr auto EAX2LISTENER_DEFAULTDECAYTIME = 1.49F;
+
+constexpr auto EAX2LISTENER_MINDECAYHFRATIO = 0.1F;
+constexpr auto EAX2LISTENER_MAXDECAYHFRATIO = 2.0F;
+constexpr auto EAX2LISTENER_DEFAULTDECAYHFRATIO = 0.83F;
+
+constexpr auto EAX2LISTENER_MINREFLECTIONS = -10'000L;
+constexpr auto EAX2LISTENER_MAXREFLECTIONS = 1'000L;
+constexpr auto EAX2LISTENER_DEFAULTREFLECTIONS = -2'602L;
+
+constexpr auto EAX2LISTENER_MINREFLECTIONSDELAY = 0.0F;
+constexpr auto EAX2LISTENER_MAXREFLECTIONSDELAY = 0.3F;
+constexpr auto EAX2LISTENER_DEFAULTREFLECTIONSDELAY = 0.007F;
+
+constexpr auto EAX2LISTENER_MINREVERB = -10'000L;
+constexpr auto EAX2LISTENER_MAXREVERB = 2'000L;
+constexpr auto EAX2LISTENER_DEFAULTREVERB = 200L;
+
+constexpr auto EAX2LISTENER_MINREVERBDELAY = 0.0F;
+constexpr auto EAX2LISTENER_MAXREVERBDELAY = 0.1F;
+constexpr auto EAX2LISTENER_DEFAULTREVERBDELAY = 0.011F;
+
+constexpr auto EAX2LISTENER_MINENVIRONMENT = 0UL;
+constexpr auto EAX2LISTENER_MAXENVIRONMENT = EAX2_ENVIRONMENT_COUNT - 1;
+constexpr auto EAX2LISTENER_DEFAULTENVIRONMENT = EAX2_ENVIRONMENT_GENERIC;
+
+constexpr auto EAX2LISTENER_MINENVIRONMENTSIZE = 1.0F;
+constexpr auto EAX2LISTENER_MAXENVIRONMENTSIZE = 100.0F;
+constexpr auto EAX2LISTENER_DEFAULTENVIRONMENTSIZE = 7.5F;
+
+constexpr auto EAX2LISTENER_MINENVIRONMENTDIFFUSION = 0.0F;
+constexpr auto EAX2LISTENER_MAXENVIRONMENTDIFFUSION = 1.0F;
+constexpr auto EAX2LISTENER_DEFAULTENVIRONMENTDIFFUSION = 1.0F;
+
+constexpr auto EAX2LISTENER_MINAIRABSORPTIONHF = -100.0F;
+constexpr auto EAX2LISTENER_MAXAIRABSORPTIONHF = 0.0F;
+constexpr auto EAX2LISTENER_DEFAULTAIRABSORPTIONHF = -5.0F;
+
+constexpr auto EAX2LISTENER_DEFAULTFLAGS =
+    EAX2LISTENERFLAGS_DECAYTIMESCALE |
+    EAX2LISTENERFLAGS_REFLECTIONSSCALE |
+    EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE |
+    EAX2LISTENERFLAGS_REVERBSCALE |
+    EAX2LISTENERFLAGS_REVERBDELAYSCALE |
+    EAX2LISTENERFLAGS_DECAYHFLIMIT;
+
+
+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
+constexpr auto HEADPHONES = 0UL;
+constexpr auto SPEAKERS_2 = 1UL;
+constexpr auto SPEAKERS_4 = 2UL;
+constexpr auto SPEAKERS_5 = 3UL; // 5.1 speakers
+constexpr auto SPEAKERS_6 = 4UL; // 6.1 speakers
+constexpr auto SPEAKERS_7 = 5UL; // 7.1 speakers
+
+constexpr auto EAXCONTEXT_MINSPEAKERCONFIG = HEADPHONES;
+constexpr auto EAXCONTEXT_MAXSPEAKERCONFIG = SPEAKERS_7;
+
+// EAX50
+constexpr auto EAX_40 = 5UL; // EAX 4.0
+constexpr auto EAX_50 = 6UL; // 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;
+}; // EAX50CONTEXTPROPERTIES
+
+
+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 EAX40CONTEXT_DEFAULTPRIMARYFXSLOTID;
+extern const GUID EAX50CONTEXT_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
+
+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;
+constexpr auto EAXSOURCEFLAGS_UPMIX = 0x00000010UL;
+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;
+
+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
+
+// 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 EAX40ACTIVEFXSLOTS 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
+
+
+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 Eax1ReverbPresets = std::array<EAX_REVERBPROPERTIES, EAX1_ENVIRONMENT_COUNT>;
+extern const Eax1ReverbPresets EAX1REVERB_PRESETS;
+
+using Eax2ReverbPresets = std::array<EAX20LISTENERPROPERTIES, EAX2_ENVIRONMENT_COUNT>;
+extern const Eax2ReverbPresets EAX2REVERB_PRESETS;
+
+using EaxReverbPresets = std::array<EAXREVERBPROPERTIES, EAX1_ENVIRONMENT_COUNT>;
+extern const EaxReverbPresets EAXREVERB_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
diff --git a/al/eax/call.cpp b/al/eax/call.cpp
new file mode 100644 (file)
index 0000000..689d5cf
--- /dev/null
@@ -0,0 +1,219 @@
+#include "config.h"
+#include "call.h"
+#include "exception.h"
+
+namespace {
+
+constexpr auto deferred_flag = 0x80000000U;
+
+class EaxCallException : public EaxException {
+public:
+    explicit EaxCallException(const char* message)
+        : EaxException{"EAX_CALL", message}
+    {}
+}; // EaxCallException
+
+} // namespace
+
+EaxCall::EaxCall(
+    EaxCallType type,
+    const GUID& property_set_guid,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size)
+    : mCallType{type}, mVersion{0}, mPropertySetId{EaxCallPropertySetId::none}
+    , mIsDeferred{(property_id & deferred_flag) != 0}
+    , mPropertyId{property_id & ~deferred_flag}, mPropertySourceId{property_source_id}
+    , mPropertyBuffer{property_buffer}, mPropertyBufferSize{property_size}
+{
+    switch(mCallType)
+    {
+        case EaxCallType::get:
+        case EaxCallType::set:
+            break;
+
+        default:
+            fail("Invalid type.");
+    }
+
+    if (false)
+    {
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_Context)
+    {
+        mVersion = 4;
+        mPropertySetId = EaxCallPropertySetId::context;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_Context)
+    {
+        mVersion = 5;
+        mPropertySetId = EaxCallPropertySetId::context;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX20_ListenerProperties)
+    {
+        mVersion = 2;
+        mFxSlotIndex = 0u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot_effect;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX30_ListenerProperties)
+    {
+        mVersion = 3;
+        mFxSlotIndex = 0u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot_effect;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot0)
+    {
+        mVersion = 4;
+        mFxSlotIndex = 0u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot0)
+    {
+        mVersion = 5;
+        mFxSlotIndex = 0u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot1)
+    {
+        mVersion = 4;
+        mFxSlotIndex = 1u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot1)
+    {
+        mVersion = 5;
+        mFxSlotIndex = 1u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot2)
+    {
+        mVersion = 4;
+        mFxSlotIndex = 2u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot2)
+    {
+        mVersion = 5;
+        mFxSlotIndex = 2u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot3)
+    {
+        mVersion = 4;
+        mFxSlotIndex = 3u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot3)
+    {
+        mVersion = 5;
+        mFxSlotIndex = 3u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX20_BufferProperties)
+    {
+        mVersion = 2;
+        mPropertySetId = EaxCallPropertySetId::source;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX30_BufferProperties)
+    {
+        mVersion = 3;
+        mPropertySetId = EaxCallPropertySetId::source;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_Source)
+    {
+        mVersion = 4;
+        mPropertySetId = EaxCallPropertySetId::source;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_Source)
+    {
+        mVersion = 5;
+        mPropertySetId = EaxCallPropertySetId::source;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX_ReverbProperties)
+    {
+        mVersion = 1;
+        mFxSlotIndex = 0u;
+        mPropertySetId = EaxCallPropertySetId::fx_slot_effect;
+    }
+    else if (property_set_guid == DSPROPSETID_EAXBUFFER_ReverbProperties)
+    {
+        mVersion = 1;
+        mPropertySetId = EaxCallPropertySetId::source;
+    }
+    else
+    {
+        fail("Unsupported property set id.");
+    }
+
+    switch(mPropertyId)
+    {
+    case EAXCONTEXT_LASTERROR:
+    case EAXCONTEXT_SPEAKERCONFIG:
+    case EAXCONTEXT_EAXSESSION:
+    case EAXFXSLOT_NONE:
+    case EAXFXSLOT_ALLPARAMETERS:
+    case EAXFXSLOT_LOADEFFECT:
+    case EAXFXSLOT_VOLUME:
+    case EAXFXSLOT_LOCK:
+    case EAXFXSLOT_FLAGS:
+    case EAXFXSLOT_OCCLUSION:
+    case EAXFXSLOT_OCCLUSIONLFRATIO:
+        // EAX allow to set "defer" flag on immediate-only properties.
+        // If we don't clear our flag then "applyAllUpdates" in EAX context won't be called.
+        mIsDeferred = false;
+        break;
+    }
+
+    if(!mIsDeferred)
+    {
+        if(mPropertySetId != EaxCallPropertySetId::fx_slot && mPropertyId != 0)
+        {
+            if(mPropertyBuffer == nullptr)
+                fail("Null property buffer.");
+
+            if(mPropertyBufferSize == 0)
+                fail("Empty property.");
+        }
+    }
+
+    if(mPropertySetId == EaxCallPropertySetId::source && mPropertySourceId == 0)
+        fail("Null AL source id.");
+
+    if(mPropertySetId == EaxCallPropertySetId::fx_slot)
+    {
+        if(mPropertyId < EAXFXSLOT_NONE)
+            mPropertySetId = EaxCallPropertySetId::fx_slot_effect;
+    }
+}
+
+[[noreturn]] void EaxCall::fail(const char* message)
+{
+    throw EaxCallException{message};
+}
+
+[[noreturn]] void EaxCall::fail_too_small()
+{
+    fail("Property buffer too small.");
+}
+
+EaxCall create_eax_call(
+    EaxCallType type,
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size)
+{
+    if(!property_set_id)
+        throw EaxCallException{"Null property set ID."};
+
+    return EaxCall{
+        type,
+        *property_set_id,
+        property_id,
+        property_source_id,
+        property_buffer,
+        property_size
+    };
+}
diff --git a/al/eax/call.h b/al/eax/call.h
new file mode 100644 (file)
index 0000000..5ec33b0
--- /dev/null
@@ -0,0 +1,97 @@
+#ifndef EAX_EAX_CALL_INCLUDED
+#define EAX_EAX_CALL_INCLUDED
+
+#include "AL/al.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "api.h"
+#include "fx_slot_index.h"
+
+enum class EaxCallType {
+    none,
+    get,
+    set,
+}; // EaxCallType
+
+enum class EaxCallPropertySetId {
+    none,
+    context,
+    fx_slot,
+    source,
+    fx_slot_effect,
+}; // EaxCallPropertySetId
+
+class EaxCall {
+public:
+    EaxCall(
+        EaxCallType type,
+        const GUID& property_set_guid,
+        ALuint property_id,
+        ALuint property_source_id,
+        ALvoid* property_buffer,
+        ALuint property_size);
+
+    bool is_get() const noexcept { return mCallType == EaxCallType::get; }
+    bool is_deferred() const noexcept { return mIsDeferred; }
+    int get_version() const noexcept { return mVersion; }
+    EaxCallPropertySetId get_property_set_id() const noexcept { return mPropertySetId; }
+    ALuint get_property_id() const noexcept { return mPropertyId; }
+    ALuint get_property_al_name() const noexcept { return mPropertySourceId; }
+    EaxFxSlotIndex get_fx_slot_index() const noexcept { return mFxSlotIndex; }
+
+    template<typename TException, typename TValue>
+    TValue& get_value() const
+    {
+        if(mPropertyBufferSize < sizeof(TValue))
+            fail_too_small();
+
+        return *static_cast<TValue*>(mPropertyBuffer);
+    }
+
+    template<typename TValue>
+    al::span<TValue> get_values(size_t max_count) const
+    {
+        if(max_count == 0 || mPropertyBufferSize < sizeof(TValue))
+            fail_too_small();
+
+        const auto count = minz(mPropertyBufferSize / sizeof(TValue), max_count);
+        return al::as_span(static_cast<TValue*>(mPropertyBuffer), count);
+    }
+
+    template<typename TValue>
+    al::span<TValue> get_values() const
+    {
+        return get_values<TValue>(~size_t{});
+    }
+
+    template<typename TException, typename TValue>
+    void set_value(const TValue& value) const
+    {
+        get_value<TException, TValue>() = value;
+    }
+
+private:
+    const EaxCallType mCallType;
+    int mVersion;
+    EaxFxSlotIndex mFxSlotIndex;
+    EaxCallPropertySetId mPropertySetId;
+    bool mIsDeferred;
+
+    const ALuint mPropertyId;
+    const ALuint mPropertySourceId;
+    ALvoid*const mPropertyBuffer;
+    const ALuint mPropertyBufferSize;
+
+    [[noreturn]] static void fail(const char* message);
+    [[noreturn]] static void fail_too_small();
+}; // EaxCall
+
+EaxCall create_eax_call(
+    EaxCallType type,
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size);
+
+#endif // !EAX_EAX_CALL_INCLUDED
diff --git a/al/eax/effect.h b/al/eax/effect.h
new file mode 100644 (file)
index 0000000..a0b4e71
--- /dev/null
@@ -0,0 +1,418 @@
+#ifndef EAX_EFFECT_INCLUDED
+#define EAX_EFFECT_INCLUDED
+
+
+#include <cassert>
+#include <memory>
+
+#include "alnumeric.h"
+#include "AL/al.h"
+#include "core/effects/base.h"
+#include "call.h"
+
+struct EaxEffectErrorMessages
+{
+    static constexpr auto unknown_property_id() noexcept { return "Unknown property id."; }
+    static constexpr auto unknown_version() noexcept { return "Unknown version."; }
+}; // EaxEffectErrorMessages
+
+/* TODO: Use std::variant (C++17). */
+enum class EaxEffectType {
+    None, Reverb, Chorus, Autowah, Compressor, Distortion, Echo, Equalizer, Flanger,
+    FrequencyShifter, Modulator, PitchShifter, VocalMorpher
+};
+struct EaxEffectProps {
+    EaxEffectType mType;
+    union {
+        EAXREVERBPROPERTIES mReverb;
+        EAXCHORUSPROPERTIES mChorus;
+        EAXAUTOWAHPROPERTIES mAutowah;
+        EAXAGCCOMPRESSORPROPERTIES mCompressor;
+        EAXDISTORTIONPROPERTIES mDistortion;
+        EAXECHOPROPERTIES mEcho;
+        EAXEQUALIZERPROPERTIES mEqualizer;
+        EAXFLANGERPROPERTIES mFlanger;
+        EAXFREQUENCYSHIFTERPROPERTIES mFrequencyShifter;
+        EAXRINGMODULATORPROPERTIES mModulator;
+        EAXPITCHSHIFTERPROPERTIES mPitchShifter;
+        EAXVOCALMORPHERPROPERTIES mVocalMorpher;
+    };
+};
+
+constexpr ALenum EnumFromEaxEffectType(const EaxEffectProps &props)
+{
+    switch(props.mType)
+    {
+    case EaxEffectType::None: break;
+    case EaxEffectType::Reverb: return AL_EFFECT_EAXREVERB;
+    case EaxEffectType::Chorus: return AL_EFFECT_CHORUS;
+    case EaxEffectType::Autowah: return AL_EFFECT_AUTOWAH;
+    case EaxEffectType::Compressor: return AL_EFFECT_COMPRESSOR;
+    case EaxEffectType::Distortion: return AL_EFFECT_DISTORTION;
+    case EaxEffectType::Echo: return AL_EFFECT_ECHO;
+    case EaxEffectType::Equalizer: return AL_EFFECT_EQUALIZER;
+    case EaxEffectType::Flanger: return AL_EFFECT_FLANGER;
+    case EaxEffectType::FrequencyShifter: return AL_EFFECT_FREQUENCY_SHIFTER;
+    case EaxEffectType::Modulator: return AL_EFFECT_RING_MODULATOR;
+    case EaxEffectType::PitchShifter: return AL_EFFECT_PITCH_SHIFTER;
+    case EaxEffectType::VocalMorpher: return AL_EFFECT_VOCAL_MORPHER;
+    }
+    return AL_EFFECT_NULL;
+}
+
+struct EaxReverbCommitter {
+    struct Exception;
+
+    EaxReverbCommitter(EaxEffectProps &eaxprops, EffectProps &alprops)
+        : mEaxProps{eaxprops}, mAlProps{alprops}
+    { }
+
+    EaxEffectProps &mEaxProps;
+    EffectProps &mAlProps;
+
+    [[noreturn]] static void fail(const char* message);
+    [[noreturn]] static void fail_unknown_property_id()
+    { fail(EaxEffectErrorMessages::unknown_property_id()); }
+
+    template<typename TValidator, typename TProperty>
+    static void defer(const EaxCall& call, TProperty& property)
+    {
+        const auto& value = call.get_value<Exception, const TProperty>();
+        TValidator{}(value);
+        property = value;
+    }
+
+    template<typename TValidator, typename TDeferrer, typename TProperties, typename TProperty>
+    static void defer(const EaxCall& call, TProperties& properties, TProperty&)
+    {
+        const auto& value = call.get_value<Exception, const TProperty>();
+        TValidator{}(value);
+        TDeferrer{}(properties, value);
+    }
+
+    template<typename TValidator, typename TProperty>
+    static void defer3(const EaxCall& call, EAXREVERBPROPERTIES& properties, TProperty& property)
+    {
+        const auto& value = call.get_value<Exception, const TProperty>();
+        TValidator{}(value);
+        if (value == property)
+            return;
+        property = value;
+        properties.ulEnvironment = EAX_ENVIRONMENT_UNDEFINED;
+    }
+
+
+    bool commit(const EAX_REVERBPROPERTIES &props);
+    bool commit(const EAX20LISTENERPROPERTIES &props);
+    bool commit(const EAXREVERBPROPERTIES &props);
+    bool commit(const EaxEffectProps &props);
+
+    static void SetDefaults(EAX_REVERBPROPERTIES &props);
+    static void SetDefaults(EAX20LISTENERPROPERTIES &props);
+    static void SetDefaults(EAXREVERBPROPERTIES &props);
+    static void SetDefaults(EaxEffectProps &props);
+
+    static void Get(const EaxCall &call, const EAX_REVERBPROPERTIES &props);
+    static void Get(const EaxCall &call, const EAX20LISTENERPROPERTIES &props);
+    static void Get(const EaxCall &call, const EAXREVERBPROPERTIES &props);
+    static void Get(const EaxCall &call, const EaxEffectProps &props);
+
+    static void Set(const EaxCall &call, EAX_REVERBPROPERTIES &props);
+    static void Set(const EaxCall &call, EAX20LISTENERPROPERTIES &props);
+    static void Set(const EaxCall &call, EAXREVERBPROPERTIES &props);
+    static void Set(const EaxCall &call, EaxEffectProps &props);
+
+    static void translate(const EAX_REVERBPROPERTIES& src, EaxEffectProps& dst) noexcept;
+    static void translate(const EAX20LISTENERPROPERTIES& src, EaxEffectProps& dst) noexcept;
+    static void translate(const EAXREVERBPROPERTIES& src, EaxEffectProps& dst) noexcept;
+};
+
+template<typename T>
+struct EaxCommitter {
+    struct Exception;
+
+    EaxCommitter(EaxEffectProps &eaxprops, EffectProps &alprops)
+        : mEaxProps{eaxprops}, mAlProps{alprops}
+    { }
+
+    EaxEffectProps &mEaxProps;
+    EffectProps &mAlProps;
+
+    template<typename TValidator, typename TProperty>
+    static void defer(const EaxCall& call, TProperty& property)
+    {
+        const auto& value = call.get_value<Exception, const TProperty>();
+        TValidator{}(value);
+        property = value;
+    }
+
+    [[noreturn]] static void fail(const char *message);
+    [[noreturn]] static void fail_unknown_property_id()
+    { fail(EaxEffectErrorMessages::unknown_property_id()); }
+
+    bool commit(const EaxEffectProps &props);
+
+    static void SetDefaults(EaxEffectProps &props);
+    static void Get(const EaxCall &call, const EaxEffectProps &props);
+    static void Set(const EaxCall &call, EaxEffectProps &props);
+};
+
+struct EaxAutowahCommitter : public EaxCommitter<EaxAutowahCommitter> {
+    using EaxCommitter<EaxAutowahCommitter>::EaxCommitter;
+};
+struct EaxChorusCommitter : public EaxCommitter<EaxChorusCommitter> {
+    using EaxCommitter<EaxChorusCommitter>::EaxCommitter;
+};
+struct EaxCompressorCommitter : public EaxCommitter<EaxCompressorCommitter> {
+    using EaxCommitter<EaxCompressorCommitter>::EaxCommitter;
+};
+struct EaxDistortionCommitter : public EaxCommitter<EaxDistortionCommitter> {
+    using EaxCommitter<EaxDistortionCommitter>::EaxCommitter;
+};
+struct EaxEchoCommitter : public EaxCommitter<EaxEchoCommitter> {
+    using EaxCommitter<EaxEchoCommitter>::EaxCommitter;
+};
+struct EaxEqualizerCommitter : public EaxCommitter<EaxEqualizerCommitter> {
+    using EaxCommitter<EaxEqualizerCommitter>::EaxCommitter;
+};
+struct EaxFlangerCommitter : public EaxCommitter<EaxFlangerCommitter> {
+    using EaxCommitter<EaxFlangerCommitter>::EaxCommitter;
+};
+struct EaxFrequencyShifterCommitter : public EaxCommitter<EaxFrequencyShifterCommitter> {
+    using EaxCommitter<EaxFrequencyShifterCommitter>::EaxCommitter;
+};
+struct EaxModulatorCommitter : public EaxCommitter<EaxModulatorCommitter> {
+    using EaxCommitter<EaxModulatorCommitter>::EaxCommitter;
+};
+struct EaxPitchShifterCommitter : public EaxCommitter<EaxPitchShifterCommitter> {
+    using EaxCommitter<EaxPitchShifterCommitter>::EaxCommitter;
+};
+struct EaxVocalMorpherCommitter : public EaxCommitter<EaxVocalMorpherCommitter> {
+    using EaxCommitter<EaxVocalMorpherCommitter>::EaxCommitter;
+};
+struct EaxNullCommitter : public EaxCommitter<EaxNullCommitter> {
+    using EaxCommitter<EaxNullCommitter>::EaxCommitter;
+};
+
+
+class EaxEffect {
+public:
+    EaxEffect() noexcept = default;
+    ~EaxEffect() = default;
+
+    ALenum al_effect_type_{AL_EFFECT_NULL};
+    EffectProps al_effect_props_{};
+
+    using Props1 = EAX_REVERBPROPERTIES;
+    using Props2 = EAX20LISTENERPROPERTIES;
+    using Props3 = EAXREVERBPROPERTIES;
+    using Props4 = EaxEffectProps;
+
+    struct State1 {
+        Props1 i; // Immediate.
+        Props1 d; // Deferred.
+    };
+
+    struct State2 {
+        Props2 i; // Immediate.
+        Props2 d; // Deferred.
+    };
+
+    struct State3 {
+        Props3 i; // Immediate.
+        Props3 d; // Deferred.
+    };
+
+    struct State4 {
+        Props4 i; // Immediate.
+        Props4 d; // Deferred.
+    };
+
+    int version_{};
+    bool changed_{};
+    Props4 props_{};
+    State1 state1_{};
+    State2 state2_{};
+    State3 state3_{};
+    State4 state4_{};
+    State4 state5_{};
+
+
+    template<typename T, typename ...Args>
+    void call_set_defaults(Args&& ...args)
+    { return T::SetDefaults(std::forward<Args>(args)...); }
+
+    void call_set_defaults(const ALenum altype, EaxEffectProps &props)
+    {
+        if(altype == AL_EFFECT_EAXREVERB)
+            return call_set_defaults<EaxReverbCommitter>(props);
+        if(altype == AL_EFFECT_CHORUS)
+            return call_set_defaults<EaxChorusCommitter>(props);
+        if(altype == AL_EFFECT_AUTOWAH)
+            return call_set_defaults<EaxAutowahCommitter>(props);
+        if(altype == AL_EFFECT_COMPRESSOR)
+            return call_set_defaults<EaxCompressorCommitter>(props);
+        if(altype == AL_EFFECT_DISTORTION)
+            return call_set_defaults<EaxDistortionCommitter>(props);
+        if(altype == AL_EFFECT_ECHO)
+            return call_set_defaults<EaxEchoCommitter>(props);
+        if(altype == AL_EFFECT_EQUALIZER)
+            return call_set_defaults<EaxEqualizerCommitter>(props);
+        if(altype == AL_EFFECT_FLANGER)
+            return call_set_defaults<EaxFlangerCommitter>(props);
+        if(altype == AL_EFFECT_FREQUENCY_SHIFTER)
+            return call_set_defaults<EaxFrequencyShifterCommitter>(props);
+        if(altype == AL_EFFECT_RING_MODULATOR)
+            return call_set_defaults<EaxModulatorCommitter>(props);
+        if(altype == AL_EFFECT_PITCH_SHIFTER)
+            return call_set_defaults<EaxPitchShifterCommitter>(props);
+        if(altype == AL_EFFECT_VOCAL_MORPHER)
+            return call_set_defaults<EaxVocalMorpherCommitter>(props);
+        return call_set_defaults<EaxNullCommitter>(props);
+    }
+
+    template<typename T>
+    void init()
+    {
+        call_set_defaults<EaxReverbCommitter>(state1_.d);
+        state1_.i = state1_.d;
+        call_set_defaults<EaxReverbCommitter>(state2_.d);
+        state2_.i = state2_.d;
+        call_set_defaults<EaxReverbCommitter>(state3_.d);
+        state3_.i = state3_.d;
+        call_set_defaults<T>(state4_.d);
+        state4_.i = state4_.d;
+        call_set_defaults<T>(state5_.d);
+        state5_.i = state5_.d;
+    }
+
+    void set_defaults(int eax_version, ALenum altype)
+    {
+        switch(eax_version)
+        {
+        case 1: call_set_defaults<EaxReverbCommitter>(state1_.d); break;
+        case 2: call_set_defaults<EaxReverbCommitter>(state2_.d); break;
+        case 3: call_set_defaults<EaxReverbCommitter>(state3_.d); break;
+        case 4: call_set_defaults(altype, state4_.d); break;
+        case 5: call_set_defaults(altype, state5_.d); break;
+        }
+        changed_ = true;
+    }
+
+
+#define EAXCALL(T, Callable, ...)                                             \
+    if(T == EaxEffectType::Reverb)                                            \
+        return Callable<EaxReverbCommitter>(__VA_ARGS__);                     \
+    if(T == EaxEffectType::Chorus)                                            \
+        return Callable<EaxChorusCommitter>(__VA_ARGS__);                     \
+    if(T == EaxEffectType::Autowah)                                           \
+        return Callable<EaxAutowahCommitter>(__VA_ARGS__);                    \
+    if(T == EaxEffectType::Compressor)                                        \
+        return Callable<EaxCompressorCommitter>(__VA_ARGS__);                 \
+    if(T == EaxEffectType::Distortion)                                        \
+        return Callable<EaxDistortionCommitter>(__VA_ARGS__);                 \
+    if(T == EaxEffectType::Echo)                                              \
+        return Callable<EaxEchoCommitter>(__VA_ARGS__);                       \
+    if(T == EaxEffectType::Equalizer)                                         \
+        return Callable<EaxEqualizerCommitter>(__VA_ARGS__);                  \
+    if(T == EaxEffectType::Flanger)                                           \
+        return Callable<EaxFlangerCommitter>(__VA_ARGS__);                    \
+    if(T == EaxEffectType::FrequencyShifter)                                  \
+        return Callable<EaxFrequencyShifterCommitter>(__VA_ARGS__);           \
+    if(T == EaxEffectType::Modulator)                                         \
+        return Callable<EaxModulatorCommitter>(__VA_ARGS__);                  \
+    if(T == EaxEffectType::PitchShifter)                                      \
+        return Callable<EaxPitchShifterCommitter>(__VA_ARGS__);               \
+    if(T == EaxEffectType::VocalMorpher)                                      \
+        return Callable<EaxVocalMorpherCommitter>(__VA_ARGS__);               \
+    return Callable<EaxNullCommitter>(__VA_ARGS__)
+
+    template<typename T, typename ...Args>
+    static void call_set(Args&& ...args)
+    { return T::Set(std::forward<Args>(args)...); }
+
+    static void call_set(const EaxCall &call, EaxEffectProps &props)
+    { EAXCALL(props.mType, call_set, call, props); }
+
+    void set(const EaxCall &call)
+    {
+        switch(call.get_version())
+        {
+        case 1: call_set<EaxReverbCommitter>(call, state1_.d); break;
+        case 2: call_set<EaxReverbCommitter>(call, state2_.d); break;
+        case 3: call_set<EaxReverbCommitter>(call, state3_.d); break;
+        case 4: call_set(call, state4_.d); break;
+        case 5: call_set(call, state5_.d); break;
+        }
+        changed_ = true;
+    }
+
+
+    template<typename T, typename ...Args>
+    static void call_get(Args&& ...args)
+    { return T::Get(std::forward<Args>(args)...); }
+
+    static void call_get(const EaxCall &call, const EaxEffectProps &props)
+    { EAXCALL(props.mType, call_get, call, props); }
+
+    void get(const EaxCall &call)
+    {
+        switch(call.get_version())
+        {
+        case 1: call_get<EaxReverbCommitter>(call, state1_.d); break;
+        case 2: call_get<EaxReverbCommitter>(call, state2_.d); break;
+        case 3: call_get<EaxReverbCommitter>(call, state3_.d); break;
+        case 4: call_get(call, state4_.d); break;
+        case 5: call_get(call, state5_.d); break;
+        }
+    }
+
+
+    template<typename T, typename ...Args>
+    bool call_commit(Args&& ...args)
+    { return T{props_, al_effect_props_}.commit(std::forward<Args>(args)...); }
+
+    bool call_commit(const EaxEffectProps &props)
+    { EAXCALL(props.mType, call_commit, props); }
+
+    bool commit(int eax_version)
+    {
+        changed_ |= version_ != eax_version;
+        if(!changed_) return false;
+
+        bool ret{version_ != eax_version};
+        version_ = eax_version;
+        changed_ = false;
+
+        switch(eax_version)
+        {
+        case 1:
+            state1_.i = state1_.d;
+            ret |= call_commit<EaxReverbCommitter>(state1_.d);
+            break;
+        case 2:
+            state2_.i = state2_.d;
+            ret |= call_commit<EaxReverbCommitter>(state2_.d);
+            break;
+        case 3:
+            state3_.i = state3_.d;
+            ret |= call_commit<EaxReverbCommitter>(state3_.d);
+            break;
+        case 4:
+            state4_.i = state4_.d;
+            ret |= call_commit(state4_.d);
+            break;
+        case 5:
+            state5_.i = state5_.d;
+            ret |= call_commit(state5_.d);
+            break;
+        }
+        al_effect_type_ = EnumFromEaxEffectType(props_);
+        return ret;
+    }
+#undef EAXCALL
+}; // EaxEffect
+
+using EaxEffectUPtr = std::unique_ptr<EaxEffect>;
+
+#endif // !EAX_EFFECT_INCLUDED
diff --git a/al/eax/exception.cpp b/al/eax/exception.cpp
new file mode 100644 (file)
index 0000000..435e744
--- /dev/null
@@ -0,0 +1,59 @@
+#include "config.h"
+
+#include "exception.h"
+
+#include <cassert>
+#include <string>
+
+
+EaxException::EaxException(const char *context, const char *message)
+    : std::runtime_error{make_message(context, message)}
+{
+}
+EaxException::~EaxException() = default;
+
+
+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;
+}
diff --git a/al/eax/exception.h b/al/eax/exception.h
new file mode 100644 (file)
index 0000000..3ae88cd
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef EAX_EXCEPTION_INCLUDED
+#define EAX_EXCEPTION_INCLUDED
+
+
+#include <stdexcept>
+#include <string>
+
+
+class EaxException : public std::runtime_error {
+    static std::string make_message(const char *context, const char *message);
+
+public:
+    EaxException(const char *context, const char *message);
+    ~EaxException() override;
+}; // EaxException
+
+
+#endif // !EAX_EXCEPTION_INCLUDED
diff --git a/al/eax/fx_slot_index.cpp b/al/eax/fx_slot_index.cpp
new file mode 100644 (file)
index 0000000..28b1188
--- /dev/null
@@ -0,0 +1,71 @@
+#include "config.h"
+
+#include "fx_slot_index.h"
+
+#include "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};
+}
diff --git a/al/eax/fx_slot_index.h b/al/eax/fx_slot_index.h
new file mode 100644 (file)
index 0000000..63dba03
--- /dev/null
@@ -0,0 +1,41 @@
+#ifndef EAX_FX_SLOT_INDEX_INCLUDED
+#define EAX_FX_SLOT_INDEX_INCLUDED
+
+
+#include <cstddef>
+
+#include "aloptional.h"
+#include "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
diff --git a/al/eax/fx_slots.cpp b/al/eax/fx_slots.cpp
new file mode 100644 (file)
index 0000000..d04b70d
--- /dev/null
@@ -0,0 +1,75 @@
+#include "config.h"
+
+#include "fx_slots.h"
+
+#include <array>
+
+#include "api.h"
+#include "exception.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()];
+}
+
+[[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;
+    }
+}
diff --git a/al/eax/fx_slots.h b/al/eax/fx_slots.h
new file mode 100644 (file)
index 0000000..18b2d3a
--- /dev/null
@@ -0,0 +1,49 @@
+#ifndef EAX_FX_SLOTS_INCLUDED
+#define EAX_FX_SLOTS_INCLUDED
+
+
+#include <array>
+
+#include "al/auxeffectslot.h"
+
+#include "api.h"
+#include "call.h"
+#include "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);
+
+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
diff --git a/al/eax/globals.cpp b/al/eax/globals.cpp
new file mode 100644 (file)
index 0000000..80e9dbf
--- /dev/null
@@ -0,0 +1,21 @@
+#include "config.h"
+
+#include "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";
diff --git a/al/eax/globals.h b/al/eax/globals.h
new file mode 100644 (file)
index 0000000..1b4d63b
--- /dev/null
@@ -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
diff --git a/al/eax/utils.cpp b/al/eax/utils.cpp
new file mode 100644 (file)
index 0000000..b3ed6ca
--- /dev/null
@@ -0,0 +1,26 @@
+#include "config.h"
+
+#include "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);
+
+    try {
+        std::rethrow_exception(exception_ptr);
+    }
+    catch(const std::exception& ex) {
+        const auto ex_message = ex.what();
+        ERR("%s %s\n", message ? message : "", ex_message);
+    }
+    catch(...) {
+        ERR("%s %s\n", message ? message : "", "Generic exception.");
+    }
+}
diff --git a/al/eax/utils.h b/al/eax/utils.h
new file mode 100644 (file)
index 0000000..8ff75a1
--- /dev/null
@@ -0,0 +1,95 @@
+#ifndef EAX_UTILS_INCLUDED
+#define EAX_UTILS_INCLUDED
+
+#include <algorithm>
+#include <cstdint>
+#include <string>
+#include <type_traits>
+
+using EaxDirtyFlags = unsigned int;
+
+struct EaxAlLowPassParam {
+    float gain;
+    float gain_hf;
+};
+
+void eax_log_exception(const char *message) 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;
+};
+
+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
diff --git a/al/eax/x_ram.h b/al/eax/x_ram.h
new file mode 100644 (file)
index 0000000..438b991
--- /dev/null
@@ -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
diff --git a/al/effect.cpp b/al/effect.cpp
new file mode 100644 (file)
index 0000000..bde8991
--- /dev/null
@@ -0,0 +1,766 @@
+/**
+ * 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 "effect.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <new>
+#include <numeric>
+#include <utility>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+#include "AL/efx-presets.h"
+#include "AL/efx.h"
+
+#include "albit.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 "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 },
+    { "reverb",      REVERB_EFFECT,      AL_EFFECT_REVERB },
+    { "autowah",     AUTOWAH_EFFECT,     AL_EFFECT_AUTOWAH },
+    { "chorus",      CHORUS_EFFECT,      AL_EFFECT_CHORUS },
+    { "compressor",  COMPRESSOR_EFFECT,  AL_EFFECT_COMPRESSOR },
+    { "distortion",  DISTORTION_EFFECT,  AL_EFFECT_DISTORTION },
+    { "echo",        ECHO_EFFECT,        AL_EFFECT_ECHO },
+    { "equalizer",   EQUALIZER_EFFECT,   AL_EFFECT_EQUALIZER },
+    { "flanger",     FLANGER_EFFECT,     AL_EFFECT_FLANGER },
+    { "fshifter",    FSHIFTER_EFFECT,    AL_EFFECT_FREQUENCY_SHIFTER },
+    { "modulator",   MODULATOR_EFFECT,   AL_EFFECT_RING_MODULATOR },
+    { "pshifter",    PSHIFTER_EFFECT,    AL_EFFECT_PITCH_SHIFTER },
+    { "vmorpher",    VMORPHER_EFFECT,    AL_EFFECT_VOCAL_MORPHER },
+    { "dedicated",   DEDICATED_EFFECT,   AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT },
+    { "dedicated",   DEDICATED_EFFECT,   AL_EFFECT_DEDICATED_DIALOGUE },
+    { "convolution", CONVOLUTION_EFFECT, AL_EFFECT_CONVOLUTION_REVERB_SOFT },
+};
+
+bool DisabledEffects[MAX_EFFECTS];
+
+
+effect_exception::effect_exception(ALenum code, const char *msg, ...) : mErrorCode{code}
+{
+    std::va_list args;
+    va_start(args, msg);
+    setMessage(msg, args);
+    va_end(args);
+}
+effect_exception::~effect_exception() = default;
+
+namespace {
+
+struct EffectPropsItem {
+    ALenum Type;
+    const EffectProps &DefaultProps;
+    const EffectVtable &Vtable;
+};
+constexpr EffectPropsItem EffectPropsList[] = {
+    { AL_EFFECT_NULL, NullEffectProps, NullEffectVtable },
+    { AL_EFFECT_EAXREVERB, ReverbEffectProps, ReverbEffectVtable },
+    { AL_EFFECT_REVERB, StdReverbEffectProps, StdReverbEffectVtable },
+    { AL_EFFECT_AUTOWAH, AutowahEffectProps, AutowahEffectVtable },
+    { AL_EFFECT_CHORUS, ChorusEffectProps, ChorusEffectVtable },
+    { AL_EFFECT_COMPRESSOR, CompressorEffectProps, CompressorEffectVtable },
+    { AL_EFFECT_DISTORTION, DistortionEffectProps, DistortionEffectVtable },
+    { AL_EFFECT_ECHO, EchoEffectProps, EchoEffectVtable },
+    { AL_EFFECT_EQUALIZER, EqualizerEffectProps, EqualizerEffectVtable },
+    { AL_EFFECT_FLANGER, FlangerEffectProps, FlangerEffectVtable },
+    { AL_EFFECT_FREQUENCY_SHIFTER, FshifterEffectProps, FshifterEffectVtable },
+    { AL_EFFECT_RING_MODULATOR, ModulatorEffectProps, ModulatorEffectVtable },
+    { AL_EFFECT_PITCH_SHIFTER, PshifterEffectProps, PshifterEffectVtable },
+    { AL_EFFECT_VOCAL_MORPHER, VmorpherEffectProps, VmorpherEffectVtable },
+    { AL_EFFECT_DEDICATED_DIALOGUE, DedicatedEffectProps, DedicatedEffectVtable },
+    { AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT, DedicatedEffectProps, DedicatedEffectVtable },
+    { AL_EFFECT_CONVOLUTION_REVERB_SOFT, ConvolutionEffectProps, ConvolutionEffectVtable },
+};
+
+
+void ALeffect_setParami(ALeffect *effect, ALenum param, int value)
+{ effect->vtab->setParami(&effect->Props, param, value); }
+void ALeffect_setParamiv(ALeffect *effect, ALenum param, const int *values)
+{ effect->vtab->setParamiv(&effect->Props, param, values); }
+void ALeffect_setParamf(ALeffect *effect, ALenum param, float value)
+{ effect->vtab->setParamf(&effect->Props, param, value); }
+void ALeffect_setParamfv(ALeffect *effect, ALenum param, const float *values)
+{ effect->vtab->setParamfv(&effect->Props, param, values); }
+
+void ALeffect_getParami(const ALeffect *effect, ALenum param, int *value)
+{ effect->vtab->getParami(&effect->Props, param, value); }
+void ALeffect_getParamiv(const ALeffect *effect, ALenum param, int *values)
+{ effect->vtab->getParamiv(&effect->Props, param, values); }
+void ALeffect_getParamf(const ALeffect *effect, ALenum param, float *value)
+{ effect->vtab->getParamf(&effect->Props, param, value); }
+void ALeffect_getParamfv(const ALeffect *effect, ALenum param, float *values)
+{ effect->vtab->getParamfv(&effect->Props, param, values); }
+
+
+const EffectPropsItem *getEffectPropsItemByType(ALenum type)
+{
+    auto iter = std::find_if(std::begin(EffectPropsList), std::end(EffectPropsList),
+        [type](const EffectPropsItem &item) noexcept -> bool
+        { return item.Type == type; });
+    return (iter != std::end(EffectPropsList)) ? al::to_address(iter) : nullptr;
+}
+
+void InitEffectParams(ALeffect *effect, ALenum type)
+{
+    const EffectPropsItem *item{getEffectPropsItemByType(type)};
+    if(item)
+    {
+        effect->Props = item->DefaultProps;
+        effect->vtab = &item->Vtable;
+    }
+    else
+    {
+        effect->Props = EffectProps{};
+        effect->vtab = &NullEffectVtable;
+    }
+    effect->type = type;
+}
+
+bool EnsureEffects(ALCdevice *device, size_t needed)
+{
+    size_t count{std::accumulate(device->EffectList.cbegin(), device->EffectList.cend(), size_t{0},
+        [](size_t cur, const EffectSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<ALuint>(al::popcount(sublist.FreeMask)); })};
+
+    while(needed > count)
+    {
+        if(device->EffectList.size() >= 1<<25) UNLIKELY
+            return false;
+
+        device->EffectList.emplace_back();
+        auto sublist = device->EffectList.end() - 1;
+        sublist->FreeMask = ~0_u64;
+        sublist->Effects = static_cast<ALeffect*>(al_calloc(alignof(ALeffect), sizeof(ALeffect)*64));
+        if(!sublist->Effects) UNLIKELY
+        {
+            device->EffectList.pop_back();
+            return false;
+        }
+        count += 64;
+    }
+    return true;
+}
+
+ALeffect *AllocEffect(ALCdevice *device)
+{
+    auto sublist = std::find_if(device->EffectList.begin(), device->EffectList.end(),
+        [](const EffectSubList &entry) noexcept -> bool
+        { 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{al::construct_at(sublist->Effects + slidx)};
+    InitEffectParams(effect, AL_EFFECT_NULL);
+
+    /* Add 1 to avoid effect ID 0. */
+    effect->id = ((lidx<<6) | slidx) + 1;
+
+    sublist->FreeMask &= ~(1_u64 << slidx);
+
+    return effect;
+}
+
+void FreeEffect(ALCdevice *device, ALeffect *effect)
+{
+    const ALuint id{effect->id - 1};
+    const size_t lidx{id >> 6};
+    const ALuint slidx{id & 0x3f};
+
+    al::destroy_at(effect);
+
+    device->EffectList[lidx].FreeMask |= 1_u64 << slidx;
+}
+
+inline ALeffect *LookupEffect(ALCdevice *device, ALuint id)
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= device->EffectList.size()) UNLIKELY
+        return nullptr;
+    EffectSubList &sublist = device->EffectList[lidx];
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Effects + slidx;
+}
+
+} // namespace
+
+AL_API void AL_APIENTRY alGenEffects(ALsizei n, ALuint *effects)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Generating %d effects", n);
+    if(n <= 0) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+    if(!EnsureEffects(device, static_cast<ALuint>(n)))
+    {
+        context->setError(AL_OUT_OF_MEMORY, "Failed to allocate %d effect%s", n, (n==1)?"":"s");
+        return;
+    }
+
+    if(n == 1) LIKELY
+    {
+        /* Special handling for the easy and normal case. */
+        ALeffect *effect{AllocEffect(device)};
+        effects[0] = effect->id;
+    }
+    else
+    {
+        /* Store the allocated buffer IDs in a separate local list, to avoid
+         * modifying the user storage in case of failure.
+         */
+        al::vector<ALuint> ids;
+        ids.reserve(static_cast<ALuint>(n));
+        do {
+            ALeffect *effect{AllocEffect(device)};
+            ids.emplace_back(effect->id);
+        } while(--n);
+        std::copy(ids.cbegin(), ids.cend(), effects);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDeleteEffects(ALsizei n, const ALuint *effects)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Deleting %d effects", n);
+    if(n <= 0) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    /* First try to find any effects that are invalid. */
+    auto validate_effect = [device](const ALuint eid) -> bool
+    { return !eid || LookupEffect(device, eid) != nullptr; };
+
+    const ALuint *effects_end = effects + n;
+    auto inveffect = std::find_if_not(effects, effects_end, validate_effect);
+    if(inveffect != effects_end) UNLIKELY
+    {
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", *inveffect);
+        return;
+    }
+
+    /* All good. Delete non-0 effect IDs. */
+    auto delete_effect = [device](ALuint eid) -> void
+    {
+        ALeffect *effect{eid ? LookupEffect(device, eid) : nullptr};
+        if(effect) FreeEffect(device, effect);
+    };
+    std::for_each(effects, effects_end, delete_effect);
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alIsEffect(ALuint effect)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(context) LIKELY
+    {
+        ALCdevice *device{context->mALDevice.get()};
+        std::lock_guard<std::mutex> _{device->EffectLock};
+        if(!effect || LookupEffect(device, effect))
+            return AL_TRUE;
+    }
+    return AL_FALSE;
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alEffecti(ALuint effect, ALenum param, ALint value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else if(param == AL_EFFECT_TYPE)
+    {
+        bool isOk{value == AL_EFFECT_NULL};
+        if(!isOk)
+        {
+            for(const EffectList &effectitem : gEffectList)
+            {
+                if(value == effectitem.val && !DisabledEffects[effectitem.type])
+                {
+                    isOk = true;
+                    break;
+                }
+            }
+        }
+
+        if(isOk)
+            InitEffectParams(aleffect, value);
+        else
+            context->setError(AL_INVALID_VALUE, "Effect type 0x%04x not supported", value);
+    }
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_setParami(aleffect, param, value);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alEffectiv(ALuint effect, ALenum param, const ALint *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_EFFECT_TYPE:
+        alEffecti(effect, param, values[0]);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_setParamiv(aleffect, param, values);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alEffectf(ALuint effect, ALenum param, ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_setParamf(aleffect, param, value);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alEffectfv(ALuint effect, ALenum param, const ALfloat *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_setParamfv(aleffect, param, values);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetEffecti(ALuint effect, ALenum param, ALint *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    const ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else if(param == AL_EFFECT_TYPE)
+        *value = aleffect->type;
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_getParami(aleffect, param, value);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetEffectiv(ALuint effect, ALenum param, ALint *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_EFFECT_TYPE:
+        alGetEffecti(effect, param, values);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    const ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_getParamiv(aleffect, param, values);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetEffectf(ALuint effect, ALenum param, ALfloat *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    const ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_getParamf(aleffect, param, value);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetEffectfv(ALuint effect, ALenum param, ALfloat *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->EffectLock};
+
+    const ALeffect *aleffect{LookupEffect(device, effect)};
+    if(!aleffect) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid effect ID %u", effect);
+    else try
+    {
+        /* Call the appropriate handler */
+        ALeffect_getParamfv(aleffect, param, values);
+    }
+    catch(effect_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+
+void InitEffect(ALeffect *effect)
+{
+    InitEffectParams(effect, AL_EFFECT_NULL);
+}
+
+EffectSubList::~EffectSubList()
+{
+    uint64_t usemask{~FreeMask};
+    while(usemask)
+    {
+        const int idx{al::countr_zero(usemask)};
+        al::destroy_at(Effects+idx);
+        usemask &= ~(1_u64 << idx);
+    }
+    FreeMask = ~usemask;
+    al_free(Effects);
+    Effects = nullptr;
+}
+
+
+#define DECL(x) { #x, EFX_REVERB_PRESET_##x }
+static const struct {
+    const char name[32];
+    EFXEAXREVERBPROPERTIES props;
+} reverblist[] = {
+    DECL(GENERIC),
+    DECL(PADDEDCELL),
+    DECL(ROOM),
+    DECL(BATHROOM),
+    DECL(LIVINGROOM),
+    DECL(STONEROOM),
+    DECL(AUDITORIUM),
+    DECL(CONCERTHALL),
+    DECL(CAVE),
+    DECL(ARENA),
+    DECL(HANGAR),
+    DECL(CARPETEDHALLWAY),
+    DECL(HALLWAY),
+    DECL(STONECORRIDOR),
+    DECL(ALLEY),
+    DECL(FOREST),
+    DECL(CITY),
+    DECL(MOUNTAINS),
+    DECL(QUARRY),
+    DECL(PLAIN),
+    DECL(PARKINGLOT),
+    DECL(SEWERPIPE),
+    DECL(UNDERWATER),
+    DECL(DRUGGED),
+    DECL(DIZZY),
+    DECL(PSYCHOTIC),
+
+    DECL(CASTLE_SMALLROOM),
+    DECL(CASTLE_SHORTPASSAGE),
+    DECL(CASTLE_MEDIUMROOM),
+    DECL(CASTLE_LARGEROOM),
+    DECL(CASTLE_LONGPASSAGE),
+    DECL(CASTLE_HALL),
+    DECL(CASTLE_CUPBOARD),
+    DECL(CASTLE_COURTYARD),
+    DECL(CASTLE_ALCOVE),
+
+    DECL(FACTORY_SMALLROOM),
+    DECL(FACTORY_SHORTPASSAGE),
+    DECL(FACTORY_MEDIUMROOM),
+    DECL(FACTORY_LARGEROOM),
+    DECL(FACTORY_LONGPASSAGE),
+    DECL(FACTORY_HALL),
+    DECL(FACTORY_CUPBOARD),
+    DECL(FACTORY_COURTYARD),
+    DECL(FACTORY_ALCOVE),
+
+    DECL(ICEPALACE_SMALLROOM),
+    DECL(ICEPALACE_SHORTPASSAGE),
+    DECL(ICEPALACE_MEDIUMROOM),
+    DECL(ICEPALACE_LARGEROOM),
+    DECL(ICEPALACE_LONGPASSAGE),
+    DECL(ICEPALACE_HALL),
+    DECL(ICEPALACE_CUPBOARD),
+    DECL(ICEPALACE_COURTYARD),
+    DECL(ICEPALACE_ALCOVE),
+
+    DECL(SPACESTATION_SMALLROOM),
+    DECL(SPACESTATION_SHORTPASSAGE),
+    DECL(SPACESTATION_MEDIUMROOM),
+    DECL(SPACESTATION_LARGEROOM),
+    DECL(SPACESTATION_LONGPASSAGE),
+    DECL(SPACESTATION_HALL),
+    DECL(SPACESTATION_CUPBOARD),
+    DECL(SPACESTATION_ALCOVE),
+
+    DECL(WOODEN_SMALLROOM),
+    DECL(WOODEN_SHORTPASSAGE),
+    DECL(WOODEN_MEDIUMROOM),
+    DECL(WOODEN_LARGEROOM),
+    DECL(WOODEN_LONGPASSAGE),
+    DECL(WOODEN_HALL),
+    DECL(WOODEN_CUPBOARD),
+    DECL(WOODEN_COURTYARD),
+    DECL(WOODEN_ALCOVE),
+
+    DECL(SPORT_EMPTYSTADIUM),
+    DECL(SPORT_SQUASHCOURT),
+    DECL(SPORT_SMALLSWIMMINGPOOL),
+    DECL(SPORT_LARGESWIMMINGPOOL),
+    DECL(SPORT_GYMNASIUM),
+    DECL(SPORT_FULLSTADIUM),
+    DECL(SPORT_STADIUMTANNOY),
+
+    DECL(PREFAB_WORKSHOP),
+    DECL(PREFAB_SCHOOLROOM),
+    DECL(PREFAB_PRACTISEROOM),
+    DECL(PREFAB_OUTHOUSE),
+    DECL(PREFAB_CARAVAN),
+
+    DECL(DOME_TOMB),
+    DECL(PIPE_SMALL),
+    DECL(DOME_SAINTPAULS),
+    DECL(PIPE_LONGTHIN),
+    DECL(PIPE_LARGE),
+    DECL(PIPE_RESONANT),
+
+    DECL(OUTDOORS_BACKYARD),
+    DECL(OUTDOORS_ROLLINGPLAINS),
+    DECL(OUTDOORS_DEEPCANYON),
+    DECL(OUTDOORS_CREEK),
+    DECL(OUTDOORS_VALLEY),
+
+    DECL(MOOD_HEAVEN),
+    DECL(MOOD_HELL),
+    DECL(MOOD_MEMORY),
+
+    DECL(DRIVING_COMMENTATOR),
+    DECL(DRIVING_PITGARAGE),
+    DECL(DRIVING_INCAR_RACER),
+    DECL(DRIVING_INCAR_SPORTS),
+    DECL(DRIVING_INCAR_LUXURY),
+    DECL(DRIVING_FULLGRANDSTAND),
+    DECL(DRIVING_EMPTYGRANDSTAND),
+    DECL(DRIVING_TUNNEL),
+
+    DECL(CITY_STREETS),
+    DECL(CITY_SUBWAY),
+    DECL(CITY_MUSEUM),
+    DECL(CITY_LIBRARY),
+    DECL(CITY_UNDERPASS),
+    DECL(CITY_ABANDONED),
+
+    DECL(DUSTYROOM),
+    DECL(CHAPEL),
+    DECL(SMALLWATERROOM),
+};
+#undef DECL
+
+void LoadReverbPreset(const char *name, ALeffect *effect)
+{
+    if(al::strcasecmp(name, "NONE") == 0)
+    {
+        InitEffectParams(effect, AL_EFFECT_NULL);
+        TRACE("Loading reverb '%s'\n", "NONE");
+        return;
+    }
+
+    if(!DisabledEffects[EAXREVERB_EFFECT])
+        InitEffectParams(effect, AL_EFFECT_EAXREVERB);
+    else if(!DisabledEffects[REVERB_EFFECT])
+        InitEffectParams(effect, AL_EFFECT_REVERB);
+    else
+        InitEffectParams(effect, AL_EFFECT_NULL);
+    for(const auto &reverbitem : reverblist)
+    {
+        const EFXEAXREVERBPROPERTIES *props;
+
+        if(al::strcasecmp(name, reverbitem.name) != 0)
+            continue;
+
+        TRACE("Loading reverb '%s'\n", reverbitem.name);
+        props = &reverbitem.props;
+        effect->Props.Reverb.Density   = props->flDensity;
+        effect->Props.Reverb.Diffusion = props->flDiffusion;
+        effect->Props.Reverb.Gain   = props->flGain;
+        effect->Props.Reverb.GainHF = props->flGainHF;
+        effect->Props.Reverb.GainLF = props->flGainLF;
+        effect->Props.Reverb.DecayTime    = props->flDecayTime;
+        effect->Props.Reverb.DecayHFRatio = props->flDecayHFRatio;
+        effect->Props.Reverb.DecayLFRatio = props->flDecayLFRatio;
+        effect->Props.Reverb.ReflectionsGain   = props->flReflectionsGain;
+        effect->Props.Reverb.ReflectionsDelay  = props->flReflectionsDelay;
+        effect->Props.Reverb.ReflectionsPan[0] = props->flReflectionsPan[0];
+        effect->Props.Reverb.ReflectionsPan[1] = props->flReflectionsPan[1];
+        effect->Props.Reverb.ReflectionsPan[2] = props->flReflectionsPan[2];
+        effect->Props.Reverb.LateReverbGain   = props->flLateReverbGain;
+        effect->Props.Reverb.LateReverbDelay  = props->flLateReverbDelay;
+        effect->Props.Reverb.LateReverbPan[0] = props->flLateReverbPan[0];
+        effect->Props.Reverb.LateReverbPan[1] = props->flLateReverbPan[1];
+        effect->Props.Reverb.LateReverbPan[2] = props->flLateReverbPan[2];
+        effect->Props.Reverb.EchoTime  = props->flEchoTime;
+        effect->Props.Reverb.EchoDepth = props->flEchoDepth;
+        effect->Props.Reverb.ModulationTime  = props->flModulationTime;
+        effect->Props.Reverb.ModulationDepth = props->flModulationDepth;
+        effect->Props.Reverb.AirAbsorptionGainHF = props->flAirAbsorptionGainHF;
+        effect->Props.Reverb.HFReference = props->flHFReference;
+        effect->Props.Reverb.LFReference = props->flLFReference;
+        effect->Props.Reverb.RoomRolloffFactor = props->flRoomRolloffFactor;
+        effect->Props.Reverb.DecayHFLimit = props->iDecayHFLimit ? AL_TRUE : AL_FALSE;
+        return;
+    }
+
+    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;
+}
diff --git a/al/effect.h b/al/effect.h
new file mode 100644 (file)
index 0000000..a1d4331
--- /dev/null
@@ -0,0 +1,62 @@
+#ifndef AL_EFFECT_H
+#define AL_EFFECT_H
+
+#include "AL/al.h"
+#include "AL/efx.h"
+
+#include "al/effects/effects.h"
+#include "alc/effects/base.h"
+
+
+enum {
+    EAXREVERB_EFFECT = 0,
+    REVERB_EFFECT,
+    AUTOWAH_EFFECT,
+    CHORUS_EFFECT,
+    COMPRESSOR_EFFECT,
+    DISTORTION_EFFECT,
+    ECHO_EFFECT,
+    EQUALIZER_EFFECT,
+    FLANGER_EFFECT,
+    FSHIFTER_EFFECT,
+    MODULATOR_EFFECT,
+    PSHIFTER_EFFECT,
+    VMORPHER_EFFECT,
+    DEDICATED_EFFECT,
+    CONVOLUTION_EFFECT,
+
+    MAX_EFFECTS
+};
+extern bool DisabledEffects[MAX_EFFECTS];
+
+extern float ReverbBoost;
+
+struct EffectList {
+    const char name[16];
+    int type;
+    ALenum val;
+};
+extern const EffectList gEffectList[16];
+
+
+struct ALeffect {
+    // Effect type (AL_EFFECT_NULL, ...)
+    ALenum type{AL_EFFECT_NULL};
+
+    EffectProps Props{};
+
+    const EffectVtable *vtab{nullptr};
+
+    /* Self ID */
+    ALuint id{0u};
+
+    DISABLE_ALLOC()
+};
+
+void InitEffect(ALeffect *effect);
+
+void LoadReverbPreset(const char *name, ALeffect *effect);
+
+bool IsValidEffectType(ALenum type) noexcept;
+
+#endif
diff --git a/al/effects/autowah.cpp b/al/effects/autowah.cpp
new file mode 100644 (file)
index 0000000..129318f
--- /dev/null
@@ -0,0 +1,252 @@
+
+#include "config.h"
+
+#include <cmath>
+#include <cstdlib>
+
+#include <algorithm>
+
+#include "AL/efx.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)
+{
+    switch(param)
+    {
+    case AL_AUTOWAH_ATTACK_TIME:
+        if(!(val >= AL_AUTOWAH_MIN_ATTACK_TIME && val <= AL_AUTOWAH_MAX_ATTACK_TIME))
+            throw effect_exception{AL_INVALID_VALUE, "Autowah attack time out of range"};
+        props->Autowah.AttackTime = val;
+        break;
+
+    case AL_AUTOWAH_RELEASE_TIME:
+        if(!(val >= AL_AUTOWAH_MIN_RELEASE_TIME && val <= AL_AUTOWAH_MAX_RELEASE_TIME))
+            throw effect_exception{AL_INVALID_VALUE, "Autowah release time out of range"};
+        props->Autowah.ReleaseTime = val;
+        break;
+
+    case AL_AUTOWAH_RESONANCE:
+        if(!(val >= AL_AUTOWAH_MIN_RESONANCE && val <= AL_AUTOWAH_MAX_RESONANCE))
+            throw effect_exception{AL_INVALID_VALUE, "Autowah resonance out of range"};
+        props->Autowah.Resonance = val;
+        break;
+
+    case AL_AUTOWAH_PEAK_GAIN:
+        if(!(val >= AL_AUTOWAH_MIN_PEAK_GAIN && val <= AL_AUTOWAH_MAX_PEAK_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Autowah peak gain out of range"};
+        props->Autowah.PeakGain = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid autowah float property 0x%04x", param};
+    }
+}
+void Autowah_setParamfv(EffectProps *props,  ALenum param, const float *vals)
+{ Autowah_setParamf(props, param, vals[0]); }
+
+void Autowah_setParami(EffectProps*, ALenum param, int)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid autowah integer property 0x%04x", param}; }
+void Autowah_setParamiv(EffectProps*, ALenum param, const int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid autowah integer vector property 0x%04x",
+        param};
+}
+
+void Autowah_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_AUTOWAH_ATTACK_TIME:
+        *val = props->Autowah.AttackTime;
+        break;
+
+    case AL_AUTOWAH_RELEASE_TIME:
+        *val = props->Autowah.ReleaseTime;
+        break;
+
+    case AL_AUTOWAH_RESONANCE:
+        *val = props->Autowah.Resonance;
+        break;
+
+    case AL_AUTOWAH_PEAK_GAIN:
+        *val = props->Autowah.PeakGain;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid autowah float property 0x%04x", param};
+    }
+
+}
+void Autowah_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Autowah_getParamf(props, param, vals); }
+
+void Autowah_getParami(const EffectProps*, ALenum param, int*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid autowah integer property 0x%04x", param}; }
+void Autowah_getParamiv(const EffectProps*, ALenum param, int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid autowah integer vector property 0x%04x",
+        param};
+}
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Autowah.AttackTime = AL_AUTOWAH_DEFAULT_ATTACK_TIME;
+    props.Autowah.ReleaseTime = AL_AUTOWAH_DEFAULT_RELEASE_TIME;
+    props.Autowah.Resonance = AL_AUTOWAH_DEFAULT_RESONANCE;
+    props.Autowah.PeakGain = AL_AUTOWAH_DEFAULT_PEAK_GAIN;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Autowah);
+
+const EffectProps AutowahEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using AutowahCommitter = EaxCommitter<EaxAutowahCommitter>;
+
+struct AttackTimeValidator {
+    void operator()(float flAttackTime) const
+    {
+        eax_validate_range<AutowahCommitter::Exception>(
+            "Attack Time",
+            flAttackTime,
+            EAXAUTOWAH_MINATTACKTIME,
+            EAXAUTOWAH_MAXATTACKTIME);
+    }
+}; // AttackTimeValidator
+
+struct ReleaseTimeValidator {
+    void operator()(float flReleaseTime) const
+    {
+        eax_validate_range<AutowahCommitter::Exception>(
+            "Release Time",
+            flReleaseTime,
+            EAXAUTOWAH_MINRELEASETIME,
+            EAXAUTOWAH_MAXRELEASETIME);
+    }
+}; // ReleaseTimeValidator
+
+struct ResonanceValidator {
+    void operator()(long lResonance) const
+    {
+        eax_validate_range<AutowahCommitter::Exception>(
+            "Resonance",
+            lResonance,
+            EAXAUTOWAH_MINRESONANCE,
+            EAXAUTOWAH_MAXRESONANCE);
+    }
+}; // ResonanceValidator
+
+struct PeakLevelValidator {
+    void operator()(long lPeakLevel) const
+    {
+        eax_validate_range<AutowahCommitter::Exception>(
+            "Peak Level",
+            lPeakLevel,
+            EAXAUTOWAH_MINPEAKLEVEL,
+            EAXAUTOWAH_MAXPEAKLEVEL);
+    }
+}; // PeakLevelValidator
+
+struct AllValidator {
+    void operator()(const EAXAUTOWAHPROPERTIES& all) const
+    {
+        AttackTimeValidator{}(all.flAttackTime);
+        ReleaseTimeValidator{}(all.flReleaseTime);
+        ResonanceValidator{}(all.lResonance);
+        PeakLevelValidator{}(all.lPeakLevel);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct AutowahCommitter::Exception : public EaxException
+{
+    explicit Exception(const char *message) : EaxException{"EAX_AUTOWAH_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void AutowahCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool AutowahCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType
+        && mEaxProps.mAutowah.flAttackTime == props.mAutowah.flAttackTime
+        && mEaxProps.mAutowah.flReleaseTime == props.mAutowah.flReleaseTime
+        && mEaxProps.mAutowah.lResonance == props.mAutowah.lResonance
+        && mEaxProps.mAutowah.lPeakLevel == props.mAutowah.lPeakLevel)
+        return false;
+
+    mEaxProps = props;
+
+    mAlProps.Autowah.AttackTime = props.mAutowah.flAttackTime;
+    mAlProps.Autowah.ReleaseTime = props.mAutowah.flReleaseTime;
+    mAlProps.Autowah.Resonance = level_mb_to_gain(static_cast<float>(props.mAutowah.lResonance));
+    mAlProps.Autowah.PeakGain = level_mb_to_gain(static_cast<float>(props.mAutowah.lPeakLevel));
+
+    return true;
+}
+
+template<>
+void AutowahCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::Autowah;
+    props.mAutowah.flAttackTime = EAXAUTOWAH_DEFAULTATTACKTIME;
+    props.mAutowah.flReleaseTime = EAXAUTOWAH_DEFAULTRELEASETIME;
+    props.mAutowah.lResonance = EAXAUTOWAH_DEFAULTRESONANCE;
+    props.mAutowah.lPeakLevel = EAXAUTOWAH_DEFAULTPEAKLEVEL;
+}
+
+template<>
+void AutowahCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXAUTOWAH_NONE: break;
+    case EAXAUTOWAH_ALLPARAMETERS: call.set_value<Exception>(props.mAutowah); break;
+    case EAXAUTOWAH_ATTACKTIME: call.set_value<Exception>(props.mAutowah.flAttackTime); break;
+    case EAXAUTOWAH_RELEASETIME: call.set_value<Exception>(props.mAutowah.flReleaseTime); break;
+    case EAXAUTOWAH_RESONANCE: call.set_value<Exception>(props.mAutowah.lResonance); break;
+    case EAXAUTOWAH_PEAKLEVEL: call.set_value<Exception>(props.mAutowah.lPeakLevel); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void AutowahCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXAUTOWAH_NONE: break;
+    case EAXAUTOWAH_ALLPARAMETERS: defer<AllValidator>(call, props.mAutowah); break;
+    case EAXAUTOWAH_ATTACKTIME: defer<AttackTimeValidator>(call, props.mAutowah.flAttackTime); break;
+    case EAXAUTOWAH_RELEASETIME: defer<ReleaseTimeValidator>(call, props.mAutowah.flReleaseTime); break;
+    case EAXAUTOWAH_RESONANCE: defer<ResonanceValidator>(call, props.mAutowah.lResonance); break;
+    case EAXAUTOWAH_PEAKLEVEL: defer<PeakLevelValidator>(call, props.mAutowah.lPeakLevel); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/chorus.cpp b/al/effects/chorus.cpp
new file mode 100644 (file)
index 0000000..305259a
--- /dev/null
@@ -0,0 +1,724 @@
+
+#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"
+
+#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");
+
+inline al::optional<ChorusWaveform> WaveformFromEnum(ALenum type)
+{
+    switch(type)
+    {
+    case AL_CHORUS_WAVEFORM_SINUSOID: return ChorusWaveform::Sinusoid;
+    case AL_CHORUS_WAVEFORM_TRIANGLE: return ChorusWaveform::Triangle;
+    }
+    return al::nullopt;
+}
+inline ALenum EnumFromWaveform(ChorusWaveform type)
+{
+    switch(type)
+    {
+    case ChorusWaveform::Sinusoid: return AL_CHORUS_WAVEFORM_SINUSOID;
+    case ChorusWaveform::Triangle: return AL_CHORUS_WAVEFORM_TRIANGLE;
+    }
+    throw std::runtime_error{"Invalid chorus waveform: "+std::to_string(static_cast<int>(type))};
+}
+
+void Chorus_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_CHORUS_WAVEFORM:
+        if(auto formopt = WaveformFromEnum(val))
+            props->Chorus.Waveform = *formopt;
+        else
+            throw effect_exception{AL_INVALID_VALUE, "Invalid chorus waveform: 0x%04x", val};
+        break;
+
+    case AL_CHORUS_PHASE:
+        if(!(val >= AL_CHORUS_MIN_PHASE && val <= AL_CHORUS_MAX_PHASE))
+            throw effect_exception{AL_INVALID_VALUE, "Chorus phase out of range: %d", val};
+        props->Chorus.Phase = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid chorus integer property 0x%04x", param};
+    }
+}
+void Chorus_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ Chorus_setParami(props, param, vals[0]); }
+void Chorus_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_CHORUS_RATE:
+        if(!(val >= AL_CHORUS_MIN_RATE && val <= AL_CHORUS_MAX_RATE))
+            throw effect_exception{AL_INVALID_VALUE, "Chorus rate out of range: %f", val};
+        props->Chorus.Rate = val;
+        break;
+
+    case AL_CHORUS_DEPTH:
+        if(!(val >= AL_CHORUS_MIN_DEPTH && val <= AL_CHORUS_MAX_DEPTH))
+            throw effect_exception{AL_INVALID_VALUE, "Chorus depth out of range: %f", val};
+        props->Chorus.Depth = val;
+        break;
+
+    case AL_CHORUS_FEEDBACK:
+        if(!(val >= AL_CHORUS_MIN_FEEDBACK && val <= AL_CHORUS_MAX_FEEDBACK))
+            throw effect_exception{AL_INVALID_VALUE, "Chorus feedback out of range: %f", val};
+        props->Chorus.Feedback = val;
+        break;
+
+    case AL_CHORUS_DELAY:
+        if(!(val >= AL_CHORUS_MIN_DELAY && val <= AL_CHORUS_MAX_DELAY))
+            throw effect_exception{AL_INVALID_VALUE, "Chorus delay out of range: %f", val};
+        props->Chorus.Delay = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid chorus float property 0x%04x", param};
+    }
+}
+void Chorus_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Chorus_setParamf(props, param, vals[0]); }
+
+void Chorus_getParami(const EffectProps *props, ALenum param, int *val)
+{
+    switch(param)
+    {
+    case AL_CHORUS_WAVEFORM:
+        *val = EnumFromWaveform(props->Chorus.Waveform);
+        break;
+
+    case AL_CHORUS_PHASE:
+        *val = props->Chorus.Phase;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid chorus integer property 0x%04x", param};
+    }
+}
+void Chorus_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ Chorus_getParami(props, param, vals); }
+void Chorus_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_CHORUS_RATE:
+        *val = props->Chorus.Rate;
+        break;
+
+    case AL_CHORUS_DEPTH:
+        *val = props->Chorus.Depth;
+        break;
+
+    case AL_CHORUS_FEEDBACK:
+        *val = props->Chorus.Feedback;
+        break;
+
+    case AL_CHORUS_DELAY:
+        *val = props->Chorus.Delay;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid chorus float property 0x%04x", param};
+    }
+}
+void Chorus_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Chorus_getParamf(props, param, vals); }
+
+EffectProps genDefaultChorusProps() noexcept
+{
+    EffectProps props{};
+    props.Chorus.Waveform = *WaveformFromEnum(AL_CHORUS_DEFAULT_WAVEFORM);
+    props.Chorus.Phase = AL_CHORUS_DEFAULT_PHASE;
+    props.Chorus.Rate = AL_CHORUS_DEFAULT_RATE;
+    props.Chorus.Depth = AL_CHORUS_DEFAULT_DEPTH;
+    props.Chorus.Feedback = AL_CHORUS_DEFAULT_FEEDBACK;
+    props.Chorus.Delay = AL_CHORUS_DEFAULT_DELAY;
+    return props;
+}
+
+
+void Flanger_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_FLANGER_WAVEFORM:
+        if(auto formopt = WaveformFromEnum(val))
+            props->Chorus.Waveform = *formopt;
+        else
+            throw effect_exception{AL_INVALID_VALUE, "Invalid flanger waveform: 0x%04x", val};
+        break;
+
+    case AL_FLANGER_PHASE:
+        if(!(val >= AL_FLANGER_MIN_PHASE && val <= AL_FLANGER_MAX_PHASE))
+            throw effect_exception{AL_INVALID_VALUE, "Flanger phase out of range: %d", val};
+        props->Chorus.Phase = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid flanger integer property 0x%04x", param};
+    }
+}
+void Flanger_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ Flanger_setParami(props, param, vals[0]); }
+void Flanger_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_FLANGER_RATE:
+        if(!(val >= AL_FLANGER_MIN_RATE && val <= AL_FLANGER_MAX_RATE))
+            throw effect_exception{AL_INVALID_VALUE, "Flanger rate out of range: %f", val};
+        props->Chorus.Rate = val;
+        break;
+
+    case AL_FLANGER_DEPTH:
+        if(!(val >= AL_FLANGER_MIN_DEPTH && val <= AL_FLANGER_MAX_DEPTH))
+            throw effect_exception{AL_INVALID_VALUE, "Flanger depth out of range: %f", val};
+        props->Chorus.Depth = val;
+        break;
+
+    case AL_FLANGER_FEEDBACK:
+        if(!(val >= AL_FLANGER_MIN_FEEDBACK && val <= AL_FLANGER_MAX_FEEDBACK))
+            throw effect_exception{AL_INVALID_VALUE, "Flanger feedback out of range: %f", val};
+        props->Chorus.Feedback = val;
+        break;
+
+    case AL_FLANGER_DELAY:
+        if(!(val >= AL_FLANGER_MIN_DELAY && val <= AL_FLANGER_MAX_DELAY))
+            throw effect_exception{AL_INVALID_VALUE, "Flanger delay out of range: %f", val};
+        props->Chorus.Delay = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid flanger float property 0x%04x", param};
+    }
+}
+void Flanger_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Flanger_setParamf(props, param, vals[0]); }
+
+void Flanger_getParami(const EffectProps *props, ALenum param, int *val)
+{
+    switch(param)
+    {
+    case AL_FLANGER_WAVEFORM:
+        *val = EnumFromWaveform(props->Chorus.Waveform);
+        break;
+
+    case AL_FLANGER_PHASE:
+        *val = props->Chorus.Phase;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid flanger integer property 0x%04x", param};
+    }
+}
+void Flanger_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ Flanger_getParami(props, param, vals); }
+void Flanger_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_FLANGER_RATE:
+        *val = props->Chorus.Rate;
+        break;
+
+    case AL_FLANGER_DEPTH:
+        *val = props->Chorus.Depth;
+        break;
+
+    case AL_FLANGER_FEEDBACK:
+        *val = props->Chorus.Feedback;
+        break;
+
+    case AL_FLANGER_DELAY:
+        *val = props->Chorus.Delay;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid flanger float property 0x%04x", param};
+    }
+}
+void Flanger_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Flanger_getParamf(props, param, vals); }
+
+EffectProps genDefaultFlangerProps() noexcept
+{
+    EffectProps props{};
+    props.Chorus.Waveform = *WaveformFromEnum(AL_FLANGER_DEFAULT_WAVEFORM);
+    props.Chorus.Phase = AL_FLANGER_DEFAULT_PHASE;
+    props.Chorus.Rate = AL_FLANGER_DEFAULT_RATE;
+    props.Chorus.Depth = AL_FLANGER_DEFAULT_DEPTH;
+    props.Chorus.Feedback = AL_FLANGER_DEFAULT_FEEDBACK;
+    props.Chorus.Delay = AL_FLANGER_DEFAULT_DELAY;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Chorus);
+
+const EffectProps ChorusEffectProps{genDefaultChorusProps()};
+
+DEFINE_ALEFFECT_VTABLE(Flanger);
+
+const EffectProps FlangerEffectProps{genDefaultFlangerProps()};
+
+
+#ifdef ALSOFT_EAX
+namespace {
+
+struct EaxChorusTraits {
+    using Props = EAXCHORUSPROPERTIES;
+    using Committer = EaxChorusCommitter;
+    static constexpr auto Field = &EaxEffectProps::mChorus;
+
+    static constexpr auto eax_effect_type() { return EaxEffectType::Chorus; }
+    static constexpr auto efx_effect() { return AL_EFFECT_CHORUS; }
+
+    static constexpr auto eax_none_param_id() { return EAXCHORUS_NONE; }
+    static constexpr auto eax_allparameters_param_id() { return EAXCHORUS_ALLPARAMETERS; }
+    static constexpr auto eax_waveform_param_id() { return EAXCHORUS_WAVEFORM; }
+    static constexpr auto eax_phase_param_id() { return EAXCHORUS_PHASE; }
+    static constexpr auto eax_rate_param_id() { return EAXCHORUS_RATE; }
+    static constexpr auto eax_depth_param_id() { return EAXCHORUS_DEPTH; }
+    static constexpr auto eax_feedback_param_id() { return EAXCHORUS_FEEDBACK; }
+    static constexpr auto eax_delay_param_id() { return EAXCHORUS_DELAY; }
+
+    static constexpr auto eax_min_waveform() { return EAXCHORUS_MINWAVEFORM; }
+    static constexpr auto eax_min_phase() { return EAXCHORUS_MINPHASE; }
+    static constexpr auto eax_min_rate() { return EAXCHORUS_MINRATE; }
+    static constexpr auto eax_min_depth() { return EAXCHORUS_MINDEPTH; }
+    static constexpr auto eax_min_feedback() { return EAXCHORUS_MINFEEDBACK; }
+    static constexpr auto eax_min_delay() { return EAXCHORUS_MINDELAY; }
+
+    static constexpr auto eax_max_waveform() { return EAXCHORUS_MAXWAVEFORM; }
+    static constexpr auto eax_max_phase() { return EAXCHORUS_MAXPHASE; }
+    static constexpr auto eax_max_rate() { return EAXCHORUS_MAXRATE; }
+    static constexpr auto eax_max_depth() { return EAXCHORUS_MAXDEPTH; }
+    static constexpr auto eax_max_feedback() { return EAXCHORUS_MAXFEEDBACK; }
+    static constexpr auto eax_max_delay() { return EAXCHORUS_MAXDELAY; }
+
+    static constexpr auto eax_default_waveform() { return EAXCHORUS_DEFAULTWAVEFORM; }
+    static constexpr auto eax_default_phase() { return EAXCHORUS_DEFAULTPHASE; }
+    static constexpr auto eax_default_rate() { return EAXCHORUS_DEFAULTRATE; }
+    static constexpr auto eax_default_depth() { return EAXCHORUS_DEFAULTDEPTH; }
+    static constexpr auto eax_default_feedback() { return EAXCHORUS_DEFAULTFEEDBACK; }
+    static constexpr auto eax_default_delay() { return EAXCHORUS_DEFAULTDELAY; }
+
+    static constexpr auto efx_min_waveform() { return AL_CHORUS_MIN_WAVEFORM; }
+    static constexpr auto efx_min_phase() { return AL_CHORUS_MIN_PHASE; }
+    static constexpr auto efx_min_rate() { return AL_CHORUS_MIN_RATE; }
+    static constexpr auto efx_min_depth() { return AL_CHORUS_MIN_DEPTH; }
+    static constexpr auto efx_min_feedback() { return AL_CHORUS_MIN_FEEDBACK; }
+    static constexpr auto efx_min_delay() { return AL_CHORUS_MIN_DELAY; }
+
+    static constexpr auto efx_max_waveform() { return AL_CHORUS_MAX_WAVEFORM; }
+    static constexpr auto efx_max_phase() { return AL_CHORUS_MAX_PHASE; }
+    static constexpr auto efx_max_rate() { return AL_CHORUS_MAX_RATE; }
+    static constexpr auto efx_max_depth() { return AL_CHORUS_MAX_DEPTH; }
+    static constexpr auto efx_max_feedback() { return AL_CHORUS_MAX_FEEDBACK; }
+    static constexpr auto efx_max_delay() { return AL_CHORUS_MAX_DELAY; }
+
+    static constexpr auto efx_default_waveform() { return AL_CHORUS_DEFAULT_WAVEFORM; }
+    static constexpr auto efx_default_phase() { return AL_CHORUS_DEFAULT_PHASE; }
+    static constexpr auto efx_default_rate() { return AL_CHORUS_DEFAULT_RATE; }
+    static constexpr auto efx_default_depth() { return AL_CHORUS_DEFAULT_DEPTH; }
+    static constexpr auto efx_default_feedback() { return AL_CHORUS_DEFAULT_FEEDBACK; }
+    static constexpr auto efx_default_delay() { return AL_CHORUS_DEFAULT_DELAY; }
+
+    static ChorusWaveform eax_waveform(unsigned long type)
+    {
+        if(type == EAX_CHORUS_SINUSOID) return ChorusWaveform::Sinusoid;
+        if(type == EAX_CHORUS_TRIANGLE) return ChorusWaveform::Triangle;
+        return ChorusWaveform::Sinusoid;
+    }
+}; // EaxChorusTraits
+
+struct EaxFlangerTraits {
+    using Props = EAXFLANGERPROPERTIES;
+    using Committer = EaxFlangerCommitter;
+    static constexpr auto Field = &EaxEffectProps::mFlanger;
+
+    static constexpr auto eax_effect_type() { return EaxEffectType::Flanger; }
+    static constexpr auto efx_effect() { return AL_EFFECT_FLANGER; }
+
+    static constexpr auto eax_none_param_id() { return EAXFLANGER_NONE; }
+    static constexpr auto eax_allparameters_param_id() { return EAXFLANGER_ALLPARAMETERS; }
+    static constexpr auto eax_waveform_param_id() { return EAXFLANGER_WAVEFORM; }
+    static constexpr auto eax_phase_param_id() { return EAXFLANGER_PHASE; }
+    static constexpr auto eax_rate_param_id() { return EAXFLANGER_RATE; }
+    static constexpr auto eax_depth_param_id() { return EAXFLANGER_DEPTH; }
+    static constexpr auto eax_feedback_param_id() { return EAXFLANGER_FEEDBACK; }
+    static constexpr auto eax_delay_param_id() { return EAXFLANGER_DELAY; }
+
+    static constexpr auto eax_min_waveform() { return EAXFLANGER_MINWAVEFORM; }
+    static constexpr auto eax_min_phase() { return EAXFLANGER_MINPHASE; }
+    static constexpr auto eax_min_rate() { return EAXFLANGER_MINRATE; }
+    static constexpr auto eax_min_depth() { return EAXFLANGER_MINDEPTH; }
+    static constexpr auto eax_min_feedback() { return EAXFLANGER_MINFEEDBACK; }
+    static constexpr auto eax_min_delay() { return EAXFLANGER_MINDELAY; }
+
+    static constexpr auto eax_max_waveform() { return EAXFLANGER_MAXWAVEFORM; }
+    static constexpr auto eax_max_phase() { return EAXFLANGER_MAXPHASE; }
+    static constexpr auto eax_max_rate() { return EAXFLANGER_MAXRATE; }
+    static constexpr auto eax_max_depth() { return EAXFLANGER_MAXDEPTH; }
+    static constexpr auto eax_max_feedback() { return EAXFLANGER_MAXFEEDBACK; }
+    static constexpr auto eax_max_delay() { return EAXFLANGER_MAXDELAY; }
+
+    static constexpr auto eax_default_waveform() { return EAXFLANGER_DEFAULTWAVEFORM; }
+    static constexpr auto eax_default_phase() { return EAXFLANGER_DEFAULTPHASE; }
+    static constexpr auto eax_default_rate() { return EAXFLANGER_DEFAULTRATE; }
+    static constexpr auto eax_default_depth() { return EAXFLANGER_DEFAULTDEPTH; }
+    static constexpr auto eax_default_feedback() { return EAXFLANGER_DEFAULTFEEDBACK; }
+    static constexpr auto eax_default_delay() { return EAXFLANGER_DEFAULTDELAY; }
+
+    static constexpr auto efx_min_waveform() { return AL_FLANGER_MIN_WAVEFORM; }
+    static constexpr auto efx_min_phase() { return AL_FLANGER_MIN_PHASE; }
+    static constexpr auto efx_min_rate() { return AL_FLANGER_MIN_RATE; }
+    static constexpr auto efx_min_depth() { return AL_FLANGER_MIN_DEPTH; }
+    static constexpr auto efx_min_feedback() { return AL_FLANGER_MIN_FEEDBACK; }
+    static constexpr auto efx_min_delay() { return AL_FLANGER_MIN_DELAY; }
+
+    static constexpr auto efx_max_waveform() { return AL_FLANGER_MAX_WAVEFORM; }
+    static constexpr auto efx_max_phase() { return AL_FLANGER_MAX_PHASE; }
+    static constexpr auto efx_max_rate() { return AL_FLANGER_MAX_RATE; }
+    static constexpr auto efx_max_depth() { return AL_FLANGER_MAX_DEPTH; }
+    static constexpr auto efx_max_feedback() { return AL_FLANGER_MAX_FEEDBACK; }
+    static constexpr auto efx_max_delay() { return AL_FLANGER_MAX_DELAY; }
+
+    static constexpr auto efx_default_waveform() { return AL_FLANGER_DEFAULT_WAVEFORM; }
+    static constexpr auto efx_default_phase() { return AL_FLANGER_DEFAULT_PHASE; }
+    static constexpr auto efx_default_rate() { return AL_FLANGER_DEFAULT_RATE; }
+    static constexpr auto efx_default_depth() { return AL_FLANGER_DEFAULT_DEPTH; }
+    static constexpr auto efx_default_feedback() { return AL_FLANGER_DEFAULT_FEEDBACK; }
+    static constexpr auto efx_default_delay() { return AL_FLANGER_DEFAULT_DELAY; }
+
+    static ChorusWaveform eax_waveform(unsigned long type)
+    {
+        if(type == EAX_FLANGER_SINUSOID) return ChorusWaveform::Sinusoid;
+        if(type == EAX_FLANGER_TRIANGLE) return ChorusWaveform::Triangle;
+        return ChorusWaveform::Sinusoid;
+    }
+}; // EaxFlangerTraits
+
+template<typename TTraits>
+struct ChorusFlangerEffect {
+    using Traits = TTraits;
+    using Committer = typename Traits::Committer;
+    using Exception = typename Committer::Exception;
+
+    static constexpr auto Field = Traits::Field;
+
+    struct WaveformValidator {
+        void operator()(unsigned long ulWaveform) const
+        {
+            eax_validate_range<Exception>(
+                "Waveform",
+                ulWaveform,
+                Traits::eax_min_waveform(),
+                Traits::eax_max_waveform());
+        }
+    }; // WaveformValidator
+
+    struct PhaseValidator {
+        void operator()(long lPhase) const
+        {
+            eax_validate_range<Exception>(
+                "Phase",
+                lPhase,
+                Traits::eax_min_phase(),
+                Traits::eax_max_phase());
+        }
+    }; // PhaseValidator
+
+    struct RateValidator {
+        void operator()(float flRate) const
+        {
+            eax_validate_range<Exception>(
+                "Rate",
+                flRate,
+                Traits::eax_min_rate(),
+                Traits::eax_max_rate());
+        }
+    }; // RateValidator
+
+    struct DepthValidator {
+        void operator()(float flDepth) const
+        {
+            eax_validate_range<Exception>(
+                "Depth",
+                flDepth,
+                Traits::eax_min_depth(),
+                Traits::eax_max_depth());
+        }
+    }; // DepthValidator
+
+    struct FeedbackValidator {
+        void operator()(float flFeedback) const
+        {
+            eax_validate_range<Exception>(
+                "Feedback",
+                flFeedback,
+                Traits::eax_min_feedback(),
+                Traits::eax_max_feedback());
+        }
+    }; // FeedbackValidator
+
+    struct DelayValidator {
+        void operator()(float flDelay) const
+        {
+            eax_validate_range<Exception>(
+                "Delay",
+                flDelay,
+                Traits::eax_min_delay(),
+                Traits::eax_max_delay());
+        }
+    }; // DelayValidator
+
+    struct AllValidator {
+        void operator()(const typename Traits::Props& all) const
+        {
+            WaveformValidator{}(all.ulWaveform);
+            PhaseValidator{}(all.lPhase);
+            RateValidator{}(all.flRate);
+            DepthValidator{}(all.flDepth);
+            FeedbackValidator{}(all.flFeedback);
+            DelayValidator{}(all.flDelay);
+        }
+    }; // AllValidator
+
+public:
+    static void SetDefaults(EaxEffectProps &props)
+    {
+        auto&& all = props.*Field;
+        props.mType = Traits::eax_effect_type();
+        all.ulWaveform = Traits::eax_default_waveform();
+        all.lPhase = Traits::eax_default_phase();
+        all.flRate = Traits::eax_default_rate();
+        all.flDepth = Traits::eax_default_depth();
+        all.flFeedback = Traits::eax_default_feedback();
+        all.flDelay = Traits::eax_default_delay();
+    }
+
+
+    static void Get(const EaxCall &call, const EaxEffectProps &props)
+    {
+        auto&& all = props.*Field;
+        switch(call.get_property_id())
+        {
+        case Traits::eax_none_param_id():
+            break;
+
+        case Traits::eax_allparameters_param_id():
+            call.template set_value<Exception>(all);
+            break;
+
+        case Traits::eax_waveform_param_id():
+            call.template set_value<Exception>(all.ulWaveform);
+            break;
+
+        case Traits::eax_phase_param_id():
+            call.template set_value<Exception>(all.lPhase);
+            break;
+
+        case Traits::eax_rate_param_id():
+            call.template set_value<Exception>(all.flRate);
+            break;
+
+        case Traits::eax_depth_param_id():
+            call.template set_value<Exception>(all.flDepth);
+            break;
+
+        case Traits::eax_feedback_param_id():
+            call.template set_value<Exception>(all.flFeedback);
+            break;
+
+        case Traits::eax_delay_param_id():
+            call.template set_value<Exception>(all.flDelay);
+            break;
+
+        default:
+            Committer::fail_unknown_property_id();
+        }
+    }
+
+    static void Set(const EaxCall &call, EaxEffectProps &props)
+    {
+        auto&& all = props.*Field;
+        switch(call.get_property_id())
+        {
+        case Traits::eax_none_param_id():
+            break;
+
+        case Traits::eax_allparameters_param_id():
+            Committer::template defer<AllValidator>(call, all);
+            break;
+
+        case Traits::eax_waveform_param_id():
+            Committer::template defer<WaveformValidator>(call, all.ulWaveform);
+            break;
+
+        case Traits::eax_phase_param_id():
+            Committer::template defer<PhaseValidator>(call, all.lPhase);
+            break;
+
+        case Traits::eax_rate_param_id():
+            Committer::template defer<RateValidator>(call, all.flRate);
+            break;
+
+        case Traits::eax_depth_param_id():
+            Committer::template defer<DepthValidator>(call, all.flDepth);
+            break;
+
+        case Traits::eax_feedback_param_id():
+            Committer::template defer<FeedbackValidator>(call, all.flFeedback);
+            break;
+
+        case Traits::eax_delay_param_id():
+            Committer::template defer<DelayValidator>(call, all.flDelay);
+            break;
+
+        default:
+            Committer::fail_unknown_property_id();
+        }
+    }
+
+    static bool Commit(const EaxEffectProps &props, EaxEffectProps &props_, EffectProps &al_props_)
+    {
+        if(props.mType == props_.mType)
+        {
+            auto&& src = props_.*Field;
+            auto&& dst = props.*Field;
+            if(dst.ulWaveform == src.ulWaveform && dst.lPhase == src.lPhase
+                && dst.flRate == src.flRate && dst.flDepth == src.flDepth
+                && dst.flFeedback == src.flFeedback && dst.flDelay == src.flDelay)
+                return false;
+        }
+
+        props_ = props;
+        auto&& dst = props.*Field;
+
+        al_props_.Chorus.Waveform = Traits::eax_waveform(dst.ulWaveform);
+        al_props_.Chorus.Phase = static_cast<int>(dst.lPhase);
+        al_props_.Chorus.Rate = dst.flRate;
+        al_props_.Chorus.Depth = dst.flDepth;
+        al_props_.Chorus.Feedback = dst.flFeedback;
+        al_props_.Chorus.Delay = dst.flDelay;
+
+        return true;
+    }
+}; // EaxChorusFlangerEffect
+
+
+using ChorusCommitter = EaxCommitter<EaxChorusCommitter>;
+using FlangerCommitter = EaxCommitter<EaxFlangerCommitter>;
+
+} // namespace
+
+template<>
+struct ChorusCommitter::Exception : public EaxException
+{
+    explicit Exception(const char *message) : EaxException{"EAX_CHORUS_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void ChorusCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool ChorusCommitter::commit(const EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxChorusTraits>;
+    return Committer::Commit(props, mEaxProps, mAlProps);
+}
+
+template<>
+void ChorusCommitter::SetDefaults(EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxChorusTraits>;
+    Committer::SetDefaults(props);
+}
+
+template<>
+void ChorusCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxChorusTraits>;
+    Committer::Get(call, props);
+}
+
+template<>
+void ChorusCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxChorusTraits>;
+    Committer::Set(call, props);
+}
+
+template<>
+struct FlangerCommitter::Exception : public EaxException
+{
+    explicit Exception(const char *message) : EaxException{"EAX_FLANGER_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void FlangerCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool FlangerCommitter::commit(const EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxFlangerTraits>;
+    return Committer::Commit(props, mEaxProps, mAlProps);
+}
+
+template<>
+void FlangerCommitter::SetDefaults(EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxFlangerTraits>;
+    Committer::SetDefaults(props);
+}
+
+template<>
+void FlangerCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxFlangerTraits>;
+    Committer::Get(call, props);
+}
+
+template<>
+void FlangerCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    using Committer = ChorusFlangerEffect<EaxFlangerTraits>;
+    Committer::Set(call, props);
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/compressor.cpp b/al/effects/compressor.cpp
new file mode 100644 (file)
index 0000000..a4aa8e7
--- /dev/null
@@ -0,0 +1,162 @@
+
+#include "config.h"
+
+#include "AL/al.h"
+#include "AL/efx.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 Compressor_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_COMPRESSOR_ONOFF:
+        if(!(val >= AL_COMPRESSOR_MIN_ONOFF && val <= AL_COMPRESSOR_MAX_ONOFF))
+            throw effect_exception{AL_INVALID_VALUE, "Compressor state out of range"};
+        props->Compressor.OnOff = (val != AL_FALSE);
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid compressor integer property 0x%04x",
+            param};
+    }
+}
+void Compressor_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ Compressor_setParami(props, param, vals[0]); }
+void Compressor_setParamf(EffectProps*, ALenum param, float)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid compressor float property 0x%04x", param}; }
+void Compressor_setParamfv(EffectProps*, ALenum param, const float*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid compressor float-vector property 0x%04x",
+        param};
+}
+
+void Compressor_getParami(const EffectProps *props, ALenum param, int *val)
+{ 
+    switch(param)
+    {
+    case AL_COMPRESSOR_ONOFF:
+        *val = props->Compressor.OnOff;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid compressor integer property 0x%04x",
+            param};
+    }
+}
+void Compressor_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ Compressor_getParami(props, param, vals); }
+void Compressor_getParamf(const EffectProps*, ALenum param, float*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid compressor float property 0x%04x", param}; }
+void Compressor_getParamfv(const EffectProps*, ALenum param, float*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid compressor float-vector property 0x%04x",
+        param};
+}
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Compressor.OnOff = AL_COMPRESSOR_DEFAULT_ONOFF;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Compressor);
+
+const EffectProps CompressorEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using CompressorCommitter = EaxCommitter<EaxCompressorCommitter>;
+
+struct OnOffValidator {
+    void operator()(unsigned long ulOnOff) const
+    {
+        eax_validate_range<CompressorCommitter::Exception>(
+            "On-Off",
+            ulOnOff,
+            EAXAGCCOMPRESSOR_MINONOFF,
+            EAXAGCCOMPRESSOR_MAXONOFF);
+    }
+}; // OnOffValidator
+
+struct AllValidator {
+    void operator()(const EAXAGCCOMPRESSORPROPERTIES& all) const
+    {
+        OnOffValidator{}(all.ulOnOff);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct CompressorCommitter::Exception : public EaxException
+{
+    explicit Exception(const char *message) : EaxException{"EAX_CHORUS_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void CompressorCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool CompressorCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType
+        && props.mCompressor.ulOnOff == mEaxProps.mCompressor.ulOnOff)
+        return false;
+
+    mEaxProps = props;
+
+    mAlProps.Compressor.OnOff = (props.mCompressor.ulOnOff != 0);
+    return true;
+}
+
+template<>
+void CompressorCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::Compressor;
+    props.mCompressor.ulOnOff = EAXAGCCOMPRESSOR_DEFAULTONOFF;
+}
+
+template<>
+void CompressorCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXAGCCOMPRESSOR_NONE: break;
+    case EAXAGCCOMPRESSOR_ALLPARAMETERS: call.set_value<Exception>(props.mCompressor); break;
+    case EAXAGCCOMPRESSOR_ONOFF: call.set_value<Exception>(props.mCompressor.ulOnOff); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void CompressorCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXAGCCOMPRESSOR_NONE: break;
+    case EAXAGCCOMPRESSOR_ALLPARAMETERS: defer<AllValidator>(call, props.mCompressor); break;
+    case EAXAGCCOMPRESSOR_ONOFF: defer<OnOffValidator>(call, props.mCompressor.ulOnOff); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/convolution.cpp b/al/effects/convolution.cpp
new file mode 100644 (file)
index 0000000..8e850fd
--- /dev/null
@@ -0,0 +1,93 @@
+
+#include "config.h"
+
+#include "AL/al.h"
+#include "alc/inprogext.h"
+
+#include "alc/effects/base.h"
+#include "effects.h"
+
+
+namespace {
+
+void Convolution_setParami(EffectProps* /*props*/, ALenum param, int /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect integer property 0x%04x",
+            param};
+    }
+}
+void Convolution_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{
+    switch(param)
+    {
+    default:
+        Convolution_setParami(props, param, vals[0]);
+    }
+}
+void Convolution_setParamf(EffectProps* /*props*/, ALenum param, float /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect float property 0x%04x",
+            param};
+    }
+}
+void Convolution_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{
+    switch(param)
+    {
+    default:
+        Convolution_setParamf(props, param, vals[0]);
+    }
+}
+
+void Convolution_getParami(const EffectProps* /*props*/, ALenum param, int* /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect integer property 0x%04x",
+            param};
+    }
+}
+void Convolution_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{
+    switch(param)
+    {
+    default:
+        Convolution_getParami(props, param, vals);
+    }
+}
+void Convolution_getParamf(const EffectProps* /*props*/, ALenum param, float* /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect float property 0x%04x",
+            param};
+    }
+}
+void Convolution_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{
+    switch(param)
+    {
+    default:
+        Convolution_getParamf(props, param, vals);
+    }
+}
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Convolution);
+
+const EffectProps ConvolutionEffectProps{genDefaultProps()};
diff --git a/al/effects/dedicated.cpp b/al/effects/dedicated.cpp
new file mode 100644 (file)
index 0000000..db57003
--- /dev/null
@@ -0,0 +1,72 @@
+
+#include "config.h"
+
+#include <cmath>
+
+#include "AL/al.h"
+#include "AL/alext.h"
+
+#include "alc/effects/base.h"
+#include "effects.h"
+
+
+namespace {
+
+void Dedicated_setParami(EffectProps*, ALenum param, int)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid dedicated integer property 0x%04x", param}; }
+void Dedicated_setParamiv(EffectProps*, ALenum param, const int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid dedicated integer-vector property 0x%04x",
+        param};
+}
+void Dedicated_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_DEDICATED_GAIN:
+        if(!(val >= 0.0f && std::isfinite(val)))
+            throw effect_exception{AL_INVALID_VALUE, "Dedicated gain out of range"};
+        props->Dedicated.Gain = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid dedicated float property 0x%04x", param};
+    }
+}
+void Dedicated_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Dedicated_setParamf(props, param, vals[0]); }
+
+void Dedicated_getParami(const EffectProps*, ALenum param, int*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid dedicated integer property 0x%04x", param}; }
+void Dedicated_getParamiv(const EffectProps*, ALenum param, int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid dedicated integer-vector property 0x%04x",
+        param};
+}
+void Dedicated_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_DEDICATED_GAIN:
+        *val = props->Dedicated.Gain;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid dedicated float property 0x%04x", param};
+    }
+}
+void Dedicated_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Dedicated_getParamf(props, param, vals); }
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Dedicated.Gain = 1.0f;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Dedicated);
+
+const EffectProps DedicatedEffectProps{genDefaultProps()};
diff --git a/al/effects/distortion.cpp b/al/effects/distortion.cpp
new file mode 100644 (file)
index 0000000..ee298dd
--- /dev/null
@@ -0,0 +1,271 @@
+
+#include "config.h"
+
+#include "AL/al.h"
+#include "AL/efx.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 Distortion_setParami(EffectProps*, ALenum param, int)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer property 0x%04x", param}; }
+void Distortion_setParamiv(EffectProps*, ALenum param, const int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer-vector property 0x%04x",
+        param};
+}
+void Distortion_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_DISTORTION_EDGE:
+        if(!(val >= AL_DISTORTION_MIN_EDGE && val <= AL_DISTORTION_MAX_EDGE))
+            throw effect_exception{AL_INVALID_VALUE, "Distortion edge out of range"};
+        props->Distortion.Edge = val;
+        break;
+
+    case AL_DISTORTION_GAIN:
+        if(!(val >= AL_DISTORTION_MIN_GAIN && val <= AL_DISTORTION_MAX_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Distortion gain out of range"};
+        props->Distortion.Gain = val;
+        break;
+
+    case AL_DISTORTION_LOWPASS_CUTOFF:
+        if(!(val >= AL_DISTORTION_MIN_LOWPASS_CUTOFF && val <= AL_DISTORTION_MAX_LOWPASS_CUTOFF))
+            throw effect_exception{AL_INVALID_VALUE, "Distortion low-pass cutoff out of range"};
+        props->Distortion.LowpassCutoff = val;
+        break;
+
+    case AL_DISTORTION_EQCENTER:
+        if(!(val >= AL_DISTORTION_MIN_EQCENTER && val <= AL_DISTORTION_MAX_EQCENTER))
+            throw effect_exception{AL_INVALID_VALUE, "Distortion EQ center out of range"};
+        props->Distortion.EQCenter = val;
+        break;
+
+    case AL_DISTORTION_EQBANDWIDTH:
+        if(!(val >= AL_DISTORTION_MIN_EQBANDWIDTH && val <= AL_DISTORTION_MAX_EQBANDWIDTH))
+            throw effect_exception{AL_INVALID_VALUE, "Distortion EQ bandwidth out of range"};
+        props->Distortion.EQBandwidth = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid distortion float property 0x%04x", param};
+    }
+}
+void Distortion_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Distortion_setParamf(props, param, vals[0]); }
+
+void Distortion_getParami(const EffectProps*, ALenum param, int*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer property 0x%04x", param}; }
+void Distortion_getParamiv(const EffectProps*, ALenum param, int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid distortion integer-vector property 0x%04x",
+        param};
+}
+void Distortion_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_DISTORTION_EDGE:
+        *val = props->Distortion.Edge;
+        break;
+
+    case AL_DISTORTION_GAIN:
+        *val = props->Distortion.Gain;
+        break;
+
+    case AL_DISTORTION_LOWPASS_CUTOFF:
+        *val = props->Distortion.LowpassCutoff;
+        break;
+
+    case AL_DISTORTION_EQCENTER:
+        *val = props->Distortion.EQCenter;
+        break;
+
+    case AL_DISTORTION_EQBANDWIDTH:
+        *val = props->Distortion.EQBandwidth;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid distortion float property 0x%04x", param};
+    }
+}
+void Distortion_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Distortion_getParamf(props, param, vals); }
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Distortion.Edge = AL_DISTORTION_DEFAULT_EDGE;
+    props.Distortion.Gain = AL_DISTORTION_DEFAULT_GAIN;
+    props.Distortion.LowpassCutoff = AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF;
+    props.Distortion.EQCenter = AL_DISTORTION_DEFAULT_EQCENTER;
+    props.Distortion.EQBandwidth = AL_DISTORTION_DEFAULT_EQBANDWIDTH;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Distortion);
+
+const EffectProps DistortionEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using DistortionCommitter = EaxCommitter<EaxDistortionCommitter>;
+
+struct EdgeValidator {
+    void operator()(float flEdge) const
+    {
+        eax_validate_range<DistortionCommitter::Exception>(
+            "Edge",
+            flEdge,
+            EAXDISTORTION_MINEDGE,
+            EAXDISTORTION_MAXEDGE);
+    }
+}; // EdgeValidator
+
+struct GainValidator {
+    void operator()(long lGain) const
+    {
+        eax_validate_range<DistortionCommitter::Exception>(
+            "Gain",
+            lGain,
+            EAXDISTORTION_MINGAIN,
+            EAXDISTORTION_MAXGAIN);
+    }
+}; // GainValidator
+
+struct LowPassCutOffValidator {
+    void operator()(float flLowPassCutOff) const
+    {
+        eax_validate_range<DistortionCommitter::Exception>(
+            "Low-pass Cut-off",
+            flLowPassCutOff,
+            EAXDISTORTION_MINLOWPASSCUTOFF,
+            EAXDISTORTION_MAXLOWPASSCUTOFF);
+    }
+}; // LowPassCutOffValidator
+
+struct EqCenterValidator {
+    void operator()(float flEQCenter) const
+    {
+        eax_validate_range<DistortionCommitter::Exception>(
+            "EQ Center",
+            flEQCenter,
+            EAXDISTORTION_MINEQCENTER,
+            EAXDISTORTION_MAXEQCENTER);
+    }
+}; // EqCenterValidator
+
+struct EqBandwidthValidator {
+    void operator()(float flEQBandwidth) const
+    {
+        eax_validate_range<DistortionCommitter::Exception>(
+            "EQ Bandwidth",
+            flEQBandwidth,
+            EAXDISTORTION_MINEQBANDWIDTH,
+            EAXDISTORTION_MAXEQBANDWIDTH);
+    }
+}; // EqBandwidthValidator
+
+struct AllValidator {
+    void operator()(const EAXDISTORTIONPROPERTIES& all) const
+    {
+        EdgeValidator{}(all.flEdge);
+        GainValidator{}(all.lGain);
+        LowPassCutOffValidator{}(all.flLowPassCutOff);
+        EqCenterValidator{}(all.flEQCenter);
+        EqBandwidthValidator{}(all.flEQBandwidth);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct DistortionCommitter::Exception : public EaxException {
+    explicit Exception(const char *message) : EaxException{"EAX_DISTORTION_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void DistortionCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool DistortionCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType && mEaxProps.mDistortion.flEdge == props.mDistortion.flEdge
+        && mEaxProps.mDistortion.lGain == props.mDistortion.lGain
+        && mEaxProps.mDistortion.flLowPassCutOff == props.mDistortion.flLowPassCutOff
+        && mEaxProps.mDistortion.flEQCenter == props.mDistortion.flEQCenter
+        && mEaxProps.mDistortion.flEQBandwidth == props.mDistortion.flEQBandwidth)
+        return false;
+
+    mEaxProps = props;
+
+    mAlProps.Distortion.Edge = props.mDistortion.flEdge;
+    mAlProps.Distortion.Gain = level_mb_to_gain(static_cast<float>(props.mDistortion.lGain));
+    mAlProps.Distortion.LowpassCutoff = props.mDistortion.flLowPassCutOff;
+    mAlProps.Distortion.EQCenter = props.mDistortion.flEQCenter;
+    mAlProps.Distortion.EQBandwidth = props.mDistortion.flEdge;
+
+    return true;
+}
+
+template<>
+void DistortionCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::Distortion;
+    props.mDistortion.flEdge = EAXDISTORTION_DEFAULTEDGE;
+    props.mDistortion.lGain = EAXDISTORTION_DEFAULTGAIN;
+    props.mDistortion.flLowPassCutOff = EAXDISTORTION_DEFAULTLOWPASSCUTOFF;
+    props.mDistortion.flEQCenter = EAXDISTORTION_DEFAULTEQCENTER;
+    props.mDistortion.flEQBandwidth = EAXDISTORTION_DEFAULTEQBANDWIDTH;
+}
+
+template<>
+void DistortionCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXDISTORTION_NONE: break;
+    case EAXDISTORTION_ALLPARAMETERS: call.set_value<Exception>(props.mDistortion); break;
+    case EAXDISTORTION_EDGE: call.set_value<Exception>(props.mDistortion.flEdge); break;
+    case EAXDISTORTION_GAIN: call.set_value<Exception>(props.mDistortion.lGain); break;
+    case EAXDISTORTION_LOWPASSCUTOFF: call.set_value<Exception>(props.mDistortion.flLowPassCutOff); break;
+    case EAXDISTORTION_EQCENTER: call.set_value<Exception>(props.mDistortion.flEQCenter); break;
+    case EAXDISTORTION_EQBANDWIDTH: call.set_value<Exception>(props.mDistortion.flEQBandwidth); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void DistortionCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXDISTORTION_NONE: break;
+    case EAXDISTORTION_ALLPARAMETERS: defer<AllValidator>(call, props.mDistortion); break;
+    case EAXDISTORTION_EDGE: defer<EdgeValidator>(call, props.mDistortion.flEdge); break;
+    case EAXDISTORTION_GAIN: defer<GainValidator>(call, props.mDistortion.lGain); break;
+    case EAXDISTORTION_LOWPASSCUTOFF: defer<LowPassCutOffValidator>(call, props.mDistortion.flLowPassCutOff); break;
+    case EAXDISTORTION_EQCENTER: defer<EqCenterValidator>(call, props.mDistortion.flEQCenter); break;
+    case EAXDISTORTION_EQBANDWIDTH: defer<EqBandwidthValidator>(call, props.mDistortion.flEQBandwidth); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/echo.cpp b/al/effects/echo.cpp
new file mode 100644 (file)
index 0000000..2eb3760
--- /dev/null
@@ -0,0 +1,268 @@
+
+#include "config.h"
+
+#include "AL/al.h"
+#include "AL/efx.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 {
+
+static_assert(EchoMaxDelay >= AL_ECHO_MAX_DELAY, "Echo max delay too short");
+static_assert(EchoMaxLRDelay >= AL_ECHO_MAX_LRDELAY, "Echo max left-right delay too short");
+
+void Echo_setParami(EffectProps*, ALenum param, int)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid echo integer property 0x%04x", param}; }
+void Echo_setParamiv(EffectProps*, ALenum param, const int*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid echo integer-vector property 0x%04x", param}; }
+void Echo_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_ECHO_DELAY:
+        if(!(val >= AL_ECHO_MIN_DELAY && val <= AL_ECHO_MAX_DELAY))
+            throw effect_exception{AL_INVALID_VALUE, "Echo delay out of range"};
+        props->Echo.Delay = val;
+        break;
+
+    case AL_ECHO_LRDELAY:
+        if(!(val >= AL_ECHO_MIN_LRDELAY && val <= AL_ECHO_MAX_LRDELAY))
+            throw effect_exception{AL_INVALID_VALUE, "Echo LR delay out of range"};
+        props->Echo.LRDelay = val;
+        break;
+
+    case AL_ECHO_DAMPING:
+        if(!(val >= AL_ECHO_MIN_DAMPING && val <= AL_ECHO_MAX_DAMPING))
+            throw effect_exception{AL_INVALID_VALUE, "Echo damping out of range"};
+        props->Echo.Damping = val;
+        break;
+
+    case AL_ECHO_FEEDBACK:
+        if(!(val >= AL_ECHO_MIN_FEEDBACK && val <= AL_ECHO_MAX_FEEDBACK))
+            throw effect_exception{AL_INVALID_VALUE, "Echo feedback out of range"};
+        props->Echo.Feedback = val;
+        break;
+
+    case AL_ECHO_SPREAD:
+        if(!(val >= AL_ECHO_MIN_SPREAD && val <= AL_ECHO_MAX_SPREAD))
+            throw effect_exception{AL_INVALID_VALUE, "Echo spread out of range"};
+        props->Echo.Spread = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid echo float property 0x%04x", param};
+    }
+}
+void Echo_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Echo_setParamf(props, param, vals[0]); }
+
+void Echo_getParami(const EffectProps*, ALenum param, int*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid echo integer property 0x%04x", param}; }
+void Echo_getParamiv(const EffectProps*, ALenum param, int*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid echo integer-vector property 0x%04x", param}; }
+void Echo_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_ECHO_DELAY:
+        *val = props->Echo.Delay;
+        break;
+
+    case AL_ECHO_LRDELAY:
+        *val = props->Echo.LRDelay;
+        break;
+
+    case AL_ECHO_DAMPING:
+        *val = props->Echo.Damping;
+        break;
+
+    case AL_ECHO_FEEDBACK:
+        *val = props->Echo.Feedback;
+        break;
+
+    case AL_ECHO_SPREAD:
+        *val = props->Echo.Spread;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid echo float property 0x%04x", param};
+    }
+}
+void Echo_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Echo_getParamf(props, param, vals); }
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Echo.Delay    = AL_ECHO_DEFAULT_DELAY;
+    props.Echo.LRDelay  = AL_ECHO_DEFAULT_LRDELAY;
+    props.Echo.Damping  = AL_ECHO_DEFAULT_DAMPING;
+    props.Echo.Feedback = AL_ECHO_DEFAULT_FEEDBACK;
+    props.Echo.Spread   = AL_ECHO_DEFAULT_SPREAD;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Echo);
+
+const EffectProps EchoEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EchoCommitter = EaxCommitter<EaxEchoCommitter>;
+
+struct DelayValidator {
+    void operator()(float flDelay) const
+    {
+        eax_validate_range<EchoCommitter::Exception>(
+            "Delay",
+            flDelay,
+            EAXECHO_MINDELAY,
+            EAXECHO_MAXDELAY);
+    }
+}; // DelayValidator
+
+struct LrDelayValidator {
+    void operator()(float flLRDelay) const
+    {
+        eax_validate_range<EchoCommitter::Exception>(
+            "LR Delay",
+            flLRDelay,
+            EAXECHO_MINLRDELAY,
+            EAXECHO_MAXLRDELAY);
+    }
+}; // LrDelayValidator
+
+struct DampingValidator {
+    void operator()(float flDamping) const
+    {
+        eax_validate_range<EchoCommitter::Exception>(
+            "Damping",
+            flDamping,
+            EAXECHO_MINDAMPING,
+            EAXECHO_MAXDAMPING);
+    }
+}; // DampingValidator
+
+struct FeedbackValidator {
+    void operator()(float flFeedback) const
+    {
+        eax_validate_range<EchoCommitter::Exception>(
+            "Feedback",
+            flFeedback,
+            EAXECHO_MINFEEDBACK,
+            EAXECHO_MAXFEEDBACK);
+    }
+}; // FeedbackValidator
+
+struct SpreadValidator {
+    void operator()(float flSpread) const
+    {
+        eax_validate_range<EchoCommitter::Exception>(
+            "Spread",
+            flSpread,
+            EAXECHO_MINSPREAD,
+            EAXECHO_MAXSPREAD);
+    }
+}; // SpreadValidator
+
+struct AllValidator {
+    void operator()(const EAXECHOPROPERTIES& all) const
+    {
+        DelayValidator{}(all.flDelay);
+        LrDelayValidator{}(all.flLRDelay);
+        DampingValidator{}(all.flDamping);
+        FeedbackValidator{}(all.flFeedback);
+        SpreadValidator{}(all.flSpread);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct EchoCommitter::Exception : public EaxException {
+    explicit Exception(const char* message) : EaxException{"EAX_ECHO_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void EchoCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool EchoCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType && mEaxProps.mEcho.flDelay == props.mEcho.flDelay
+        && mEaxProps.mEcho.flLRDelay == props.mEcho.flLRDelay
+        && mEaxProps.mEcho.flDamping == props.mEcho.flDamping
+        && mEaxProps.mEcho.flFeedback == props.mEcho.flFeedback
+        && mEaxProps.mEcho.flSpread == props.mEcho.flSpread)
+        return false;
+
+    mEaxProps = props;
+
+    mAlProps.Echo.Delay = props.mEcho.flDelay;
+    mAlProps.Echo.LRDelay = props.mEcho.flLRDelay;
+    mAlProps.Echo.Damping = props.mEcho.flDamping;
+    mAlProps.Echo.Feedback = props.mEcho.flFeedback;
+    mAlProps.Echo.Spread = props.mEcho.flSpread;
+
+    return true;
+}
+
+template<>
+void EchoCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::Echo;
+    props.mEcho.flDelay = EAXECHO_DEFAULTDELAY;
+    props.mEcho.flLRDelay = EAXECHO_DEFAULTLRDELAY;
+    props.mEcho.flDamping = EAXECHO_DEFAULTDAMPING;
+    props.mEcho.flFeedback = EAXECHO_DEFAULTFEEDBACK;
+    props.mEcho.flSpread = EAXECHO_DEFAULTSPREAD;
+}
+
+template<>
+void EchoCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXECHO_NONE: break;
+    case EAXECHO_ALLPARAMETERS: call.set_value<Exception>(props.mEcho); break;
+    case EAXECHO_DELAY: call.set_value<Exception>(props.mEcho.flDelay); break;
+    case EAXECHO_LRDELAY: call.set_value<Exception>(props.mEcho.flLRDelay); break;
+    case EAXECHO_DAMPING: call.set_value<Exception>(props.mEcho.flDamping); break;
+    case EAXECHO_FEEDBACK: call.set_value<Exception>(props.mEcho.flFeedback); break;
+    case EAXECHO_SPREAD: call.set_value<Exception>(props.mEcho.flSpread); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void EchoCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXECHO_NONE: break;
+    case EAXECHO_ALLPARAMETERS: defer<AllValidator>(call, props.mEcho); break;
+    case EAXECHO_DELAY: defer<DelayValidator>(call, props.mEcho.flDelay); break;
+    case EAXECHO_LRDELAY: defer<LrDelayValidator>(call, props.mEcho.flLRDelay); break;
+    case EAXECHO_DAMPING: defer<DampingValidator>(call, props.mEcho.flDamping); break;
+    case EAXECHO_FEEDBACK: defer<FeedbackValidator>(call, props.mEcho.flFeedback); break;
+    case EAXECHO_SPREAD: defer<SpreadValidator>(call, props.mEcho.flSpread); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/effects.cpp b/al/effects/effects.cpp
new file mode 100644 (file)
index 0000000..4a67b5f
--- /dev/null
@@ -0,0 +1,9 @@
+#include "config.h"
+
+#ifdef ALSOFT_EAX
+
+#include <cassert>
+#include "AL/efx.h"
+#include "effects.h"
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/effects.h b/al/effects/effects.h
new file mode 100644 (file)
index 0000000..9d57dd8
--- /dev/null
@@ -0,0 +1,88 @@
+#ifndef AL_EFFECTS_EFFECTS_H
+#define AL_EFFECTS_EFFECTS_H
+
+#include "AL/al.h"
+
+#include "core/except.h"
+
+#ifdef ALSOFT_EAX
+#include "al/eax/effect.h"
+#endif // ALSOFT_EAX
+
+union EffectProps;
+
+
+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, ...);
+    ~effect_exception() override;
+
+    ALenum errorCode() const noexcept { return mErrorCode; }
+};
+
+
+struct EffectVtable {
+    void (*const setParami)(EffectProps *props, ALenum param, int val);
+    void (*const setParamiv)(EffectProps *props, ALenum param, const int *vals);
+    void (*const setParamf)(EffectProps *props, ALenum param, float val);
+    void (*const setParamfv)(EffectProps *props, ALenum param, const float *vals);
+
+    void (*const getParami)(const EffectProps *props, ALenum param, int *val);
+    void (*const getParamiv)(const EffectProps *props, ALenum param, int *vals);
+    void (*const getParamf)(const EffectProps *props, ALenum param, float *val);
+    void (*const getParamfv)(const EffectProps *props, ALenum param, float *vals);
+};
+
+#define DEFINE_ALEFFECT_VTABLE(T)           \
+const EffectVtable T##EffectVtable = {      \
+    T##_setParami, T##_setParamiv,          \
+    T##_setParamf, T##_setParamfv,          \
+    T##_getParami, T##_getParamiv,          \
+    T##_getParamf, T##_getParamfv,          \
+}
+
+
+/* Default properties for the given effect types. */
+extern const EffectProps NullEffectProps;
+extern const EffectProps ReverbEffectProps;
+extern const EffectProps StdReverbEffectProps;
+extern const EffectProps AutowahEffectProps;
+extern const EffectProps ChorusEffectProps;
+extern const EffectProps CompressorEffectProps;
+extern const EffectProps DistortionEffectProps;
+extern const EffectProps EchoEffectProps;
+extern const EffectProps EqualizerEffectProps;
+extern const EffectProps FlangerEffectProps;
+extern const EffectProps FshifterEffectProps;
+extern const EffectProps ModulatorEffectProps;
+extern const EffectProps PshifterEffectProps;
+extern const EffectProps VmorpherEffectProps;
+extern const EffectProps DedicatedEffectProps;
+extern const EffectProps ConvolutionEffectProps;
+
+/* Vtables to get/set properties for the given effect types. */
+extern const EffectVtable NullEffectVtable;
+extern const EffectVtable ReverbEffectVtable;
+extern const EffectVtable StdReverbEffectVtable;
+extern const EffectVtable AutowahEffectVtable;
+extern const EffectVtable ChorusEffectVtable;
+extern const EffectVtable CompressorEffectVtable;
+extern const EffectVtable DistortionEffectVtable;
+extern const EffectVtable EchoEffectVtable;
+extern const EffectVtable EqualizerEffectVtable;
+extern const EffectVtable FlangerEffectVtable;
+extern const EffectVtable FshifterEffectVtable;
+extern const EffectVtable ModulatorEffectVtable;
+extern const EffectVtable PshifterEffectVtable;
+extern const EffectVtable VmorpherEffectVtable;
+extern const EffectVtable DedicatedEffectVtable;
+extern const EffectVtable ConvolutionEffectVtable;
+
+#endif /* AL_EFFECTS_EFFECTS_H */
diff --git a/al/effects/equalizer.cpp b/al/effects/equalizer.cpp
new file mode 100644 (file)
index 0000000..7dc703d
--- /dev/null
@@ -0,0 +1,411 @@
+
+#include "config.h"
+
+#include "AL/al.h"
+#include "AL/efx.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 Equalizer_setParami(EffectProps*, ALenum param, int)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid equalizer integer property 0x%04x", param}; }
+void Equalizer_setParamiv(EffectProps*, ALenum param, const int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid equalizer integer-vector property 0x%04x",
+        param};
+}
+void Equalizer_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_EQUALIZER_LOW_GAIN:
+        if(!(val >= AL_EQUALIZER_MIN_LOW_GAIN && val <= AL_EQUALIZER_MAX_LOW_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer low-band gain out of range"};
+        props->Equalizer.LowGain = val;
+        break;
+
+    case AL_EQUALIZER_LOW_CUTOFF:
+        if(!(val >= AL_EQUALIZER_MIN_LOW_CUTOFF && val <= AL_EQUALIZER_MAX_LOW_CUTOFF))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer low-band cutoff out of range"};
+        props->Equalizer.LowCutoff = val;
+        break;
+
+    case AL_EQUALIZER_MID1_GAIN:
+        if(!(val >= AL_EQUALIZER_MIN_MID1_GAIN && val <= AL_EQUALIZER_MAX_MID1_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer mid1-band gain out of range"};
+        props->Equalizer.Mid1Gain = val;
+        break;
+
+    case AL_EQUALIZER_MID1_CENTER:
+        if(!(val >= AL_EQUALIZER_MIN_MID1_CENTER && val <= AL_EQUALIZER_MAX_MID1_CENTER))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer mid1-band center out of range"};
+        props->Equalizer.Mid1Center = val;
+        break;
+
+    case AL_EQUALIZER_MID1_WIDTH:
+        if(!(val >= AL_EQUALIZER_MIN_MID1_WIDTH && val <= AL_EQUALIZER_MAX_MID1_WIDTH))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer mid1-band width out of range"};
+        props->Equalizer.Mid1Width = val;
+        break;
+
+    case AL_EQUALIZER_MID2_GAIN:
+        if(!(val >= AL_EQUALIZER_MIN_MID2_GAIN && val <= AL_EQUALIZER_MAX_MID2_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer mid2-band gain out of range"};
+        props->Equalizer.Mid2Gain = val;
+        break;
+
+    case AL_EQUALIZER_MID2_CENTER:
+        if(!(val >= AL_EQUALIZER_MIN_MID2_CENTER && val <= AL_EQUALIZER_MAX_MID2_CENTER))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer mid2-band center out of range"};
+        props->Equalizer.Mid2Center = val;
+        break;
+
+    case AL_EQUALIZER_MID2_WIDTH:
+        if(!(val >= AL_EQUALIZER_MIN_MID2_WIDTH && val <= AL_EQUALIZER_MAX_MID2_WIDTH))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer mid2-band width out of range"};
+        props->Equalizer.Mid2Width = val;
+        break;
+
+    case AL_EQUALIZER_HIGH_GAIN:
+        if(!(val >= AL_EQUALIZER_MIN_HIGH_GAIN && val <= AL_EQUALIZER_MAX_HIGH_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer high-band gain out of range"};
+        props->Equalizer.HighGain = val;
+        break;
+
+    case AL_EQUALIZER_HIGH_CUTOFF:
+        if(!(val >= AL_EQUALIZER_MIN_HIGH_CUTOFF && val <= AL_EQUALIZER_MAX_HIGH_CUTOFF))
+            throw effect_exception{AL_INVALID_VALUE, "Equalizer high-band cutoff out of range"};
+        props->Equalizer.HighCutoff = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid equalizer float property 0x%04x", param};
+    }
+}
+void Equalizer_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Equalizer_setParamf(props, param, vals[0]); }
+
+void Equalizer_getParami(const EffectProps*, ALenum param, int*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid equalizer integer property 0x%04x", param}; }
+void Equalizer_getParamiv(const EffectProps*, ALenum param, int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid equalizer integer-vector property 0x%04x",
+        param};
+}
+void Equalizer_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_EQUALIZER_LOW_GAIN:
+        *val = props->Equalizer.LowGain;
+        break;
+
+    case AL_EQUALIZER_LOW_CUTOFF:
+        *val = props->Equalizer.LowCutoff;
+        break;
+
+    case AL_EQUALIZER_MID1_GAIN:
+        *val = props->Equalizer.Mid1Gain;
+        break;
+
+    case AL_EQUALIZER_MID1_CENTER:
+        *val = props->Equalizer.Mid1Center;
+        break;
+
+    case AL_EQUALIZER_MID1_WIDTH:
+        *val = props->Equalizer.Mid1Width;
+        break;
+
+    case AL_EQUALIZER_MID2_GAIN:
+        *val = props->Equalizer.Mid2Gain;
+        break;
+
+    case AL_EQUALIZER_MID2_CENTER:
+        *val = props->Equalizer.Mid2Center;
+        break;
+
+    case AL_EQUALIZER_MID2_WIDTH:
+        *val = props->Equalizer.Mid2Width;
+        break;
+
+    case AL_EQUALIZER_HIGH_GAIN:
+        *val = props->Equalizer.HighGain;
+        break;
+
+    case AL_EQUALIZER_HIGH_CUTOFF:
+        *val = props->Equalizer.HighCutoff;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid equalizer float property 0x%04x", param};
+    }
+}
+void Equalizer_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Equalizer_getParamf(props, param, vals); }
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Equalizer.LowCutoff = AL_EQUALIZER_DEFAULT_LOW_CUTOFF;
+    props.Equalizer.LowGain = AL_EQUALIZER_DEFAULT_LOW_GAIN;
+    props.Equalizer.Mid1Center = AL_EQUALIZER_DEFAULT_MID1_CENTER;
+    props.Equalizer.Mid1Gain = AL_EQUALIZER_DEFAULT_MID1_GAIN;
+    props.Equalizer.Mid1Width = AL_EQUALIZER_DEFAULT_MID1_WIDTH;
+    props.Equalizer.Mid2Center = AL_EQUALIZER_DEFAULT_MID2_CENTER;
+    props.Equalizer.Mid2Gain = AL_EQUALIZER_DEFAULT_MID2_GAIN;
+    props.Equalizer.Mid2Width = AL_EQUALIZER_DEFAULT_MID2_WIDTH;
+    props.Equalizer.HighCutoff = AL_EQUALIZER_DEFAULT_HIGH_CUTOFF;
+    props.Equalizer.HighGain = AL_EQUALIZER_DEFAULT_HIGH_GAIN;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Equalizer);
+
+const EffectProps EqualizerEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EqualizerCommitter = EaxCommitter<EaxEqualizerCommitter>;
+
+struct LowGainValidator {
+    void operator()(long lLowGain) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Low Gain",
+            lLowGain,
+            EAXEQUALIZER_MINLOWGAIN,
+            EAXEQUALIZER_MAXLOWGAIN);
+    }
+}; // LowGainValidator
+
+struct LowCutOffValidator {
+    void operator()(float flLowCutOff) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Low Cutoff",
+            flLowCutOff,
+            EAXEQUALIZER_MINLOWCUTOFF,
+            EAXEQUALIZER_MAXLOWCUTOFF);
+    }
+}; // LowCutOffValidator
+
+struct Mid1GainValidator {
+    void operator()(long lMid1Gain) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Mid1 Gain",
+            lMid1Gain,
+            EAXEQUALIZER_MINMID1GAIN,
+            EAXEQUALIZER_MAXMID1GAIN);
+    }
+}; // Mid1GainValidator
+
+struct Mid1CenterValidator {
+    void operator()(float flMid1Center) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Mid1 Center",
+            flMid1Center,
+            EAXEQUALIZER_MINMID1CENTER,
+            EAXEQUALIZER_MAXMID1CENTER);
+    }
+}; // Mid1CenterValidator
+
+struct Mid1WidthValidator {
+    void operator()(float flMid1Width) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Mid1 Width",
+            flMid1Width,
+            EAXEQUALIZER_MINMID1WIDTH,
+            EAXEQUALIZER_MAXMID1WIDTH);
+    }
+}; // Mid1WidthValidator
+
+struct Mid2GainValidator {
+    void operator()(long lMid2Gain) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Mid2 Gain",
+            lMid2Gain,
+            EAXEQUALIZER_MINMID2GAIN,
+            EAXEQUALIZER_MAXMID2GAIN);
+    }
+}; // Mid2GainValidator
+
+struct Mid2CenterValidator {
+    void operator()(float flMid2Center) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Mid2 Center",
+            flMid2Center,
+            EAXEQUALIZER_MINMID2CENTER,
+            EAXEQUALIZER_MAXMID2CENTER);
+    }
+}; // Mid2CenterValidator
+
+struct Mid2WidthValidator {
+    void operator()(float flMid2Width) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "Mid2 Width",
+            flMid2Width,
+            EAXEQUALIZER_MINMID2WIDTH,
+            EAXEQUALIZER_MAXMID2WIDTH);
+    }
+}; // Mid2WidthValidator
+
+struct HighGainValidator {
+    void operator()(long lHighGain) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "High Gain",
+            lHighGain,
+            EAXEQUALIZER_MINHIGHGAIN,
+            EAXEQUALIZER_MAXHIGHGAIN);
+    }
+}; // HighGainValidator
+
+struct HighCutOffValidator {
+    void operator()(float flHighCutOff) const
+    {
+        eax_validate_range<EqualizerCommitter::Exception>(
+            "High Cutoff",
+            flHighCutOff,
+            EAXEQUALIZER_MINHIGHCUTOFF,
+            EAXEQUALIZER_MAXHIGHCUTOFF);
+    }
+}; // HighCutOffValidator
+
+struct AllValidator {
+    void operator()(const EAXEQUALIZERPROPERTIES& all) const
+    {
+        LowGainValidator{}(all.lLowGain);
+        LowCutOffValidator{}(all.flLowCutOff);
+        Mid1GainValidator{}(all.lMid1Gain);
+        Mid1CenterValidator{}(all.flMid1Center);
+        Mid1WidthValidator{}(all.flMid1Width);
+        Mid2GainValidator{}(all.lMid2Gain);
+        Mid2CenterValidator{}(all.flMid2Center);
+        Mid2WidthValidator{}(all.flMid2Width);
+        HighGainValidator{}(all.lHighGain);
+        HighCutOffValidator{}(all.flHighCutOff);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct EqualizerCommitter::Exception : public EaxException {
+    explicit Exception(const char* message) : EaxException{"EAX_EQUALIZER_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void EqualizerCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool EqualizerCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType && mEaxProps.mEqualizer.lLowGain == props.mEqualizer.lLowGain
+        && mEaxProps.mEqualizer.flLowCutOff == props.mEqualizer.flLowCutOff
+        && mEaxProps.mEqualizer.lMid1Gain == props.mEqualizer.lMid1Gain
+        && mEaxProps.mEqualizer.flMid1Center == props.mEqualizer.flMid1Center
+        && mEaxProps.mEqualizer.flMid1Width == props.mEqualizer.flMid1Width
+        && mEaxProps.mEqualizer.lMid2Gain == props.mEqualizer.lMid2Gain
+        && mEaxProps.mEqualizer.flMid2Center == props.mEqualizer.flMid2Center
+        && mEaxProps.mEqualizer.flMid2Width == props.mEqualizer.flMid2Width
+        && mEaxProps.mEqualizer.lHighGain == props.mEqualizer.lHighGain
+        && mEaxProps.mEqualizer.flHighCutOff == props.mEqualizer.flHighCutOff)
+        return false;
+
+    mEaxProps = props;
+
+    mAlProps.Equalizer.LowGain = level_mb_to_gain(static_cast<float>(props.mEqualizer.lLowGain));
+    mAlProps.Equalizer.LowCutoff = props.mEqualizer.flLowCutOff;
+    mAlProps.Equalizer.Mid1Gain = level_mb_to_gain(static_cast<float>(props.mEqualizer.lMid1Gain));
+    mAlProps.Equalizer.Mid1Center = props.mEqualizer.flMid1Center;
+    mAlProps.Equalizer.Mid1Width = props.mEqualizer.flMid1Width;
+    mAlProps.Equalizer.Mid2Gain = level_mb_to_gain(static_cast<float>(props.mEqualizer.lMid2Gain));
+    mAlProps.Equalizer.Mid2Center = props.mEqualizer.flMid2Center;
+    mAlProps.Equalizer.Mid2Width = props.mEqualizer.flMid2Width;
+    mAlProps.Equalizer.HighGain = level_mb_to_gain(static_cast<float>(props.mEqualizer.lHighGain));
+    mAlProps.Equalizer.HighCutoff = props.mEqualizer.flHighCutOff;
+
+    return true;
+}
+
+template<>
+void EqualizerCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::Equalizer;
+    props.mEqualizer.lLowGain = EAXEQUALIZER_DEFAULTLOWGAIN;
+    props.mEqualizer.flLowCutOff = EAXEQUALIZER_DEFAULTLOWCUTOFF;
+    props.mEqualizer.lMid1Gain = EAXEQUALIZER_DEFAULTMID1GAIN;
+    props.mEqualizer.flMid1Center = EAXEQUALIZER_DEFAULTMID1CENTER;
+    props.mEqualizer.flMid1Width = EAXEQUALIZER_DEFAULTMID1WIDTH;
+    props.mEqualizer.lMid2Gain = EAXEQUALIZER_DEFAULTMID2GAIN;
+    props.mEqualizer.flMid2Center = EAXEQUALIZER_DEFAULTMID2CENTER;
+    props.mEqualizer.flMid2Width = EAXEQUALIZER_DEFAULTMID2WIDTH;
+    props.mEqualizer.lHighGain = EAXEQUALIZER_DEFAULTHIGHGAIN;
+    props.mEqualizer.flHighCutOff = EAXEQUALIZER_DEFAULTHIGHCUTOFF;
+}
+
+template<>
+void EqualizerCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXEQUALIZER_NONE: break;
+    case EAXEQUALIZER_ALLPARAMETERS: call.set_value<Exception>(props.mEqualizer); break;
+    case EAXEQUALIZER_LOWGAIN: call.set_value<Exception>(props.mEqualizer.lLowGain); break;
+    case EAXEQUALIZER_LOWCUTOFF: call.set_value<Exception>(props.mEqualizer.flLowCutOff); break;
+    case EAXEQUALIZER_MID1GAIN: call.set_value<Exception>(props.mEqualizer.lMid1Gain); break;
+    case EAXEQUALIZER_MID1CENTER: call.set_value<Exception>(props.mEqualizer.flMid1Center); break;
+    case EAXEQUALIZER_MID1WIDTH: call.set_value<Exception>(props.mEqualizer.flMid1Width); break;
+    case EAXEQUALIZER_MID2GAIN: call.set_value<Exception>(props.mEqualizer.lMid2Gain); break;
+    case EAXEQUALIZER_MID2CENTER: call.set_value<Exception>(props.mEqualizer.flMid2Center); break;
+    case EAXEQUALIZER_MID2WIDTH: call.set_value<Exception>(props.mEqualizer.flMid2Width); break;
+    case EAXEQUALIZER_HIGHGAIN: call.set_value<Exception>(props.mEqualizer.lHighGain); break;
+    case EAXEQUALIZER_HIGHCUTOFF: call.set_value<Exception>(props.mEqualizer.flHighCutOff); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void EqualizerCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXEQUALIZER_NONE: break;
+    case EAXEQUALIZER_ALLPARAMETERS: defer<AllValidator>(call, props.mEqualizer); break;
+    case EAXEQUALIZER_LOWGAIN: defer<LowGainValidator>(call, props.mEqualizer.lLowGain); break;
+    case EAXEQUALIZER_LOWCUTOFF: defer<LowCutOffValidator>(call, props.mEqualizer.flLowCutOff); break;
+    case EAXEQUALIZER_MID1GAIN: defer<Mid1GainValidator>(call, props.mEqualizer.lMid1Gain); break;
+    case EAXEQUALIZER_MID1CENTER: defer<Mid1CenterValidator>(call, props.mEqualizer.flMid1Center); break;
+    case EAXEQUALIZER_MID1WIDTH: defer<Mid1WidthValidator>(call, props.mEqualizer.flMid1Width); break;
+    case EAXEQUALIZER_MID2GAIN: defer<Mid2GainValidator>(call, props.mEqualizer.lMid2Gain); break;
+    case EAXEQUALIZER_MID2CENTER: defer<Mid2CenterValidator>(call, props.mEqualizer.flMid2Center); break;
+    case EAXEQUALIZER_MID2WIDTH: defer<Mid2WidthValidator>(call, props.mEqualizer.flMid2Width); break;
+    case EAXEQUALIZER_HIGHGAIN: defer<HighGainValidator>(call, props.mEqualizer.lHighGain); break;
+    case EAXEQUALIZER_HIGHCUTOFF: defer<HighCutOffValidator>(call, props.mEqualizer.flHighCutOff); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/fshifter.cpp b/al/effects/fshifter.cpp
new file mode 100644 (file)
index 0000000..949db20
--- /dev/null
@@ -0,0 +1,264 @@
+
+#include "config.h"
+
+#include <stdexcept>
+
+#include "AL/al.h"
+#include "AL/efx.h"
+
+#include "alc/effects/base.h"
+#include "aloptional.h"
+#include "effects.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+#include "alnumeric.h"
+#include "al/eax/exception.h"
+#include "al/eax/utils.h"
+#endif // ALSOFT_EAX
+
+
+namespace {
+
+al::optional<FShifterDirection> DirectionFromEmum(ALenum value)
+{
+    switch(value)
+    {
+    case AL_FREQUENCY_SHIFTER_DIRECTION_DOWN: return FShifterDirection::Down;
+    case AL_FREQUENCY_SHIFTER_DIRECTION_UP: return FShifterDirection::Up;
+    case AL_FREQUENCY_SHIFTER_DIRECTION_OFF: return FShifterDirection::Off;
+    }
+    return al::nullopt;
+}
+ALenum EnumFromDirection(FShifterDirection dir)
+{
+    switch(dir)
+    {
+    case FShifterDirection::Down: return AL_FREQUENCY_SHIFTER_DIRECTION_DOWN;
+    case FShifterDirection::Up: return AL_FREQUENCY_SHIFTER_DIRECTION_UP;
+    case FShifterDirection::Off: return AL_FREQUENCY_SHIFTER_DIRECTION_OFF;
+    }
+    throw std::runtime_error{"Invalid direction: "+std::to_string(static_cast<int>(dir))};
+}
+
+void Fshifter_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_FREQUENCY_SHIFTER_FREQUENCY:
+        if(!(val >= AL_FREQUENCY_SHIFTER_MIN_FREQUENCY && val <= AL_FREQUENCY_SHIFTER_MAX_FREQUENCY))
+            throw effect_exception{AL_INVALID_VALUE, "Frequency shifter frequency out of range"};
+        props->Fshifter.Frequency = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid frequency shifter float property 0x%04x",
+            param};
+    }
+}
+void Fshifter_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Fshifter_setParamf(props, param, vals[0]); }
+
+void Fshifter_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_FREQUENCY_SHIFTER_LEFT_DIRECTION:
+        if(auto diropt = DirectionFromEmum(val))
+            props->Fshifter.LeftDirection = *diropt;
+        else
+            throw effect_exception{AL_INVALID_VALUE,
+                "Unsupported frequency shifter left direction: 0x%04x", val};
+        break;
+
+    case AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION:
+        if(auto diropt = DirectionFromEmum(val))
+            props->Fshifter.RightDirection = *diropt;
+        else
+            throw effect_exception{AL_INVALID_VALUE,
+                "Unsupported frequency shifter right direction: 0x%04x", val};
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM,
+            "Invalid frequency shifter integer property 0x%04x", param};
+    }
+}
+void Fshifter_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ Fshifter_setParami(props, param, vals[0]); }
+
+void Fshifter_getParami(const EffectProps *props, ALenum param, int *val)
+{
+    switch(param)
+    {
+    case AL_FREQUENCY_SHIFTER_LEFT_DIRECTION:
+        *val = EnumFromDirection(props->Fshifter.LeftDirection);
+        break;
+    case AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION:
+        *val = EnumFromDirection(props->Fshifter.RightDirection);
+        break;
+    default:
+        throw effect_exception{AL_INVALID_ENUM,
+            "Invalid frequency shifter integer property 0x%04x", param};
+    }
+}
+void Fshifter_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ Fshifter_getParami(props, param, vals); }
+
+void Fshifter_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_FREQUENCY_SHIFTER_FREQUENCY:
+        *val = props->Fshifter.Frequency;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid frequency shifter float property 0x%04x",
+            param};
+    }
+}
+void Fshifter_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Fshifter_getParamf(props, param, vals); }
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Fshifter.Frequency      = AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY;
+    props.Fshifter.LeftDirection  = *DirectionFromEmum(AL_FREQUENCY_SHIFTER_DEFAULT_LEFT_DIRECTION);
+    props.Fshifter.RightDirection = *DirectionFromEmum(AL_FREQUENCY_SHIFTER_DEFAULT_RIGHT_DIRECTION);
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Fshifter);
+
+const EffectProps FshifterEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using FrequencyShifterCommitter = EaxCommitter<EaxFrequencyShifterCommitter>;
+
+struct FrequencyValidator {
+    void operator()(float flFrequency) const
+    {
+        eax_validate_range<FrequencyShifterCommitter::Exception>(
+            "Frequency",
+            flFrequency,
+            EAXFREQUENCYSHIFTER_MINFREQUENCY,
+            EAXFREQUENCYSHIFTER_MAXFREQUENCY);
+    }
+}; // FrequencyValidator
+
+struct LeftDirectionValidator {
+    void operator()(unsigned long ulLeftDirection) const
+    {
+        eax_validate_range<FrequencyShifterCommitter::Exception>(
+            "Left Direction",
+            ulLeftDirection,
+            EAXFREQUENCYSHIFTER_MINLEFTDIRECTION,
+            EAXFREQUENCYSHIFTER_MAXLEFTDIRECTION);
+    }
+}; // LeftDirectionValidator
+
+struct RightDirectionValidator {
+    void operator()(unsigned long ulRightDirection) const
+    {
+        eax_validate_range<FrequencyShifterCommitter::Exception>(
+            "Right Direction",
+            ulRightDirection,
+            EAXFREQUENCYSHIFTER_MINRIGHTDIRECTION,
+            EAXFREQUENCYSHIFTER_MAXRIGHTDIRECTION);
+    }
+}; // RightDirectionValidator
+
+struct AllValidator {
+    void operator()(const EAXFREQUENCYSHIFTERPROPERTIES& all) const
+    {
+        FrequencyValidator{}(all.flFrequency);
+        LeftDirectionValidator{}(all.ulLeftDirection);
+        RightDirectionValidator{}(all.ulRightDirection);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct FrequencyShifterCommitter::Exception : public EaxException {
+    explicit Exception(const char *message) : EaxException{"EAX_FREQUENCY_SHIFTER_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void FrequencyShifterCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool FrequencyShifterCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType
+        && mEaxProps.mFrequencyShifter.flFrequency == props.mFrequencyShifter.flFrequency
+        && mEaxProps.mFrequencyShifter.ulLeftDirection == props.mFrequencyShifter.ulLeftDirection
+        && mEaxProps.mFrequencyShifter.ulRightDirection == props.mFrequencyShifter.ulRightDirection)
+        return false;
+
+    mEaxProps = props;
+
+    auto get_direction = [](unsigned long dir) noexcept
+    {
+        if(dir == EAX_FREQUENCYSHIFTER_DOWN)
+            return FShifterDirection::Down;
+        if(dir == EAX_FREQUENCYSHIFTER_UP)
+            return FShifterDirection::Up;
+        return FShifterDirection::Off;
+    };
+
+    mAlProps.Fshifter.Frequency = props.mFrequencyShifter.flFrequency;
+    mAlProps.Fshifter.LeftDirection = get_direction(props.mFrequencyShifter.ulLeftDirection);
+    mAlProps.Fshifter.RightDirection = get_direction(props.mFrequencyShifter.ulRightDirection);
+
+    return true;
+}
+
+template<>
+void FrequencyShifterCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::FrequencyShifter;
+    props.mFrequencyShifter.flFrequency = EAXFREQUENCYSHIFTER_DEFAULTFREQUENCY;
+    props.mFrequencyShifter.ulLeftDirection = EAXFREQUENCYSHIFTER_DEFAULTLEFTDIRECTION;
+    props.mFrequencyShifter.ulRightDirection = EAXFREQUENCYSHIFTER_DEFAULTRIGHTDIRECTION;
+}
+
+template<>
+void FrequencyShifterCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXFREQUENCYSHIFTER_NONE: break;
+    case EAXFREQUENCYSHIFTER_ALLPARAMETERS: call.set_value<Exception>(props.mFrequencyShifter); break;
+    case EAXFREQUENCYSHIFTER_FREQUENCY: call.set_value<Exception>(props.mFrequencyShifter.flFrequency); break;
+    case EAXFREQUENCYSHIFTER_LEFTDIRECTION: call.set_value<Exception>(props.mFrequencyShifter.ulLeftDirection); break;
+    case EAXFREQUENCYSHIFTER_RIGHTDIRECTION: call.set_value<Exception>(props.mFrequencyShifter.ulRightDirection); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void FrequencyShifterCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXFREQUENCYSHIFTER_NONE: break;
+    case EAXFREQUENCYSHIFTER_ALLPARAMETERS: defer<AllValidator>(call, props.mFrequencyShifter); break;
+    case EAXFREQUENCYSHIFTER_FREQUENCY: defer<FrequencyValidator>(call, props.mFrequencyShifter.flFrequency); break;
+    case EAXFREQUENCYSHIFTER_LEFTDIRECTION: defer<LeftDirectionValidator>(call, props.mFrequencyShifter.ulLeftDirection); break;
+    case EAXFREQUENCYSHIFTER_RIGHTDIRECTION: defer<RightDirectionValidator>(call, props.mFrequencyShifter.ulRightDirection); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/modulator.cpp b/al/effects/modulator.cpp
new file mode 100644 (file)
index 0000000..5f37d08
--- /dev/null
@@ -0,0 +1,272 @@
+
+#include "config.h"
+
+#include <stdexcept>
+
+#include "AL/al.h"
+#include "AL/efx.h"
+
+#include "alc/effects/base.h"
+#include "aloptional.h"
+#include "effects.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+#include "alnumeric.h"
+#include "al/eax/exception.h"
+#include "al/eax/utils.h"
+#endif // ALSOFT_EAX
+
+
+namespace {
+
+al::optional<ModulatorWaveform> WaveformFromEmum(ALenum value)
+{
+    switch(value)
+    {
+    case AL_RING_MODULATOR_SINUSOID: return ModulatorWaveform::Sinusoid;
+    case AL_RING_MODULATOR_SAWTOOTH: return ModulatorWaveform::Sawtooth;
+    case AL_RING_MODULATOR_SQUARE: return ModulatorWaveform::Square;
+    }
+    return al::nullopt;
+}
+ALenum EnumFromWaveform(ModulatorWaveform type)
+{
+    switch(type)
+    {
+    case ModulatorWaveform::Sinusoid: return AL_RING_MODULATOR_SINUSOID;
+    case ModulatorWaveform::Sawtooth: return AL_RING_MODULATOR_SAWTOOTH;
+    case ModulatorWaveform::Square: return AL_RING_MODULATOR_SQUARE;
+    }
+    throw std::runtime_error{"Invalid modulator waveform: " +
+        std::to_string(static_cast<int>(type))};
+}
+
+void Modulator_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_RING_MODULATOR_FREQUENCY:
+        if(!(val >= AL_RING_MODULATOR_MIN_FREQUENCY && val <= AL_RING_MODULATOR_MAX_FREQUENCY))
+            throw effect_exception{AL_INVALID_VALUE, "Modulator frequency out of range: %f", val};
+        props->Modulator.Frequency = val;
+        break;
+
+    case AL_RING_MODULATOR_HIGHPASS_CUTOFF:
+        if(!(val >= AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF && val <= AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF))
+            throw effect_exception{AL_INVALID_VALUE, "Modulator high-pass cutoff out of range: %f", val};
+        props->Modulator.HighPassCutoff = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid modulator float property 0x%04x", param};
+    }
+}
+void Modulator_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Modulator_setParamf(props, param, vals[0]); }
+void Modulator_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_RING_MODULATOR_FREQUENCY:
+    case AL_RING_MODULATOR_HIGHPASS_CUTOFF:
+        Modulator_setParamf(props, param, static_cast<float>(val));
+        break;
+
+    case AL_RING_MODULATOR_WAVEFORM:
+        if(auto formopt = WaveformFromEmum(val))
+            props->Modulator.Waveform = *formopt;
+        else
+            throw effect_exception{AL_INVALID_VALUE, "Invalid modulator waveform: 0x%04x", val};
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid modulator integer property 0x%04x",
+            param};
+    }
+}
+void Modulator_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ Modulator_setParami(props, param, vals[0]); }
+
+void Modulator_getParami(const EffectProps *props, ALenum param, int *val)
+{
+    switch(param)
+    {
+    case AL_RING_MODULATOR_FREQUENCY:
+        *val = static_cast<int>(props->Modulator.Frequency);
+        break;
+    case AL_RING_MODULATOR_HIGHPASS_CUTOFF:
+        *val = static_cast<int>(props->Modulator.HighPassCutoff);
+        break;
+    case AL_RING_MODULATOR_WAVEFORM:
+        *val = EnumFromWaveform(props->Modulator.Waveform);
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid modulator integer property 0x%04x",
+            param};
+    }
+}
+void Modulator_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ Modulator_getParami(props, param, vals); }
+void Modulator_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_RING_MODULATOR_FREQUENCY:
+        *val = props->Modulator.Frequency;
+        break;
+    case AL_RING_MODULATOR_HIGHPASS_CUTOFF:
+        *val = props->Modulator.HighPassCutoff;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid modulator float property 0x%04x", param};
+    }
+}
+void Modulator_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Modulator_getParamf(props, param, vals); }
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Modulator.Frequency      = AL_RING_MODULATOR_DEFAULT_FREQUENCY;
+    props.Modulator.HighPassCutoff = AL_RING_MODULATOR_DEFAULT_HIGHPASS_CUTOFF;
+    props.Modulator.Waveform       = *WaveformFromEmum(AL_RING_MODULATOR_DEFAULT_WAVEFORM);
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Modulator);
+
+const EffectProps ModulatorEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using ModulatorCommitter = EaxCommitter<EaxModulatorCommitter>;
+
+struct FrequencyValidator {
+    void operator()(float flFrequency) const
+    {
+        eax_validate_range<ModulatorCommitter::Exception>(
+            "Frequency",
+            flFrequency,
+            EAXRINGMODULATOR_MINFREQUENCY,
+            EAXRINGMODULATOR_MAXFREQUENCY);
+    }
+}; // FrequencyValidator
+
+struct HighPassCutOffValidator {
+    void operator()(float flHighPassCutOff) const
+    {
+        eax_validate_range<ModulatorCommitter::Exception>(
+            "High-Pass Cutoff",
+            flHighPassCutOff,
+            EAXRINGMODULATOR_MINHIGHPASSCUTOFF,
+            EAXRINGMODULATOR_MAXHIGHPASSCUTOFF);
+    }
+}; // HighPassCutOffValidator
+
+struct WaveformValidator {
+    void operator()(unsigned long ulWaveform) const
+    {
+        eax_validate_range<ModulatorCommitter::Exception>(
+            "Waveform",
+            ulWaveform,
+            EAXRINGMODULATOR_MINWAVEFORM,
+            EAXRINGMODULATOR_MAXWAVEFORM);
+    }
+}; // WaveformValidator
+
+struct AllValidator {
+    void operator()(const EAXRINGMODULATORPROPERTIES& all) const
+    {
+        FrequencyValidator{}(all.flFrequency);
+        HighPassCutOffValidator{}(all.flHighPassCutOff);
+        WaveformValidator{}(all.ulWaveform);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct ModulatorCommitter::Exception : public EaxException {
+    explicit Exception(const char *message) : EaxException{"EAX_RING_MODULATOR_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void ModulatorCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool ModulatorCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType
+        && mEaxProps.mModulator.flFrequency == props.mModulator.flFrequency
+        && mEaxProps.mModulator.flHighPassCutOff == props.mModulator.flHighPassCutOff
+        && mEaxProps.mModulator.ulWaveform == props.mModulator.ulWaveform)
+        return false;
+
+    mEaxProps = props;
+
+    auto get_waveform = [](unsigned long form)
+    {
+        if(form == EAX_RINGMODULATOR_SINUSOID)
+            return ModulatorWaveform::Sinusoid;
+        if(form == EAX_RINGMODULATOR_SAWTOOTH)
+            return ModulatorWaveform::Sawtooth;
+        if(form == EAX_RINGMODULATOR_SQUARE)
+            return ModulatorWaveform::Square;
+        return ModulatorWaveform::Sinusoid;
+    };
+
+    mAlProps.Modulator.Frequency = props.mModulator.flFrequency;
+    mAlProps.Modulator.HighPassCutoff = props.mModulator.flHighPassCutOff;
+    mAlProps.Modulator.Waveform = get_waveform(props.mModulator.ulWaveform);
+
+    return true;
+}
+
+template<>
+void ModulatorCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::Modulator;
+    props.mModulator.flFrequency = EAXRINGMODULATOR_DEFAULTFREQUENCY;
+    props.mModulator.flHighPassCutOff = EAXRINGMODULATOR_DEFAULTHIGHPASSCUTOFF;
+    props.mModulator.ulWaveform = EAXRINGMODULATOR_DEFAULTWAVEFORM;
+}
+
+template<>
+void ModulatorCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXRINGMODULATOR_NONE: break;
+    case EAXRINGMODULATOR_ALLPARAMETERS: call.set_value<Exception>(props.mModulator); break;
+    case EAXRINGMODULATOR_FREQUENCY: call.set_value<Exception>(props.mModulator.flFrequency); break;
+    case EAXRINGMODULATOR_HIGHPASSCUTOFF: call.set_value<Exception>(props.mModulator.flHighPassCutOff); break;
+    case EAXRINGMODULATOR_WAVEFORM: call.set_value<Exception>(props.mModulator.ulWaveform); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void ModulatorCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch (call.get_property_id())
+    {
+    case EAXRINGMODULATOR_NONE: break;
+    case EAXRINGMODULATOR_ALLPARAMETERS: defer<AllValidator>(call, props.mModulator); break;
+    case EAXRINGMODULATOR_FREQUENCY: defer<FrequencyValidator>(call, props.mModulator.flFrequency); break;
+    case EAXRINGMODULATOR_HIGHPASSCUTOFF: defer<HighPassCutOffValidator>(call, props.mModulator.flHighPassCutOff); break;
+    case EAXRINGMODULATOR_WAVEFORM: defer<WaveformValidator>(call, props.mModulator.ulWaveform); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/null.cpp b/al/effects/null.cpp
new file mode 100644 (file)
index 0000000..0bbc183
--- /dev/null
@@ -0,0 +1,149 @@
+
+#include "config.h"
+
+#include "AL/al.h"
+#include "AL/efx.h"
+
+#include "alc/effects/base.h"
+#include "effects.h"
+
+#ifdef ALSOFT_EAX
+#include "al/eax/exception.h"
+#endif // ALSOFT_EAX
+
+
+namespace {
+
+void Null_setParami(EffectProps* /*props*/, ALenum param, int /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect integer property 0x%04x",
+            param};
+    }
+}
+void Null_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{
+    switch(param)
+    {
+    default:
+        Null_setParami(props, param, vals[0]);
+    }
+}
+void Null_setParamf(EffectProps* /*props*/, ALenum param, float /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect float property 0x%04x",
+            param};
+    }
+}
+void Null_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{
+    switch(param)
+    {
+    default:
+        Null_setParamf(props, param, vals[0]);
+    }
+}
+
+void Null_getParami(const EffectProps* /*props*/, ALenum param, int* /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect integer property 0x%04x",
+            param};
+    }
+}
+void Null_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{
+    switch(param)
+    {
+    default:
+        Null_getParami(props, param, vals);
+    }
+}
+void Null_getParamf(const EffectProps* /*props*/, ALenum param, float* /*val*/)
+{
+    switch(param)
+    {
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid null effect float property 0x%04x",
+            param};
+    }
+}
+void Null_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{
+    switch(param)
+    {
+    default:
+        Null_getParamf(props, param, vals);
+    }
+}
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Null);
+
+const EffectProps NullEffectProps{genDefaultProps()};
+
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using NullCommitter = EaxCommitter<EaxNullCommitter>;
+
+} // namespace
+
+template<>
+struct NullCommitter::Exception : public EaxException
+{
+    explicit Exception(const char *message) : EaxException{"EAX_NULL_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void NullCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool NullCommitter::commit(const EaxEffectProps &props)
+{
+    const bool ret{props.mType != mEaxProps.mType};
+    mEaxProps = props;
+    return ret;
+}
+
+template<>
+void NullCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props = EaxEffectProps{};
+    props.mType = EaxEffectType::None;
+}
+
+template<>
+void NullCommitter::Get(const EaxCall &call, const EaxEffectProps&)
+{
+    if(call.get_property_id() != 0)
+        fail_unknown_property_id();
+}
+
+template<>
+void NullCommitter::Set(const EaxCall &call, EaxEffectProps&)
+{
+    if(call.get_property_id() != 0)
+        fail_unknown_property_id();
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/pshifter.cpp b/al/effects/pshifter.cpp
new file mode 100644 (file)
index 0000000..634eb18
--- /dev/null
@@ -0,0 +1,191 @@
+
+#include "config.h"
+
+#include "AL/al.h"
+#include "AL/efx.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 Pshifter_setParamf(EffectProps*, ALenum param, float)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid pitch shifter float property 0x%04x", param}; }
+void Pshifter_setParamfv(EffectProps*, ALenum param, const float*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid pitch shifter float-vector property 0x%04x",
+        param};
+}
+
+void Pshifter_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_PITCH_SHIFTER_COARSE_TUNE:
+        if(!(val >= AL_PITCH_SHIFTER_MIN_COARSE_TUNE && val <= AL_PITCH_SHIFTER_MAX_COARSE_TUNE))
+            throw effect_exception{AL_INVALID_VALUE, "Pitch shifter coarse tune out of range"};
+        props->Pshifter.CoarseTune = val;
+        break;
+
+    case AL_PITCH_SHIFTER_FINE_TUNE:
+        if(!(val >= AL_PITCH_SHIFTER_MIN_FINE_TUNE && val <= AL_PITCH_SHIFTER_MAX_FINE_TUNE))
+            throw effect_exception{AL_INVALID_VALUE, "Pitch shifter fine tune out of range"};
+        props->Pshifter.FineTune = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid pitch shifter integer property 0x%04x",
+            param};
+    }
+}
+void Pshifter_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ Pshifter_setParami(props, param, vals[0]); }
+
+void Pshifter_getParami(const EffectProps *props, ALenum param, int *val)
+{
+    switch(param)
+    {
+    case AL_PITCH_SHIFTER_COARSE_TUNE:
+        *val = props->Pshifter.CoarseTune;
+        break;
+    case AL_PITCH_SHIFTER_FINE_TUNE:
+        *val = props->Pshifter.FineTune;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid pitch shifter integer property 0x%04x",
+            param};
+    }
+}
+void Pshifter_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ Pshifter_getParami(props, param, vals); }
+
+void Pshifter_getParamf(const EffectProps*, ALenum param, float*)
+{ throw effect_exception{AL_INVALID_ENUM, "Invalid pitch shifter float property 0x%04x", param}; }
+void Pshifter_getParamfv(const EffectProps*, ALenum param, float*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid pitch shifter float vector-property 0x%04x",
+        param};
+}
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Pshifter.CoarseTune = AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE;
+    props.Pshifter.FineTune   = AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Pshifter);
+
+const EffectProps PshifterEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using PitchShifterCommitter = EaxCommitter<EaxPitchShifterCommitter>;
+
+struct CoarseTuneValidator {
+    void operator()(long lCoarseTune) const
+    {
+        eax_validate_range<PitchShifterCommitter::Exception>(
+            "Coarse Tune",
+            lCoarseTune,
+            EAXPITCHSHIFTER_MINCOARSETUNE,
+            EAXPITCHSHIFTER_MAXCOARSETUNE);
+    }
+}; // CoarseTuneValidator
+
+struct FineTuneValidator {
+    void operator()(long lFineTune) const
+    {
+        eax_validate_range<PitchShifterCommitter::Exception>(
+            "Fine Tune",
+            lFineTune,
+            EAXPITCHSHIFTER_MINFINETUNE,
+            EAXPITCHSHIFTER_MAXFINETUNE);
+    }
+}; // FineTuneValidator
+
+struct AllValidator {
+    void operator()(const EAXPITCHSHIFTERPROPERTIES& all) const
+    {
+        CoarseTuneValidator{}(all.lCoarseTune);
+        FineTuneValidator{}(all.lFineTune);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct PitchShifterCommitter::Exception : public EaxException {
+    explicit Exception(const char *message) : EaxException{"EAX_PITCH_SHIFTER_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void PitchShifterCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool PitchShifterCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType
+        && mEaxProps.mPitchShifter.lCoarseTune == props.mPitchShifter.lCoarseTune
+        && mEaxProps.mPitchShifter.lFineTune == props.mPitchShifter.lFineTune)
+        return false;
+
+    mEaxProps = props;
+
+    mAlProps.Pshifter.CoarseTune = static_cast<int>(mEaxProps.mPitchShifter.lCoarseTune);
+    mAlProps.Pshifter.FineTune = static_cast<int>(mEaxProps.mPitchShifter.lFineTune);
+
+    return true;
+}
+
+template<>
+void PitchShifterCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::PitchShifter;
+    props.mPitchShifter.lCoarseTune = EAXPITCHSHIFTER_DEFAULTCOARSETUNE;
+    props.mPitchShifter.lFineTune = EAXPITCHSHIFTER_DEFAULTFINETUNE;
+}
+
+template<>
+void PitchShifterCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXPITCHSHIFTER_NONE: break;
+    case EAXPITCHSHIFTER_ALLPARAMETERS: call.set_value<Exception>(props.mPitchShifter); break;
+    case EAXPITCHSHIFTER_COARSETUNE: call.set_value<Exception>(props.mPitchShifter.lCoarseTune); break;
+    case EAXPITCHSHIFTER_FINETUNE: call.set_value<Exception>(props.mPitchShifter.lFineTune); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+template<>
+void PitchShifterCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXPITCHSHIFTER_NONE: break;
+    case EAXPITCHSHIFTER_ALLPARAMETERS: defer<AllValidator>(call, props.mPitchShifter); break;
+    case EAXPITCHSHIFTER_COARSETUNE: defer<CoarseTuneValidator>(call, props.mPitchShifter.lCoarseTune); break;
+    case EAXPITCHSHIFTER_FINETUNE: defer<FineTuneValidator>(call, props.mPitchShifter.lFineTune); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/reverb.cpp b/al/effects/reverb.cpp
new file mode 100644 (file)
index 0000000..440d7b4
--- /dev/null
@@ -0,0 +1,1499 @@
+
+#include "config.h"
+
+#include <cmath>
+
+#include "AL/al.h"
+#include "AL/efx.h"
+
+#include "alc/effects/base.h"
+#include "effects.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+#include "alnumeric.h"
+#include "AL/efx-presets.h"
+#include "al/eax/exception.h"
+#include "al/eax/utils.h"
+#endif // ALSOFT_EAX
+
+
+namespace {
+
+void Reverb_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_EAXREVERB_DECAY_HFLIMIT:
+        if(!(val >= AL_EAXREVERB_MIN_DECAY_HFLIMIT && val <= AL_EAXREVERB_MAX_DECAY_HFLIMIT))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb decay hflimit out of range"};
+        props->Reverb.DecayHFLimit = val != AL_FALSE;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid EAX reverb integer property 0x%04x",
+            param};
+    }
+}
+void Reverb_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ Reverb_setParami(props, param, vals[0]); }
+void Reverb_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_EAXREVERB_DENSITY:
+        if(!(val >= AL_EAXREVERB_MIN_DENSITY && val <= AL_EAXREVERB_MAX_DENSITY))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb density out of range"};
+        props->Reverb.Density = val;
+        break;
+
+    case AL_EAXREVERB_DIFFUSION:
+        if(!(val >= AL_EAXREVERB_MIN_DIFFUSION && val <= AL_EAXREVERB_MAX_DIFFUSION))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb diffusion out of range"};
+        props->Reverb.Diffusion = val;
+        break;
+
+    case AL_EAXREVERB_GAIN:
+        if(!(val >= AL_EAXREVERB_MIN_GAIN && val <= AL_EAXREVERB_MAX_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb gain out of range"};
+        props->Reverb.Gain = val;
+        break;
+
+    case AL_EAXREVERB_GAINHF:
+        if(!(val >= AL_EAXREVERB_MIN_GAINHF && val <= AL_EAXREVERB_MAX_GAINHF))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb gainhf out of range"};
+        props->Reverb.GainHF = val;
+        break;
+
+    case AL_EAXREVERB_GAINLF:
+        if(!(val >= AL_EAXREVERB_MIN_GAINLF && val <= AL_EAXREVERB_MAX_GAINLF))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb gainlf out of range"};
+        props->Reverb.GainLF = val;
+        break;
+
+    case AL_EAXREVERB_DECAY_TIME:
+        if(!(val >= AL_EAXREVERB_MIN_DECAY_TIME && val <= AL_EAXREVERB_MAX_DECAY_TIME))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb decay time out of range"};
+        props->Reverb.DecayTime = val;
+        break;
+
+    case AL_EAXREVERB_DECAY_HFRATIO:
+        if(!(val >= AL_EAXREVERB_MIN_DECAY_HFRATIO && val <= AL_EAXREVERB_MAX_DECAY_HFRATIO))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb decay hfratio out of range"};
+        props->Reverb.DecayHFRatio = val;
+        break;
+
+    case AL_EAXREVERB_DECAY_LFRATIO:
+        if(!(val >= AL_EAXREVERB_MIN_DECAY_LFRATIO && val <= AL_EAXREVERB_MAX_DECAY_LFRATIO))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb decay lfratio out of range"};
+        props->Reverb.DecayLFRatio = val;
+        break;
+
+    case AL_EAXREVERB_REFLECTIONS_GAIN:
+        if(!(val >= AL_EAXREVERB_MIN_REFLECTIONS_GAIN && val <= AL_EAXREVERB_MAX_REFLECTIONS_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb reflections gain out of range"};
+        props->Reverb.ReflectionsGain = val;
+        break;
+
+    case AL_EAXREVERB_REFLECTIONS_DELAY:
+        if(!(val >= AL_EAXREVERB_MIN_REFLECTIONS_DELAY && val <= AL_EAXREVERB_MAX_REFLECTIONS_DELAY))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb reflections delay out of range"};
+        props->Reverb.ReflectionsDelay = val;
+        break;
+
+    case AL_EAXREVERB_LATE_REVERB_GAIN:
+        if(!(val >= AL_EAXREVERB_MIN_LATE_REVERB_GAIN && val <= AL_EAXREVERB_MAX_LATE_REVERB_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb late reverb gain out of range"};
+        props->Reverb.LateReverbGain = val;
+        break;
+
+    case AL_EAXREVERB_LATE_REVERB_DELAY:
+        if(!(val >= AL_EAXREVERB_MIN_LATE_REVERB_DELAY && val <= AL_EAXREVERB_MAX_LATE_REVERB_DELAY))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb late reverb delay out of range"};
+        props->Reverb.LateReverbDelay = val;
+        break;
+
+    case AL_EAXREVERB_ECHO_TIME:
+        if(!(val >= AL_EAXREVERB_MIN_ECHO_TIME && val <= AL_EAXREVERB_MAX_ECHO_TIME))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb echo time out of range"};
+        props->Reverb.EchoTime = val;
+        break;
+
+    case AL_EAXREVERB_ECHO_DEPTH:
+        if(!(val >= AL_EAXREVERB_MIN_ECHO_DEPTH && val <= AL_EAXREVERB_MAX_ECHO_DEPTH))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb echo depth out of range"};
+        props->Reverb.EchoDepth = val;
+        break;
+
+    case AL_EAXREVERB_MODULATION_TIME:
+        if(!(val >= AL_EAXREVERB_MIN_MODULATION_TIME && val <= AL_EAXREVERB_MAX_MODULATION_TIME))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb modulation time out of range"};
+        props->Reverb.ModulationTime = val;
+        break;
+
+    case AL_EAXREVERB_MODULATION_DEPTH:
+        if(!(val >= AL_EAXREVERB_MIN_MODULATION_DEPTH && val <= AL_EAXREVERB_MAX_MODULATION_DEPTH))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb modulation depth out of range"};
+        props->Reverb.ModulationDepth = val;
+        break;
+
+    case AL_EAXREVERB_AIR_ABSORPTION_GAINHF:
+        if(!(val >= AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF && val <= AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb air absorption gainhf out of range"};
+        props->Reverb.AirAbsorptionGainHF = val;
+        break;
+
+    case AL_EAXREVERB_HFREFERENCE:
+        if(!(val >= AL_EAXREVERB_MIN_HFREFERENCE && val <= AL_EAXREVERB_MAX_HFREFERENCE))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb hfreference out of range"};
+        props->Reverb.HFReference = val;
+        break;
+
+    case AL_EAXREVERB_LFREFERENCE:
+        if(!(val >= AL_EAXREVERB_MIN_LFREFERENCE && val <= AL_EAXREVERB_MAX_LFREFERENCE))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb lfreference out of range"};
+        props->Reverb.LFReference = val;
+        break;
+
+    case AL_EAXREVERB_ROOM_ROLLOFF_FACTOR:
+        if(!(val >= AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR && val <= AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb room rolloff factor out of range"};
+        props->Reverb.RoomRolloffFactor = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid EAX reverb float property 0x%04x", param};
+    }
+}
+void Reverb_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{
+    switch(param)
+    {
+    case AL_EAXREVERB_REFLECTIONS_PAN:
+        if(!(std::isfinite(vals[0]) && std::isfinite(vals[1]) && std::isfinite(vals[2])))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb reflections pan out of range"};
+        props->Reverb.ReflectionsPan[0] = vals[0];
+        props->Reverb.ReflectionsPan[1] = vals[1];
+        props->Reverb.ReflectionsPan[2] = vals[2];
+        break;
+    case AL_EAXREVERB_LATE_REVERB_PAN:
+        if(!(std::isfinite(vals[0]) && std::isfinite(vals[1]) && std::isfinite(vals[2])))
+            throw effect_exception{AL_INVALID_VALUE, "EAX Reverb late reverb pan out of range"};
+        props->Reverb.LateReverbPan[0] = vals[0];
+        props->Reverb.LateReverbPan[1] = vals[1];
+        props->Reverb.LateReverbPan[2] = vals[2];
+        break;
+
+    default:
+        Reverb_setParamf(props, param, vals[0]);
+        break;
+    }
+}
+
+void Reverb_getParami(const EffectProps *props, ALenum param, int *val)
+{
+    switch(param)
+    {
+    case AL_EAXREVERB_DECAY_HFLIMIT:
+        *val = props->Reverb.DecayHFLimit;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid EAX reverb integer property 0x%04x",
+            param};
+    }
+}
+void Reverb_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ Reverb_getParami(props, param, vals); }
+void Reverb_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_EAXREVERB_DENSITY:
+        *val = props->Reverb.Density;
+        break;
+
+    case AL_EAXREVERB_DIFFUSION:
+        *val = props->Reverb.Diffusion;
+        break;
+
+    case AL_EAXREVERB_GAIN:
+        *val = props->Reverb.Gain;
+        break;
+
+    case AL_EAXREVERB_GAINHF:
+        *val = props->Reverb.GainHF;
+        break;
+
+    case AL_EAXREVERB_GAINLF:
+        *val = props->Reverb.GainLF;
+        break;
+
+    case AL_EAXREVERB_DECAY_TIME:
+        *val = props->Reverb.DecayTime;
+        break;
+
+    case AL_EAXREVERB_DECAY_HFRATIO:
+        *val = props->Reverb.DecayHFRatio;
+        break;
+
+    case AL_EAXREVERB_DECAY_LFRATIO:
+        *val = props->Reverb.DecayLFRatio;
+        break;
+
+    case AL_EAXREVERB_REFLECTIONS_GAIN:
+        *val = props->Reverb.ReflectionsGain;
+        break;
+
+    case AL_EAXREVERB_REFLECTIONS_DELAY:
+        *val = props->Reverb.ReflectionsDelay;
+        break;
+
+    case AL_EAXREVERB_LATE_REVERB_GAIN:
+        *val = props->Reverb.LateReverbGain;
+        break;
+
+    case AL_EAXREVERB_LATE_REVERB_DELAY:
+        *val = props->Reverb.LateReverbDelay;
+        break;
+
+    case AL_EAXREVERB_ECHO_TIME:
+        *val = props->Reverb.EchoTime;
+        break;
+
+    case AL_EAXREVERB_ECHO_DEPTH:
+        *val = props->Reverb.EchoDepth;
+        break;
+
+    case AL_EAXREVERB_MODULATION_TIME:
+        *val = props->Reverb.ModulationTime;
+        break;
+
+    case AL_EAXREVERB_MODULATION_DEPTH:
+        *val = props->Reverb.ModulationDepth;
+        break;
+
+    case AL_EAXREVERB_AIR_ABSORPTION_GAINHF:
+        *val = props->Reverb.AirAbsorptionGainHF;
+        break;
+
+    case AL_EAXREVERB_HFREFERENCE:
+        *val = props->Reverb.HFReference;
+        break;
+
+    case AL_EAXREVERB_LFREFERENCE:
+        *val = props->Reverb.LFReference;
+        break;
+
+    case AL_EAXREVERB_ROOM_ROLLOFF_FACTOR:
+        *val = props->Reverb.RoomRolloffFactor;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid EAX reverb float property 0x%04x", param};
+    }
+}
+void Reverb_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{
+    switch(param)
+    {
+    case AL_EAXREVERB_REFLECTIONS_PAN:
+        vals[0] = props->Reverb.ReflectionsPan[0];
+        vals[1] = props->Reverb.ReflectionsPan[1];
+        vals[2] = props->Reverb.ReflectionsPan[2];
+        break;
+    case AL_EAXREVERB_LATE_REVERB_PAN:
+        vals[0] = props->Reverb.LateReverbPan[0];
+        vals[1] = props->Reverb.LateReverbPan[1];
+        vals[2] = props->Reverb.LateReverbPan[2];
+        break;
+
+    default:
+        Reverb_getParamf(props, param, vals);
+        break;
+    }
+}
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Reverb.Density   = AL_EAXREVERB_DEFAULT_DENSITY;
+    props.Reverb.Diffusion = AL_EAXREVERB_DEFAULT_DIFFUSION;
+    props.Reverb.Gain   = AL_EAXREVERB_DEFAULT_GAIN;
+    props.Reverb.GainHF = AL_EAXREVERB_DEFAULT_GAINHF;
+    props.Reverb.GainLF = AL_EAXREVERB_DEFAULT_GAINLF;
+    props.Reverb.DecayTime    = AL_EAXREVERB_DEFAULT_DECAY_TIME;
+    props.Reverb.DecayHFRatio = AL_EAXREVERB_DEFAULT_DECAY_HFRATIO;
+    props.Reverb.DecayLFRatio = AL_EAXREVERB_DEFAULT_DECAY_LFRATIO;
+    props.Reverb.ReflectionsGain   = AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN;
+    props.Reverb.ReflectionsDelay  = AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY;
+    props.Reverb.ReflectionsPan[0] = AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ;
+    props.Reverb.ReflectionsPan[1] = AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ;
+    props.Reverb.ReflectionsPan[2] = AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ;
+    props.Reverb.LateReverbGain   = AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN;
+    props.Reverb.LateReverbDelay  = AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY;
+    props.Reverb.LateReverbPan[0] = AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ;
+    props.Reverb.LateReverbPan[1] = AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ;
+    props.Reverb.LateReverbPan[2] = AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ;
+    props.Reverb.EchoTime  = AL_EAXREVERB_DEFAULT_ECHO_TIME;
+    props.Reverb.EchoDepth = AL_EAXREVERB_DEFAULT_ECHO_DEPTH;
+    props.Reverb.ModulationTime  = AL_EAXREVERB_DEFAULT_MODULATION_TIME;
+    props.Reverb.ModulationDepth = AL_EAXREVERB_DEFAULT_MODULATION_DEPTH;
+    props.Reverb.AirAbsorptionGainHF = AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF;
+    props.Reverb.HFReference = AL_EAXREVERB_DEFAULT_HFREFERENCE;
+    props.Reverb.LFReference = AL_EAXREVERB_DEFAULT_LFREFERENCE;
+    props.Reverb.RoomRolloffFactor = AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR;
+    props.Reverb.DecayHFLimit = AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT;
+    return props;
+}
+
+
+void StdReverb_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_REVERB_DECAY_HFLIMIT:
+        if(!(val >= AL_REVERB_MIN_DECAY_HFLIMIT && val <= AL_REVERB_MAX_DECAY_HFLIMIT))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb decay hflimit out of range"};
+        props->Reverb.DecayHFLimit = val != AL_FALSE;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid reverb integer property 0x%04x", param};
+    }
+}
+void StdReverb_setParamiv(EffectProps *props, ALenum param, const int *vals)
+{ StdReverb_setParami(props, param, vals[0]); }
+void StdReverb_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_REVERB_DENSITY:
+        if(!(val >= AL_REVERB_MIN_DENSITY && val <= AL_REVERB_MAX_DENSITY))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb density out of range"};
+        props->Reverb.Density = val;
+        break;
+
+    case AL_REVERB_DIFFUSION:
+        if(!(val >= AL_REVERB_MIN_DIFFUSION && val <= AL_REVERB_MAX_DIFFUSION))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb diffusion out of range"};
+        props->Reverb.Diffusion = val;
+        break;
+
+    case AL_REVERB_GAIN:
+        if(!(val >= AL_REVERB_MIN_GAIN && val <= AL_REVERB_MAX_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb gain out of range"};
+        props->Reverb.Gain = val;
+        break;
+
+    case AL_REVERB_GAINHF:
+        if(!(val >= AL_REVERB_MIN_GAINHF && val <= AL_REVERB_MAX_GAINHF))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb gainhf out of range"};
+        props->Reverb.GainHF = val;
+        break;
+
+    case AL_REVERB_DECAY_TIME:
+        if(!(val >= AL_REVERB_MIN_DECAY_TIME && val <= AL_REVERB_MAX_DECAY_TIME))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb decay time out of range"};
+        props->Reverb.DecayTime = val;
+        break;
+
+    case AL_REVERB_DECAY_HFRATIO:
+        if(!(val >= AL_REVERB_MIN_DECAY_HFRATIO && val <= AL_REVERB_MAX_DECAY_HFRATIO))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb decay hfratio out of range"};
+        props->Reverb.DecayHFRatio = val;
+        break;
+
+    case AL_REVERB_REFLECTIONS_GAIN:
+        if(!(val >= AL_REVERB_MIN_REFLECTIONS_GAIN && val <= AL_REVERB_MAX_REFLECTIONS_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb reflections gain out of range"};
+        props->Reverb.ReflectionsGain = val;
+        break;
+
+    case AL_REVERB_REFLECTIONS_DELAY:
+        if(!(val >= AL_REVERB_MIN_REFLECTIONS_DELAY && val <= AL_REVERB_MAX_REFLECTIONS_DELAY))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb reflections delay out of range"};
+        props->Reverb.ReflectionsDelay = val;
+        break;
+
+    case AL_REVERB_LATE_REVERB_GAIN:
+        if(!(val >= AL_REVERB_MIN_LATE_REVERB_GAIN && val <= AL_REVERB_MAX_LATE_REVERB_GAIN))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb late reverb gain out of range"};
+        props->Reverb.LateReverbGain = val;
+        break;
+
+    case AL_REVERB_LATE_REVERB_DELAY:
+        if(!(val >= AL_REVERB_MIN_LATE_REVERB_DELAY && val <= AL_REVERB_MAX_LATE_REVERB_DELAY))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb late reverb delay out of range"};
+        props->Reverb.LateReverbDelay = val;
+        break;
+
+    case AL_REVERB_AIR_ABSORPTION_GAINHF:
+        if(!(val >= AL_REVERB_MIN_AIR_ABSORPTION_GAINHF && val <= AL_REVERB_MAX_AIR_ABSORPTION_GAINHF))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb air absorption gainhf out of range"};
+        props->Reverb.AirAbsorptionGainHF = val;
+        break;
+
+    case AL_REVERB_ROOM_ROLLOFF_FACTOR:
+        if(!(val >= AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR && val <= AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR))
+            throw effect_exception{AL_INVALID_VALUE, "Reverb room rolloff factor out of range"};
+        props->Reverb.RoomRolloffFactor = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid reverb float property 0x%04x", param};
+    }
+}
+void StdReverb_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ StdReverb_setParamf(props, param, vals[0]); }
+
+void StdReverb_getParami(const EffectProps *props, ALenum param, int *val)
+{
+    switch(param)
+    {
+    case AL_REVERB_DECAY_HFLIMIT:
+        *val = props->Reverb.DecayHFLimit;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid reverb integer property 0x%04x", param};
+    }
+}
+void StdReverb_getParamiv(const EffectProps *props, ALenum param, int *vals)
+{ StdReverb_getParami(props, param, vals); }
+void StdReverb_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_REVERB_DENSITY:
+        *val = props->Reverb.Density;
+        break;
+
+    case AL_REVERB_DIFFUSION:
+        *val = props->Reverb.Diffusion;
+        break;
+
+    case AL_REVERB_GAIN:
+        *val = props->Reverb.Gain;
+        break;
+
+    case AL_REVERB_GAINHF:
+        *val = props->Reverb.GainHF;
+        break;
+
+    case AL_REVERB_DECAY_TIME:
+        *val = props->Reverb.DecayTime;
+        break;
+
+    case AL_REVERB_DECAY_HFRATIO:
+        *val = props->Reverb.DecayHFRatio;
+        break;
+
+    case AL_REVERB_REFLECTIONS_GAIN:
+        *val = props->Reverb.ReflectionsGain;
+        break;
+
+    case AL_REVERB_REFLECTIONS_DELAY:
+        *val = props->Reverb.ReflectionsDelay;
+        break;
+
+    case AL_REVERB_LATE_REVERB_GAIN:
+        *val = props->Reverb.LateReverbGain;
+        break;
+
+    case AL_REVERB_LATE_REVERB_DELAY:
+        *val = props->Reverb.LateReverbDelay;
+        break;
+
+    case AL_REVERB_AIR_ABSORPTION_GAINHF:
+        *val = props->Reverb.AirAbsorptionGainHF;
+        break;
+
+    case AL_REVERB_ROOM_ROLLOFF_FACTOR:
+        *val = props->Reverb.RoomRolloffFactor;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid reverb float property 0x%04x", param};
+    }
+}
+void StdReverb_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ StdReverb_getParamf(props, param, vals); }
+
+EffectProps genDefaultStdProps() noexcept
+{
+    EffectProps props{};
+    props.Reverb.Density   = AL_REVERB_DEFAULT_DENSITY;
+    props.Reverb.Diffusion = AL_REVERB_DEFAULT_DIFFUSION;
+    props.Reverb.Gain   = AL_REVERB_DEFAULT_GAIN;
+    props.Reverb.GainHF = AL_REVERB_DEFAULT_GAINHF;
+    props.Reverb.GainLF = 1.0f;
+    props.Reverb.DecayTime    = AL_REVERB_DEFAULT_DECAY_TIME;
+    props.Reverb.DecayHFRatio = AL_REVERB_DEFAULT_DECAY_HFRATIO;
+    props.Reverb.DecayLFRatio = 1.0f;
+    props.Reverb.ReflectionsGain   = AL_REVERB_DEFAULT_REFLECTIONS_GAIN;
+    props.Reverb.ReflectionsDelay  = AL_REVERB_DEFAULT_REFLECTIONS_DELAY;
+    props.Reverb.ReflectionsPan[0] = 0.0f;
+    props.Reverb.ReflectionsPan[1] = 0.0f;
+    props.Reverb.ReflectionsPan[2] = 0.0f;
+    props.Reverb.LateReverbGain   = AL_REVERB_DEFAULT_LATE_REVERB_GAIN;
+    props.Reverb.LateReverbDelay  = AL_REVERB_DEFAULT_LATE_REVERB_DELAY;
+    props.Reverb.LateReverbPan[0] = 0.0f;
+    props.Reverb.LateReverbPan[1] = 0.0f;
+    props.Reverb.LateReverbPan[2] = 0.0f;
+    props.Reverb.EchoTime  = 0.25f;
+    props.Reverb.EchoDepth = 0.0f;
+    props.Reverb.ModulationTime  = 0.25f;
+    props.Reverb.ModulationDepth = 0.0f;
+    props.Reverb.AirAbsorptionGainHF = AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF;
+    props.Reverb.HFReference = 5000.0f;
+    props.Reverb.LFReference = 250.0f;
+    props.Reverb.RoomRolloffFactor = AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR;
+    props.Reverb.DecayHFLimit = AL_REVERB_DEFAULT_DECAY_HFLIMIT;
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Reverb);
+
+const EffectProps ReverbEffectProps{genDefaultProps()};
+
+DEFINE_ALEFFECT_VTABLE(StdReverb);
+
+const EffectProps StdReverbEffectProps{genDefaultStdProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+class EaxReverbEffectException : public EaxException
+{
+public:
+    explicit EaxReverbEffectException(const char* message)
+        : EaxException{"EAX_REVERB_EFFECT", message}
+    {}
+}; // EaxReverbEffectException
+
+struct EnvironmentValidator1 {
+    void operator()(unsigned long ulEnvironment) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Environment",
+            ulEnvironment,
+            EAXREVERB_MINENVIRONMENT,
+            EAX1REVERB_MAXENVIRONMENT);
+    }
+}; // EnvironmentValidator1
+
+struct VolumeValidator {
+    void operator()(float volume) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Volume",
+            volume,
+            EAX1REVERB_MINVOLUME,
+            EAX1REVERB_MAXVOLUME);
+    }
+}; // VolumeValidator
+
+struct DecayTimeValidator {
+    void operator()(float flDecayTime) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Decay Time",
+            flDecayTime,
+            EAXREVERB_MINDECAYTIME,
+            EAXREVERB_MAXDECAYTIME);
+    }
+}; // DecayTimeValidator
+
+struct DampingValidator {
+    void operator()(float damping) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Damping",
+            damping,
+            EAX1REVERB_MINDAMPING,
+            EAX1REVERB_MAXDAMPING);
+    }
+}; // DampingValidator
+
+struct AllValidator1 {
+    void operator()(const EAX_REVERBPROPERTIES& all) const
+    {
+        EnvironmentValidator1{}(all.environment);
+        VolumeValidator{}(all.fVolume);
+        DecayTimeValidator{}(all.fDecayTime_sec);
+        DampingValidator{}(all.fDamping);
+    }
+}; // AllValidator1
+
+struct RoomValidator {
+    void operator()(long lRoom) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Room",
+            lRoom,
+            EAXREVERB_MINROOM,
+            EAXREVERB_MAXROOM);
+    }
+}; // RoomValidator
+
+struct RoomHFValidator {
+    void operator()(long lRoomHF) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Room HF",
+            lRoomHF,
+            EAXREVERB_MINROOMHF,
+            EAXREVERB_MAXROOMHF);
+    }
+}; // RoomHFValidator
+
+struct RoomRolloffFactorValidator {
+    void operator()(float flRoomRolloffFactor) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Room Rolloff Factor",
+            flRoomRolloffFactor,
+            EAXREVERB_MINROOMROLLOFFFACTOR,
+            EAXREVERB_MAXROOMROLLOFFFACTOR);
+    }
+}; // RoomRolloffFactorValidator
+
+struct DecayHFRatioValidator {
+    void operator()(float flDecayHFRatio) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Decay HF Ratio",
+            flDecayHFRatio,
+            EAXREVERB_MINDECAYHFRATIO,
+            EAXREVERB_MAXDECAYHFRATIO);
+    }
+}; // DecayHFRatioValidator
+
+struct ReflectionsValidator {
+    void operator()(long lReflections) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Reflections",
+            lReflections,
+            EAXREVERB_MINREFLECTIONS,
+            EAXREVERB_MAXREFLECTIONS);
+    }
+}; // ReflectionsValidator
+
+struct ReflectionsDelayValidator {
+    void operator()(float flReflectionsDelay) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Reflections Delay",
+            flReflectionsDelay,
+            EAXREVERB_MINREFLECTIONSDELAY,
+            EAXREVERB_MAXREFLECTIONSDELAY);
+    }
+}; // ReflectionsDelayValidator
+
+struct ReverbValidator {
+    void operator()(long lReverb) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Reverb",
+            lReverb,
+            EAXREVERB_MINREVERB,
+            EAXREVERB_MAXREVERB);
+    }
+}; // ReverbValidator
+
+struct ReverbDelayValidator {
+    void operator()(float flReverbDelay) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Reverb Delay",
+            flReverbDelay,
+            EAXREVERB_MINREVERBDELAY,
+            EAXREVERB_MAXREVERBDELAY);
+    }
+}; // ReverbDelayValidator
+
+struct EnvironmentSizeValidator {
+    void operator()(float flEnvironmentSize) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Environment Size",
+            flEnvironmentSize,
+            EAXREVERB_MINENVIRONMENTSIZE,
+            EAXREVERB_MAXENVIRONMENTSIZE);
+    }
+}; // EnvironmentSizeValidator
+
+struct EnvironmentDiffusionValidator {
+    void operator()(float flEnvironmentDiffusion) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Environment Diffusion",
+            flEnvironmentDiffusion,
+            EAXREVERB_MINENVIRONMENTDIFFUSION,
+            EAXREVERB_MAXENVIRONMENTDIFFUSION);
+    }
+}; // EnvironmentDiffusionValidator
+
+struct AirAbsorptionHFValidator {
+    void operator()(float flAirAbsorptionHF) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Air Absorbtion HF",
+            flAirAbsorptionHF,
+            EAXREVERB_MINAIRABSORPTIONHF,
+            EAXREVERB_MAXAIRABSORPTIONHF);
+    }
+}; // AirAbsorptionHFValidator
+
+struct FlagsValidator2 {
+    void operator()(unsigned long ulFlags) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Flags",
+            ulFlags,
+            0UL,
+            ~EAX2LISTENERFLAGS_RESERVED);
+    }
+}; // FlagsValidator2
+
+struct AllValidator2 {
+    void operator()(const EAX20LISTENERPROPERTIES& all) const
+    {
+        RoomValidator{}(all.lRoom);
+        RoomHFValidator{}(all.lRoomHF);
+        RoomRolloffFactorValidator{}(all.flRoomRolloffFactor);
+        DecayTimeValidator{}(all.flDecayTime);
+        DecayHFRatioValidator{}(all.flDecayHFRatio);
+        ReflectionsValidator{}(all.lReflections);
+        ReflectionsDelayValidator{}(all.flReflectionsDelay);
+        ReverbValidator{}(all.lReverb);
+        ReverbDelayValidator{}(all.flReverbDelay);
+        EnvironmentValidator1{}(all.dwEnvironment);
+        EnvironmentSizeValidator{}(all.flEnvironmentSize);
+        EnvironmentDiffusionValidator{}(all.flEnvironmentDiffusion);
+        AirAbsorptionHFValidator{}(all.flAirAbsorptionHF);
+        FlagsValidator2{}(all.dwFlags);
+    }
+}; // AllValidator2
+
+struct EnvironmentValidator3 {
+    void operator()(unsigned long ulEnvironment) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Environment",
+            ulEnvironment,
+            EAXREVERB_MINENVIRONMENT,
+            EAX30REVERB_MAXENVIRONMENT);
+    }
+}; // EnvironmentValidator1
+
+struct RoomLFValidator {
+    void operator()(long lRoomLF) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Room LF",
+            lRoomLF,
+            EAXREVERB_MINROOMLF,
+            EAXREVERB_MAXROOMLF);
+    }
+}; // RoomLFValidator
+
+struct DecayLFRatioValidator {
+    void operator()(float flDecayLFRatio) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Decay LF Ratio",
+            flDecayLFRatio,
+            EAXREVERB_MINDECAYLFRATIO,
+            EAXREVERB_MAXDECAYLFRATIO);
+    }
+}; // DecayLFRatioValidator
+
+struct VectorValidator {
+    void operator()(const EAXVECTOR&) const
+    {}
+}; // VectorValidator
+
+struct EchoTimeValidator {
+    void operator()(float flEchoTime) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Echo Time",
+            flEchoTime,
+            EAXREVERB_MINECHOTIME,
+            EAXREVERB_MAXECHOTIME);
+    }
+}; // EchoTimeValidator
+
+struct EchoDepthValidator {
+    void operator()(float flEchoDepth) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Echo Depth",
+            flEchoDepth,
+            EAXREVERB_MINECHODEPTH,
+            EAXREVERB_MAXECHODEPTH);
+    }
+}; // EchoDepthValidator
+
+struct ModulationTimeValidator {
+    void operator()(float flModulationTime) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Modulation Time",
+            flModulationTime,
+            EAXREVERB_MINMODULATIONTIME,
+            EAXREVERB_MAXMODULATIONTIME);
+    }
+}; // ModulationTimeValidator
+
+struct ModulationDepthValidator {
+    void operator()(float flModulationDepth) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Modulation Depth",
+            flModulationDepth,
+            EAXREVERB_MINMODULATIONDEPTH,
+            EAXREVERB_MAXMODULATIONDEPTH);
+    }
+}; // ModulationDepthValidator
+
+struct HFReferenceValidator {
+    void operator()(float flHFReference) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "HF Reference",
+            flHFReference,
+            EAXREVERB_MINHFREFERENCE,
+            EAXREVERB_MAXHFREFERENCE);
+    }
+}; // HFReferenceValidator
+
+struct LFReferenceValidator {
+    void operator()(float flLFReference) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "LF Reference",
+            flLFReference,
+            EAXREVERB_MINLFREFERENCE,
+            EAXREVERB_MAXLFREFERENCE);
+    }
+}; // LFReferenceValidator
+
+struct FlagsValidator3 {
+    void operator()(unsigned long ulFlags) const
+    {
+        eax_validate_range<EaxReverbEffectException>(
+            "Flags",
+            ulFlags,
+            0UL,
+            ~EAXREVERBFLAGS_RESERVED);
+    }
+}; // FlagsValidator3
+
+struct AllValidator3 {
+    void operator()(const EAXREVERBPROPERTIES& all) const
+    {
+        EnvironmentValidator3{}(all.ulEnvironment);
+        EnvironmentSizeValidator{}(all.flEnvironmentSize);
+        EnvironmentDiffusionValidator{}(all.flEnvironmentDiffusion);
+        RoomValidator{}(all.lRoom);
+        RoomHFValidator{}(all.lRoomHF);
+        RoomLFValidator{}(all.lRoomLF);
+        DecayTimeValidator{}(all.flDecayTime);
+        DecayHFRatioValidator{}(all.flDecayHFRatio);
+        DecayLFRatioValidator{}(all.flDecayLFRatio);
+        ReflectionsValidator{}(all.lReflections);
+        ReflectionsDelayValidator{}(all.flReflectionsDelay);
+        VectorValidator{}(all.vReflectionsPan);
+        ReverbValidator{}(all.lReverb);
+        ReverbDelayValidator{}(all.flReverbDelay);
+        VectorValidator{}(all.vReverbPan);
+        EchoTimeValidator{}(all.flEchoTime);
+        EchoDepthValidator{}(all.flEchoDepth);
+        ModulationTimeValidator{}(all.flModulationTime);
+        ModulationDepthValidator{}(all.flModulationDepth);
+        AirAbsorptionHFValidator{}(all.flAirAbsorptionHF);
+        HFReferenceValidator{}(all.flHFReference);
+        LFReferenceValidator{}(all.flLFReference);
+        RoomRolloffFactorValidator{}(all.flRoomRolloffFactor);
+        FlagsValidator3{}(all.ulFlags);
+    }
+}; // AllValidator3
+
+struct EnvironmentDeferrer2 {
+    void operator()(EAX20LISTENERPROPERTIES& props, unsigned long dwEnvironment) const
+    {
+        props = EAX2REVERB_PRESETS[dwEnvironment];
+    }
+}; // EnvironmentDeferrer2
+
+struct EnvironmentSizeDeferrer2 {
+    void operator()(EAX20LISTENERPROPERTIES& props, float flEnvironmentSize) const
+    {
+        if (props.flEnvironmentSize == flEnvironmentSize)
+        {
+            return;
+        }
+
+        const auto scale = flEnvironmentSize / props.flEnvironmentSize;
+        props.flEnvironmentSize = flEnvironmentSize;
+
+        if ((props.dwFlags & EAX2LISTENERFLAGS_DECAYTIMESCALE) != 0)
+        {
+            props.flDecayTime = clamp(
+                props.flDecayTime * scale,
+                EAXREVERB_MINDECAYTIME,
+                EAXREVERB_MAXDECAYTIME);
+        }
+
+        if ((props.dwFlags & EAX2LISTENERFLAGS_REFLECTIONSSCALE) != 0 &&
+            (props.dwFlags & EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE) != 0)
+        {
+            props.lReflections = clamp(
+                props.lReflections - static_cast<long>(gain_to_level_mb(scale)),
+                EAXREVERB_MINREFLECTIONS,
+                EAXREVERB_MAXREFLECTIONS);
+        }
+
+        if ((props.dwFlags & EAX2LISTENERFLAGS_REFLECTIONSDELAYSCALE) != 0)
+        {
+            props.flReflectionsDelay = clamp(
+                props.flReflectionsDelay * scale,
+                EAXREVERB_MINREFLECTIONSDELAY,
+                EAXREVERB_MAXREFLECTIONSDELAY);
+        }
+
+        if ((props.dwFlags & EAX2LISTENERFLAGS_REVERBSCALE) != 0)
+        {
+            const auto log_scalar = ((props.dwFlags & EAXREVERBFLAGS_DECAYTIMESCALE) != 0) ? 2'000.0F : 3'000.0F;
+
+            props.lReverb = clamp(
+                props.lReverb - static_cast<long>(std::log10(scale) * log_scalar),
+                EAXREVERB_MINREVERB,
+                EAXREVERB_MAXREVERB);
+        }
+
+        if ((props.dwFlags & EAX2LISTENERFLAGS_REVERBDELAYSCALE) != 0)
+        {
+            props.flReverbDelay = clamp(
+                props.flReverbDelay * scale,
+                EAXREVERB_MINREVERBDELAY,
+                EAXREVERB_MAXREVERBDELAY);
+        }
+    }
+}; // EnvironmentSizeDeferrer2
+
+struct EnvironmentDeferrer3 {
+    void operator()(EAXREVERBPROPERTIES& props, unsigned long ulEnvironment) const
+    {
+        if (ulEnvironment == EAX_ENVIRONMENT_UNDEFINED)
+        {
+            props.ulEnvironment = EAX_ENVIRONMENT_UNDEFINED;
+            return;
+        }
+
+        props = EAXREVERB_PRESETS[ulEnvironment];
+    }
+}; // EnvironmentDeferrer3
+
+struct EnvironmentSizeDeferrer3 {
+    void operator()(EAXREVERBPROPERTIES& props, float flEnvironmentSize) const
+    {
+        if (props.flEnvironmentSize == flEnvironmentSize)
+        {
+            return;
+        }
+
+        const auto scale = flEnvironmentSize / props.flEnvironmentSize;
+        props.ulEnvironment = EAX_ENVIRONMENT_UNDEFINED;
+        props.flEnvironmentSize = flEnvironmentSize;
+
+        if ((props.ulFlags & EAXREVERBFLAGS_DECAYTIMESCALE) != 0)
+        {
+            props.flDecayTime = clamp(
+                props.flDecayTime * scale,
+                EAXREVERB_MINDECAYTIME,
+                EAXREVERB_MAXDECAYTIME);
+        }
+
+        if ((props.ulFlags & EAXREVERBFLAGS_REFLECTIONSSCALE) != 0 &&
+            (props.ulFlags & EAXREVERBFLAGS_REFLECTIONSDELAYSCALE) != 0)
+        {
+            props.lReflections = clamp(
+                props.lReflections - static_cast<long>(gain_to_level_mb(scale)),
+                EAXREVERB_MINREFLECTIONS,
+                EAXREVERB_MAXREFLECTIONS);
+        }
+
+        if ((props.ulFlags & EAXREVERBFLAGS_REFLECTIONSDELAYSCALE) != 0)
+        {
+            props.flReflectionsDelay = clamp(
+                props.flReflectionsDelay * scale,
+                EAXREVERB_MINREFLECTIONSDELAY,
+                EAXREVERB_MAXREFLECTIONSDELAY);
+        }
+
+        if ((props.ulFlags & EAXREVERBFLAGS_REVERBSCALE) != 0)
+        {
+            const auto log_scalar = ((props.ulFlags & EAXREVERBFLAGS_DECAYTIMESCALE) != 0) ? 2'000.0F : 3'000.0F;
+            props.lReverb = clamp(
+                props.lReverb - static_cast<long>(std::log10(scale) * log_scalar),
+                EAXREVERB_MINREVERB,
+                EAXREVERB_MAXREVERB);
+        }
+
+        if ((props.ulFlags & EAXREVERBFLAGS_REVERBDELAYSCALE) != 0)
+        {
+            props.flReverbDelay = clamp(
+                props.flReverbDelay * scale,
+                EAXREVERB_MINREVERBDELAY,
+                EAXREVERB_MAXREVERBDELAY);
+        }
+
+        if ((props.ulFlags & EAXREVERBFLAGS_ECHOTIMESCALE) != 0)
+        {
+            props.flEchoTime = clamp(
+                props.flEchoTime * scale,
+                EAXREVERB_MINECHOTIME,
+                EAXREVERB_MAXECHOTIME);
+        }
+
+        if ((props.ulFlags & EAXREVERBFLAGS_MODULATIONTIMESCALE) != 0)
+        {
+            props.flModulationTime = clamp(
+                props.flModulationTime * scale,
+                EAXREVERB_MINMODULATIONTIME,
+                EAXREVERB_MAXMODULATIONTIME);
+        }
+    }
+}; // EnvironmentSizeDeferrer3
+
+} // namespace
+
+
+struct EaxReverbCommitter::Exception : public EaxReverbEffectException
+{
+    using EaxReverbEffectException::EaxReverbEffectException;
+};
+
+[[noreturn]] void EaxReverbCommitter::fail(const char* message)
+{
+    throw Exception{message};
+}
+
+void EaxReverbCommitter::translate(const EAX_REVERBPROPERTIES& src, EaxEffectProps& dst) noexcept
+{
+    assert(src.environment <= EAX1REVERB_MAXENVIRONMENT);
+    dst.mType = EaxEffectType::Reverb;
+    dst.mReverb = EAXREVERB_PRESETS[src.environment];
+    dst.mReverb.flDecayTime = src.fDecayTime_sec;
+    dst.mReverb.flDecayHFRatio = src.fDamping;
+    dst.mReverb.lReverb = mini(static_cast<int>(gain_to_level_mb(src.fVolume)), 0);
+}
+
+void EaxReverbCommitter::translate(const EAX20LISTENERPROPERTIES& src, EaxEffectProps& dst) noexcept
+{
+    assert(src.dwEnvironment <= EAX1REVERB_MAXENVIRONMENT);
+    const auto& env = EAXREVERB_PRESETS[src.dwEnvironment];
+    dst.mType = EaxEffectType::Reverb;
+    dst.mReverb.ulEnvironment = src.dwEnvironment;
+    dst.mReverb.flEnvironmentSize = src.flEnvironmentSize;
+    dst.mReverb.flEnvironmentDiffusion = src.flEnvironmentDiffusion;
+    dst.mReverb.lRoom = src.lRoom;
+    dst.mReverb.lRoomHF = src.lRoomHF;
+    dst.mReverb.lRoomLF = env.lRoomLF;
+    dst.mReverb.flDecayTime = src.flDecayTime;
+    dst.mReverb.flDecayHFRatio = src.flDecayHFRatio;
+    dst.mReverb.flDecayLFRatio = env.flDecayLFRatio;
+    dst.mReverb.lReflections = src.lReflections;
+    dst.mReverb.flReflectionsDelay = src.flReflectionsDelay;
+    dst.mReverb.vReflectionsPan = env.vReflectionsPan;
+    dst.mReverb.lReverb = src.lReverb;
+    dst.mReverb.flReverbDelay = src.flReverbDelay;
+    dst.mReverb.vReverbPan = env.vReverbPan;
+    dst.mReverb.flEchoTime = env.flEchoTime;
+    dst.mReverb.flEchoDepth = env.flEchoDepth;
+    dst.mReverb.flModulationTime = env.flModulationTime;
+    dst.mReverb.flModulationDepth = env.flModulationDepth;
+    dst.mReverb.flAirAbsorptionHF = src.flAirAbsorptionHF;
+    dst.mReverb.flHFReference = env.flHFReference;
+    dst.mReverb.flLFReference = env.flLFReference;
+    dst.mReverb.flRoomRolloffFactor = src.flRoomRolloffFactor;
+    dst.mReverb.ulFlags = src.dwFlags;
+}
+
+void EaxReverbCommitter::translate(const EAXREVERBPROPERTIES& src, EaxEffectProps& dst) noexcept
+{
+    dst.mType = EaxEffectType::Reverb;
+    dst.mReverb = src;
+}
+
+bool EaxReverbCommitter::commit(const EAX_REVERBPROPERTIES &props)
+{
+    EaxEffectProps dst{};
+    translate(props, dst);
+    return commit(dst);
+}
+
+bool EaxReverbCommitter::commit(const EAX20LISTENERPROPERTIES &props)
+{
+    EaxEffectProps dst{};
+    translate(props, dst);
+    return commit(dst);
+}
+
+bool EaxReverbCommitter::commit(const EAXREVERBPROPERTIES &props)
+{
+    EaxEffectProps dst{};
+    translate(props, dst);
+    return commit(dst);
+}
+
+bool EaxReverbCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType
+        && memcmp(&props.mReverb, &mEaxProps.mReverb, sizeof(mEaxProps.mReverb)) == 0)
+        return false;
+
+    mEaxProps = props;
+
+    const auto size = props.mReverb.flEnvironmentSize;
+    const auto density = (size * size * size) / 16.0F;
+    mAlProps.Reverb.Density = std::min(density, AL_EAXREVERB_MAX_DENSITY);
+    mAlProps.Reverb.Diffusion = props.mReverb.flEnvironmentDiffusion;
+    mAlProps.Reverb.Gain = level_mb_to_gain(static_cast<float>(props.mReverb.lRoom));
+    mAlProps.Reverb.GainHF = level_mb_to_gain(static_cast<float>(props.mReverb.lRoomHF));
+    mAlProps.Reverb.GainLF = level_mb_to_gain(static_cast<float>(props.mReverb.lRoomLF));
+    mAlProps.Reverb.DecayTime = props.mReverb.flDecayTime;
+    mAlProps.Reverb.DecayHFRatio = props.mReverb.flDecayHFRatio;
+    mAlProps.Reverb.DecayLFRatio = mEaxProps.mReverb.flDecayLFRatio;
+    mAlProps.Reverb.ReflectionsGain = level_mb_to_gain(static_cast<float>(props.mReverb.lReflections));
+    mAlProps.Reverb.ReflectionsDelay = props.mReverb.flReflectionsDelay;
+    mAlProps.Reverb.ReflectionsPan[0] = props.mReverb.vReflectionsPan.x;
+    mAlProps.Reverb.ReflectionsPan[1] = props.mReverb.vReflectionsPan.y;
+    mAlProps.Reverb.ReflectionsPan[2] = props.mReverb.vReflectionsPan.z;
+    mAlProps.Reverb.LateReverbGain = level_mb_to_gain(static_cast<float>(props.mReverb.lReverb));
+    mAlProps.Reverb.LateReverbDelay = props.mReverb.flReverbDelay;
+    mAlProps.Reverb.LateReverbPan[0] = props.mReverb.vReverbPan.x;
+    mAlProps.Reverb.LateReverbPan[1] = props.mReverb.vReverbPan.y;
+    mAlProps.Reverb.LateReverbPan[2] = props.mReverb.vReverbPan.z;
+    mAlProps.Reverb.EchoTime = props.mReverb.flEchoTime;
+    mAlProps.Reverb.EchoDepth = props.mReverb.flEchoDepth;
+    mAlProps.Reverb.ModulationTime = props.mReverb.flModulationTime;
+    mAlProps.Reverb.ModulationDepth = props.mReverb.flModulationDepth;
+    mAlProps.Reverb.AirAbsorptionGainHF = level_mb_to_gain(props.mReverb.flAirAbsorptionHF);
+    mAlProps.Reverb.HFReference = props.mReverb.flHFReference;
+    mAlProps.Reverb.LFReference = props.mReverb.flLFReference;
+    mAlProps.Reverb.RoomRolloffFactor = props.mReverb.flRoomRolloffFactor;
+    mAlProps.Reverb.DecayHFLimit = ((props.mReverb.ulFlags & EAXREVERBFLAGS_DECAYHFLIMIT) != 0);
+    return true;
+}
+
+void EaxReverbCommitter::SetDefaults(EAX_REVERBPROPERTIES &props)
+{
+    props = EAX1REVERB_PRESETS[EAX_ENVIRONMENT_GENERIC];
+}
+
+void EaxReverbCommitter::SetDefaults(EAX20LISTENERPROPERTIES &props)
+{
+    props = EAX2REVERB_PRESETS[EAX2_ENVIRONMENT_GENERIC];
+    props.lRoom = -10'000L;
+}
+
+void EaxReverbCommitter::SetDefaults(EAXREVERBPROPERTIES &props)
+{
+    props = EAXREVERB_PRESETS[EAX_ENVIRONMENT_GENERIC];
+}
+
+void EaxReverbCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::Reverb;
+    SetDefaults(props.mReverb);
+}
+
+
+void EaxReverbCommitter::Get(const EaxCall &call, const EAX_REVERBPROPERTIES &props)
+{
+    switch(call.get_property_id())
+    {
+    case DSPROPERTY_EAX_ALL: call.set_value<Exception>(props); break;
+    case DSPROPERTY_EAX_ENVIRONMENT: call.set_value<Exception>(props.environment); break;
+    case DSPROPERTY_EAX_VOLUME: call.set_value<Exception>(props.fVolume); break;
+    case DSPROPERTY_EAX_DECAYTIME: call.set_value<Exception>(props.fDecayTime_sec); break;
+    case DSPROPERTY_EAX_DAMPING: call.set_value<Exception>(props.fDamping); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+void EaxReverbCommitter::Get(const EaxCall &call, const EAX20LISTENERPROPERTIES &props)
+{
+    switch(call.get_property_id())
+    {
+    case DSPROPERTY_EAX20LISTENER_NONE: break;
+    case DSPROPERTY_EAX20LISTENER_ALLPARAMETERS: call.set_value<Exception>(props); break;
+    case DSPROPERTY_EAX20LISTENER_ROOM: call.set_value<Exception>(props.lRoom); break;
+    case DSPROPERTY_EAX20LISTENER_ROOMHF: call.set_value<Exception>(props.lRoomHF); break;
+    case DSPROPERTY_EAX20LISTENER_ROOMROLLOFFFACTOR: call.set_value<Exception>(props.flRoomRolloffFactor); break;
+    case DSPROPERTY_EAX20LISTENER_DECAYTIME: call.set_value<Exception>(props.flDecayTime); break;
+    case DSPROPERTY_EAX20LISTENER_DECAYHFRATIO: call.set_value<Exception>(props.flDecayHFRatio); break;
+    case DSPROPERTY_EAX20LISTENER_REFLECTIONS: call.set_value<Exception>(props.lReflections); break;
+    case DSPROPERTY_EAX20LISTENER_REFLECTIONSDELAY: call.set_value<Exception>(props.flReflectionsDelay); break;
+    case DSPROPERTY_EAX20LISTENER_REVERB: call.set_value<Exception>(props.lReverb); break;
+    case DSPROPERTY_EAX20LISTENER_REVERBDELAY: call.set_value<Exception>(props.flReverbDelay); break;
+    case DSPROPERTY_EAX20LISTENER_ENVIRONMENT: call.set_value<Exception>(props.dwEnvironment); break;
+    case DSPROPERTY_EAX20LISTENER_ENVIRONMENTSIZE: call.set_value<Exception>(props.flEnvironmentSize); break;
+    case DSPROPERTY_EAX20LISTENER_ENVIRONMENTDIFFUSION: call.set_value<Exception>(props.flEnvironmentDiffusion); break;
+    case DSPROPERTY_EAX20LISTENER_AIRABSORPTIONHF: call.set_value<Exception>(props.flAirAbsorptionHF); break;
+    case DSPROPERTY_EAX20LISTENER_FLAGS: call.set_value<Exception>(props.dwFlags); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+void EaxReverbCommitter::Get(const EaxCall &call, const EAXREVERBPROPERTIES &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXREVERB_NONE: break;
+    case EAXREVERB_ALLPARAMETERS: call.set_value<Exception>(props); break;
+    case EAXREVERB_ENVIRONMENT: call.set_value<Exception>(props.ulEnvironment); break;
+    case EAXREVERB_ENVIRONMENTSIZE: call.set_value<Exception>(props.flEnvironmentSize); break;
+    case EAXREVERB_ENVIRONMENTDIFFUSION: call.set_value<Exception>(props.flEnvironmentDiffusion); break;
+    case EAXREVERB_ROOM: call.set_value<Exception>(props.lRoom); break;
+    case EAXREVERB_ROOMHF: call.set_value<Exception>(props.lRoomHF); break;
+    case EAXREVERB_ROOMLF: call.set_value<Exception>(props.lRoomLF); break;
+    case EAXREVERB_DECAYTIME: call.set_value<Exception>(props.flDecayTime); break;
+    case EAXREVERB_DECAYHFRATIO: call.set_value<Exception>(props.flDecayHFRatio); break;
+    case EAXREVERB_DECAYLFRATIO: call.set_value<Exception>(props.flDecayLFRatio); break;
+    case EAXREVERB_REFLECTIONS: call.set_value<Exception>(props.lReflections); break;
+    case EAXREVERB_REFLECTIONSDELAY: call.set_value<Exception>(props.flReflectionsDelay); break;
+    case EAXREVERB_REFLECTIONSPAN: call.set_value<Exception>(props.vReflectionsPan); break;
+    case EAXREVERB_REVERB: call.set_value<Exception>(props.lReverb); break;
+    case EAXREVERB_REVERBDELAY: call.set_value<Exception>(props.flReverbDelay); break;
+    case EAXREVERB_REVERBPAN: call.set_value<Exception>(props.vReverbPan); break;
+    case EAXREVERB_ECHOTIME: call.set_value<Exception>(props.flEchoTime); break;
+    case EAXREVERB_ECHODEPTH: call.set_value<Exception>(props.flEchoDepth); break;
+    case EAXREVERB_MODULATIONTIME: call.set_value<Exception>(props.flModulationTime); break;
+    case EAXREVERB_MODULATIONDEPTH: call.set_value<Exception>(props.flModulationDepth); break;
+    case EAXREVERB_AIRABSORPTIONHF: call.set_value<Exception>(props.flAirAbsorptionHF); break;
+    case EAXREVERB_HFREFERENCE: call.set_value<Exception>(props.flHFReference); break;
+    case EAXREVERB_LFREFERENCE: call.set_value<Exception>(props.flLFReference); break;
+    case EAXREVERB_ROOMROLLOFFFACTOR: call.set_value<Exception>(props.flRoomRolloffFactor); break;
+    case EAXREVERB_FLAGS: call.set_value<Exception>(props.ulFlags); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+void EaxReverbCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    Get(call, props.mReverb);
+}
+
+
+void EaxReverbCommitter::Set(const EaxCall &call, EAX_REVERBPROPERTIES &props)
+{
+    switch(call.get_property_id())
+    {
+    case DSPROPERTY_EAX_ALL: defer<AllValidator1>(call, props); break;
+    case DSPROPERTY_EAX_ENVIRONMENT: defer<EnvironmentValidator1>(call, props.environment); break;
+    case DSPROPERTY_EAX_VOLUME: defer<VolumeValidator>(call, props.fVolume); break;
+    case DSPROPERTY_EAX_DECAYTIME: defer<DecayTimeValidator>(call, props.fDecayTime_sec); break;
+    case DSPROPERTY_EAX_DAMPING: defer<DampingValidator>(call, props.fDamping); break;
+    default: fail_unknown_property_id();
+    }
+}
+
+void EaxReverbCommitter::Set(const EaxCall &call, EAX20LISTENERPROPERTIES &props)
+{
+    switch(call.get_property_id())
+    {
+    case DSPROPERTY_EAX20LISTENER_NONE:
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_ALLPARAMETERS:
+        defer<AllValidator2>(call, props);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_ROOM:
+        defer<RoomValidator>(call, props.lRoom);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_ROOMHF:
+        defer<RoomHFValidator>(call, props.lRoomHF);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_ROOMROLLOFFFACTOR:
+        defer<RoomRolloffFactorValidator>(call, props.flRoomRolloffFactor);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_DECAYTIME:
+        defer<DecayTimeValidator>(call, props.flDecayTime);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_DECAYHFRATIO:
+        defer<DecayHFRatioValidator>(call, props.flDecayHFRatio);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_REFLECTIONS:
+        defer<ReflectionsValidator>(call, props.lReflections);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_REFLECTIONSDELAY:
+        defer<ReflectionsDelayValidator>(call, props.flReverbDelay);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_REVERB:
+        defer<ReverbValidator>(call, props.lReverb);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_REVERBDELAY:
+        defer<ReverbDelayValidator>(call, props.flReverbDelay);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_ENVIRONMENT:
+        defer<EnvironmentValidator1, EnvironmentDeferrer2>(call, props, props.dwEnvironment);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_ENVIRONMENTSIZE:
+        defer<EnvironmentSizeValidator, EnvironmentSizeDeferrer2>(call, props, props.flEnvironmentSize);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_ENVIRONMENTDIFFUSION:
+        defer<EnvironmentDiffusionValidator>(call, props.flEnvironmentDiffusion);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_AIRABSORPTIONHF:
+        defer<AirAbsorptionHFValidator>(call, props.flAirAbsorptionHF);
+        break;
+
+    case DSPROPERTY_EAX20LISTENER_FLAGS:
+        defer<FlagsValidator2>(call, props.dwFlags);
+        break;
+
+    default:
+        fail_unknown_property_id();
+    }
+}
+
+void EaxReverbCommitter::Set(const EaxCall &call, EAXREVERBPROPERTIES &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXREVERB_NONE:
+        break;
+
+    case EAXREVERB_ALLPARAMETERS:
+        defer<AllValidator3>(call, props);
+        break;
+
+    case EAXREVERB_ENVIRONMENT:
+        defer<EnvironmentValidator3, EnvironmentDeferrer3>(call, props, props.ulEnvironment);
+        break;
+
+    case EAXREVERB_ENVIRONMENTSIZE:
+        defer<EnvironmentSizeValidator, EnvironmentSizeDeferrer3>(call, props, props.flEnvironmentSize);
+        break;
+
+    case EAXREVERB_ENVIRONMENTDIFFUSION:
+        defer3<EnvironmentDiffusionValidator>(call, props, props.flEnvironmentDiffusion);
+        break;
+
+    case EAXREVERB_ROOM:
+        defer3<RoomValidator>(call, props, props.lRoom);
+        break;
+
+    case EAXREVERB_ROOMHF:
+        defer3<RoomHFValidator>(call, props, props.lRoomHF);
+        break;
+
+    case EAXREVERB_ROOMLF:
+        defer3<RoomLFValidator>(call, props, props.lRoomLF);
+        break;
+
+    case EAXREVERB_DECAYTIME:
+        defer3<DecayTimeValidator>(call, props, props.flDecayTime);
+        break;
+
+    case EAXREVERB_DECAYHFRATIO:
+        defer3<DecayHFRatioValidator>(call, props, props.flDecayHFRatio);
+        break;
+
+    case EAXREVERB_DECAYLFRATIO:
+        defer3<DecayLFRatioValidator>(call, props, props.flDecayLFRatio);
+        break;
+
+    case EAXREVERB_REFLECTIONS:
+        defer3<ReflectionsValidator>(call, props, props.lReflections);
+        break;
+
+    case EAXREVERB_REFLECTIONSDELAY:
+        defer3<ReflectionsDelayValidator>(call, props, props.flReflectionsDelay);
+        break;
+
+    case EAXREVERB_REFLECTIONSPAN:
+        defer3<VectorValidator>(call, props, props.vReflectionsPan);
+        break;
+
+    case EAXREVERB_REVERB:
+        defer3<ReverbValidator>(call, props, props.lReverb);
+        break;
+
+    case EAXREVERB_REVERBDELAY:
+        defer3<ReverbDelayValidator>(call, props, props.flReverbDelay);
+        break;
+
+    case EAXREVERB_REVERBPAN:
+        defer3<VectorValidator>(call, props, props.vReverbPan);
+        break;
+
+    case EAXREVERB_ECHOTIME:
+        defer3<EchoTimeValidator>(call, props, props.flEchoTime);
+        break;
+
+    case EAXREVERB_ECHODEPTH:
+        defer3<EchoDepthValidator>(call, props, props.flEchoDepth);
+        break;
+
+    case EAXREVERB_MODULATIONTIME:
+        defer3<ModulationTimeValidator>(call, props, props.flModulationTime);
+        break;
+
+    case EAXREVERB_MODULATIONDEPTH:
+        defer3<ModulationDepthValidator>(call, props, props.flModulationDepth);
+        break;
+
+    case EAXREVERB_AIRABSORPTIONHF:
+        defer3<AirAbsorptionHFValidator>(call, props, props.flAirAbsorptionHF);
+        break;
+
+    case EAXREVERB_HFREFERENCE:
+        defer3<HFReferenceValidator>(call, props, props.flHFReference);
+        break;
+
+    case EAXREVERB_LFREFERENCE:
+        defer3<LFReferenceValidator>(call, props, props.flLFReference);
+        break;
+
+    case EAXREVERB_ROOMROLLOFFFACTOR:
+        defer3<RoomRolloffFactorValidator>(call, props, props.flRoomRolloffFactor);
+        break;
+
+    case EAXREVERB_FLAGS:
+        defer3<FlagsValidator3>(call, props, props.ulFlags);
+        break;
+
+    default:
+        fail_unknown_property_id();
+    }
+}
+
+void EaxReverbCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    Set(call, props.mReverb);
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/effects/vmorpher.cpp b/al/effects/vmorpher.cpp
new file mode 100644 (file)
index 0000000..21ea368
--- /dev/null
@@ -0,0 +1,520 @@
+
+#include "config.h"
+
+#include <stdexcept>
+
+#include "AL/al.h"
+#include "AL/efx.h"
+
+#include "alc/effects/base.h"
+#include "aloptional.h"
+#include "effects.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+#include "alnumeric.h"
+#include "al/eax/exception.h"
+#include "al/eax/utils.h"
+#endif // ALSOFT_EAX
+
+
+namespace {
+
+al::optional<VMorpherPhenome> PhenomeFromEnum(ALenum val)
+{
+#define HANDLE_PHENOME(x) case AL_VOCAL_MORPHER_PHONEME_ ## x:                \
+    return VMorpherPhenome::x
+    switch(val)
+    {
+    HANDLE_PHENOME(A);
+    HANDLE_PHENOME(E);
+    HANDLE_PHENOME(I);
+    HANDLE_PHENOME(O);
+    HANDLE_PHENOME(U);
+    HANDLE_PHENOME(AA);
+    HANDLE_PHENOME(AE);
+    HANDLE_PHENOME(AH);
+    HANDLE_PHENOME(AO);
+    HANDLE_PHENOME(EH);
+    HANDLE_PHENOME(ER);
+    HANDLE_PHENOME(IH);
+    HANDLE_PHENOME(IY);
+    HANDLE_PHENOME(UH);
+    HANDLE_PHENOME(UW);
+    HANDLE_PHENOME(B);
+    HANDLE_PHENOME(D);
+    HANDLE_PHENOME(F);
+    HANDLE_PHENOME(G);
+    HANDLE_PHENOME(J);
+    HANDLE_PHENOME(K);
+    HANDLE_PHENOME(L);
+    HANDLE_PHENOME(M);
+    HANDLE_PHENOME(N);
+    HANDLE_PHENOME(P);
+    HANDLE_PHENOME(R);
+    HANDLE_PHENOME(S);
+    HANDLE_PHENOME(T);
+    HANDLE_PHENOME(V);
+    HANDLE_PHENOME(Z);
+    }
+    return al::nullopt;
+#undef HANDLE_PHENOME
+}
+ALenum EnumFromPhenome(VMorpherPhenome phenome)
+{
+#define HANDLE_PHENOME(x) case VMorpherPhenome::x: return AL_VOCAL_MORPHER_PHONEME_ ## x
+    switch(phenome)
+    {
+    HANDLE_PHENOME(A);
+    HANDLE_PHENOME(E);
+    HANDLE_PHENOME(I);
+    HANDLE_PHENOME(O);
+    HANDLE_PHENOME(U);
+    HANDLE_PHENOME(AA);
+    HANDLE_PHENOME(AE);
+    HANDLE_PHENOME(AH);
+    HANDLE_PHENOME(AO);
+    HANDLE_PHENOME(EH);
+    HANDLE_PHENOME(ER);
+    HANDLE_PHENOME(IH);
+    HANDLE_PHENOME(IY);
+    HANDLE_PHENOME(UH);
+    HANDLE_PHENOME(UW);
+    HANDLE_PHENOME(B);
+    HANDLE_PHENOME(D);
+    HANDLE_PHENOME(F);
+    HANDLE_PHENOME(G);
+    HANDLE_PHENOME(J);
+    HANDLE_PHENOME(K);
+    HANDLE_PHENOME(L);
+    HANDLE_PHENOME(M);
+    HANDLE_PHENOME(N);
+    HANDLE_PHENOME(P);
+    HANDLE_PHENOME(R);
+    HANDLE_PHENOME(S);
+    HANDLE_PHENOME(T);
+    HANDLE_PHENOME(V);
+    HANDLE_PHENOME(Z);
+    }
+    throw std::runtime_error{"Invalid phenome: "+std::to_string(static_cast<int>(phenome))};
+#undef HANDLE_PHENOME
+}
+
+al::optional<VMorpherWaveform> WaveformFromEmum(ALenum value)
+{
+    switch(value)
+    {
+    case AL_VOCAL_MORPHER_WAVEFORM_SINUSOID: return VMorpherWaveform::Sinusoid;
+    case AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE: return VMorpherWaveform::Triangle;
+    case AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH: return VMorpherWaveform::Sawtooth;
+    }
+    return al::nullopt;
+}
+ALenum EnumFromWaveform(VMorpherWaveform type)
+{
+    switch(type)
+    {
+    case VMorpherWaveform::Sinusoid: return AL_VOCAL_MORPHER_WAVEFORM_SINUSOID;
+    case VMorpherWaveform::Triangle: return AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE;
+    case VMorpherWaveform::Sawtooth: return AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH;
+    }
+    throw std::runtime_error{"Invalid vocal morpher waveform: " +
+        std::to_string(static_cast<int>(type))};
+}
+
+void Vmorpher_setParami(EffectProps *props, ALenum param, int val)
+{
+    switch(param)
+    {
+    case AL_VOCAL_MORPHER_PHONEMEA:
+        if(auto phenomeopt = PhenomeFromEnum(val))
+            props->Vmorpher.PhonemeA = *phenomeopt;
+        else
+            throw effect_exception{AL_INVALID_VALUE, "Vocal morpher phoneme-a out of range: 0x%04x", val};
+        break;
+
+    case AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING:
+        if(!(val >= AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING && val <= AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING))
+            throw effect_exception{AL_INVALID_VALUE, "Vocal morpher phoneme-a coarse tuning out of range"};
+        props->Vmorpher.PhonemeACoarseTuning = val;
+        break;
+
+    case AL_VOCAL_MORPHER_PHONEMEB:
+        if(auto phenomeopt = PhenomeFromEnum(val))
+            props->Vmorpher.PhonemeB = *phenomeopt;
+        else
+            throw effect_exception{AL_INVALID_VALUE, "Vocal morpher phoneme-b out of range: 0x%04x", val};
+        break;
+
+    case AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING:
+        if(!(val >= AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING && val <= AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING))
+            throw effect_exception{AL_INVALID_VALUE, "Vocal morpher phoneme-b coarse tuning out of range"};
+        props->Vmorpher.PhonemeBCoarseTuning = val;
+        break;
+
+    case AL_VOCAL_MORPHER_WAVEFORM:
+        if(auto formopt = WaveformFromEmum(val))
+            props->Vmorpher.Waveform = *formopt;
+        else
+            throw effect_exception{AL_INVALID_VALUE, "Vocal morpher waveform out of range: 0x%04x", val};
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid vocal morpher integer property 0x%04x",
+            param};
+    }
+}
+void Vmorpher_setParamiv(EffectProps*, ALenum param, const int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid vocal morpher integer-vector property 0x%04x",
+        param};
+}
+void Vmorpher_setParamf(EffectProps *props, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_VOCAL_MORPHER_RATE:
+        if(!(val >= AL_VOCAL_MORPHER_MIN_RATE && val <= AL_VOCAL_MORPHER_MAX_RATE))
+            throw effect_exception{AL_INVALID_VALUE, "Vocal morpher rate out of range"};
+        props->Vmorpher.Rate = val;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid vocal morpher float property 0x%04x",
+            param};
+    }
+}
+void Vmorpher_setParamfv(EffectProps *props, ALenum param, const float *vals)
+{ Vmorpher_setParamf(props, param, vals[0]); }
+
+void Vmorpher_getParami(const EffectProps *props, ALenum param, int* val)
+{
+    switch(param)
+    {
+    case AL_VOCAL_MORPHER_PHONEMEA:
+        *val = EnumFromPhenome(props->Vmorpher.PhonemeA);
+        break;
+
+    case AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING:
+        *val = props->Vmorpher.PhonemeACoarseTuning;
+        break;
+
+    case AL_VOCAL_MORPHER_PHONEMEB:
+        *val = EnumFromPhenome(props->Vmorpher.PhonemeB);
+        break;
+
+    case AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING:
+        *val = props->Vmorpher.PhonemeBCoarseTuning;
+        break;
+
+    case AL_VOCAL_MORPHER_WAVEFORM:
+        *val = EnumFromWaveform(props->Vmorpher.Waveform);
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid vocal morpher integer property 0x%04x",
+            param};
+    }
+}
+void Vmorpher_getParamiv(const EffectProps*, ALenum param, int*)
+{
+    throw effect_exception{AL_INVALID_ENUM, "Invalid vocal morpher integer-vector property 0x%04x",
+        param};
+}
+void Vmorpher_getParamf(const EffectProps *props, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_VOCAL_MORPHER_RATE:
+        *val = props->Vmorpher.Rate;
+        break;
+
+    default:
+        throw effect_exception{AL_INVALID_ENUM, "Invalid vocal morpher float property 0x%04x",
+            param};
+    }
+}
+void Vmorpher_getParamfv(const EffectProps *props, ALenum param, float *vals)
+{ Vmorpher_getParamf(props, param, vals); }
+
+EffectProps genDefaultProps() noexcept
+{
+    EffectProps props{};
+    props.Vmorpher.Rate                 = AL_VOCAL_MORPHER_DEFAULT_RATE;
+    props.Vmorpher.PhonemeA             = *PhenomeFromEnum(AL_VOCAL_MORPHER_DEFAULT_PHONEMEA);
+    props.Vmorpher.PhonemeB             = *PhenomeFromEnum(AL_VOCAL_MORPHER_DEFAULT_PHONEMEB);
+    props.Vmorpher.PhonemeACoarseTuning = AL_VOCAL_MORPHER_DEFAULT_PHONEMEA_COARSE_TUNING;
+    props.Vmorpher.PhonemeBCoarseTuning = AL_VOCAL_MORPHER_DEFAULT_PHONEMEB_COARSE_TUNING;
+    props.Vmorpher.Waveform             = *WaveformFromEmum(AL_VOCAL_MORPHER_DEFAULT_WAVEFORM);
+    return props;
+}
+
+} // namespace
+
+DEFINE_ALEFFECT_VTABLE(Vmorpher);
+
+const EffectProps VmorpherEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using VocalMorpherCommitter = EaxCommitter<EaxVocalMorpherCommitter>;
+
+struct PhonemeAValidator {
+    void operator()(unsigned long ulPhonemeA) const
+    {
+        eax_validate_range<VocalMorpherCommitter::Exception>(
+            "Phoneme A",
+            ulPhonemeA,
+            EAXVOCALMORPHER_MINPHONEMEA,
+            EAXVOCALMORPHER_MAXPHONEMEA);
+    }
+}; // PhonemeAValidator
+
+struct PhonemeACoarseTuningValidator {
+    void operator()(long lPhonemeACoarseTuning) const
+    {
+        eax_validate_range<VocalMorpherCommitter::Exception>(
+            "Phoneme A Coarse Tuning",
+            lPhonemeACoarseTuning,
+            EAXVOCALMORPHER_MINPHONEMEACOARSETUNING,
+            EAXVOCALMORPHER_MAXPHONEMEACOARSETUNING);
+    }
+}; // PhonemeACoarseTuningValidator
+
+struct PhonemeBValidator {
+    void operator()(unsigned long ulPhonemeB) const
+    {
+        eax_validate_range<VocalMorpherCommitter::Exception>(
+            "Phoneme B",
+            ulPhonemeB,
+            EAXVOCALMORPHER_MINPHONEMEB,
+            EAXVOCALMORPHER_MAXPHONEMEB);
+    }
+}; // PhonemeBValidator
+
+struct PhonemeBCoarseTuningValidator {
+    void operator()(long lPhonemeBCoarseTuning) const
+    {
+        eax_validate_range<VocalMorpherCommitter::Exception>(
+            "Phoneme B Coarse Tuning",
+            lPhonemeBCoarseTuning,
+            EAXVOCALMORPHER_MINPHONEMEBCOARSETUNING,
+            EAXVOCALMORPHER_MAXPHONEMEBCOARSETUNING);
+    }
+}; // PhonemeBCoarseTuningValidator
+
+struct WaveformValidator {
+    void operator()(unsigned long ulWaveform) const
+    {
+        eax_validate_range<VocalMorpherCommitter::Exception>(
+            "Waveform",
+            ulWaveform,
+            EAXVOCALMORPHER_MINWAVEFORM,
+            EAXVOCALMORPHER_MAXWAVEFORM);
+    }
+}; // WaveformValidator
+
+struct RateValidator {
+    void operator()(float flRate) const
+    {
+        eax_validate_range<VocalMorpherCommitter::Exception>(
+            "Rate",
+            flRate,
+            EAXVOCALMORPHER_MINRATE,
+            EAXVOCALMORPHER_MAXRATE);
+    }
+}; // RateValidator
+
+struct AllValidator {
+    void operator()(const EAXVOCALMORPHERPROPERTIES& all) const
+    {
+        PhonemeAValidator{}(all.ulPhonemeA);
+        PhonemeACoarseTuningValidator{}(all.lPhonemeACoarseTuning);
+        PhonemeBValidator{}(all.ulPhonemeB);
+        PhonemeBCoarseTuningValidator{}(all.lPhonemeBCoarseTuning);
+        WaveformValidator{}(all.ulWaveform);
+        RateValidator{}(all.flRate);
+    }
+}; // AllValidator
+
+} // namespace
+
+template<>
+struct VocalMorpherCommitter::Exception : public EaxException {
+    explicit Exception(const char *message) : EaxException{"EAX_VOCAL_MORPHER_EFFECT", message}
+    { }
+};
+
+template<>
+[[noreturn]] void VocalMorpherCommitter::fail(const char *message)
+{
+    throw Exception{message};
+}
+
+template<>
+bool VocalMorpherCommitter::commit(const EaxEffectProps &props)
+{
+    if(props.mType == mEaxProps.mType
+        && mEaxProps.mVocalMorpher.ulPhonemeA == props.mVocalMorpher.ulPhonemeA
+        && mEaxProps.mVocalMorpher.lPhonemeACoarseTuning == props.mVocalMorpher.lPhonemeACoarseTuning
+        && mEaxProps.mVocalMorpher.ulPhonemeB == props.mVocalMorpher.ulPhonemeB
+        && mEaxProps.mVocalMorpher.lPhonemeBCoarseTuning == props.mVocalMorpher.lPhonemeBCoarseTuning
+        && mEaxProps.mVocalMorpher.ulWaveform == props.mVocalMorpher.ulWaveform
+        && mEaxProps.mVocalMorpher.flRate == props.mVocalMorpher.flRate)
+        return false;
+
+    mEaxProps = props;
+
+    auto get_phoneme = [](unsigned long phoneme) noexcept
+    {
+#define HANDLE_PHENOME(x) case x: return VMorpherPhenome::x
+        switch(phoneme)
+        {
+        HANDLE_PHENOME(A);
+        HANDLE_PHENOME(E);
+        HANDLE_PHENOME(I);
+        HANDLE_PHENOME(O);
+        HANDLE_PHENOME(U);
+        HANDLE_PHENOME(AA);
+        HANDLE_PHENOME(AE);
+        HANDLE_PHENOME(AH);
+        HANDLE_PHENOME(AO);
+        HANDLE_PHENOME(EH);
+        HANDLE_PHENOME(ER);
+        HANDLE_PHENOME(IH);
+        HANDLE_PHENOME(IY);
+        HANDLE_PHENOME(UH);
+        HANDLE_PHENOME(UW);
+        HANDLE_PHENOME(B);
+        HANDLE_PHENOME(D);
+        HANDLE_PHENOME(F);
+        HANDLE_PHENOME(G);
+        HANDLE_PHENOME(J);
+        HANDLE_PHENOME(K);
+        HANDLE_PHENOME(L);
+        HANDLE_PHENOME(M);
+        HANDLE_PHENOME(N);
+        HANDLE_PHENOME(P);
+        HANDLE_PHENOME(R);
+        HANDLE_PHENOME(S);
+        HANDLE_PHENOME(T);
+        HANDLE_PHENOME(V);
+        HANDLE_PHENOME(Z);
+        }
+        return VMorpherPhenome::A;
+#undef HANDLE_PHENOME
+    };
+    auto get_waveform = [](unsigned long form) noexcept
+    {
+        if(form == EAX_VOCALMORPHER_SINUSOID) return VMorpherWaveform::Sinusoid;
+        if(form == EAX_VOCALMORPHER_TRIANGLE) return VMorpherWaveform::Triangle;
+        if(form == EAX_VOCALMORPHER_SAWTOOTH) return VMorpherWaveform::Sawtooth;
+        return VMorpherWaveform::Sinusoid;
+    };
+
+    mAlProps.Vmorpher.PhonemeA = get_phoneme(props.mVocalMorpher.ulPhonemeA);
+    mAlProps.Vmorpher.PhonemeACoarseTuning = static_cast<int>(props.mVocalMorpher.lPhonemeACoarseTuning);
+    mAlProps.Vmorpher.PhonemeB = get_phoneme(props.mVocalMorpher.ulPhonemeB);
+    mAlProps.Vmorpher.PhonemeBCoarseTuning = static_cast<int>(props.mVocalMorpher.lPhonemeBCoarseTuning);
+    mAlProps.Vmorpher.Waveform = get_waveform(props.mVocalMorpher.ulWaveform);
+    mAlProps.Vmorpher.Rate = props.mVocalMorpher.flRate;
+
+    return true;
+}
+
+template<>
+void VocalMorpherCommitter::SetDefaults(EaxEffectProps &props)
+{
+    props.mType = EaxEffectType::VocalMorpher;
+    props.mVocalMorpher.ulPhonemeA = EAXVOCALMORPHER_DEFAULTPHONEMEA;
+    props.mVocalMorpher.lPhonemeACoarseTuning = EAXVOCALMORPHER_DEFAULTPHONEMEACOARSETUNING;
+    props.mVocalMorpher.ulPhonemeB = EAXVOCALMORPHER_DEFAULTPHONEMEB;
+    props.mVocalMorpher.lPhonemeBCoarseTuning = EAXVOCALMORPHER_DEFAULTPHONEMEBCOARSETUNING;
+    props.mVocalMorpher.ulWaveform = EAXVOCALMORPHER_DEFAULTWAVEFORM;
+    props.mVocalMorpher.flRate = EAXVOCALMORPHER_DEFAULTRATE;
+}
+
+template<>
+void VocalMorpherCommitter::Get(const EaxCall &call, const EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXVOCALMORPHER_NONE:
+        break;
+
+    case EAXVOCALMORPHER_ALLPARAMETERS:
+        call.set_value<Exception>(props.mVocalMorpher);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEA:
+        call.set_value<Exception>(props.mVocalMorpher.ulPhonemeA);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEACOARSETUNING:
+        call.set_value<Exception>(props.mVocalMorpher.lPhonemeACoarseTuning);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEB:
+        call.set_value<Exception>(props.mVocalMorpher.ulPhonemeB);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEBCOARSETUNING:
+        call.set_value<Exception>(props.mVocalMorpher.lPhonemeBCoarseTuning);
+        break;
+
+    case EAXVOCALMORPHER_WAVEFORM:
+        call.set_value<Exception>(props.mVocalMorpher.ulWaveform);
+        break;
+
+    case EAXVOCALMORPHER_RATE:
+        call.set_value<Exception>(props.mVocalMorpher.flRate);
+        break;
+
+    default:
+        fail_unknown_property_id();
+    }
+}
+
+template<>
+void VocalMorpherCommitter::Set(const EaxCall &call, EaxEffectProps &props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXVOCALMORPHER_NONE:
+        break;
+
+    case EAXVOCALMORPHER_ALLPARAMETERS:
+        defer<AllValidator>(call, props.mVocalMorpher);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEA:
+        defer<PhonemeAValidator>(call, props.mVocalMorpher.ulPhonemeA);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEACOARSETUNING:
+        defer<PhonemeACoarseTuningValidator>(call, props.mVocalMorpher.lPhonemeACoarseTuning);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEB:
+        defer<PhonemeBValidator>(call, props.mVocalMorpher.ulPhonemeB);
+        break;
+
+    case EAXVOCALMORPHER_PHONEMEBCOARSETUNING:
+        defer<PhonemeBCoarseTuningValidator>(call, props.mVocalMorpher.lPhonemeBCoarseTuning);
+        break;
+
+    case EAXVOCALMORPHER_WAVEFORM:
+        defer<WaveformValidator>(call, props.mVocalMorpher.ulWaveform);
+        break;
+
+    case EAXVOCALMORPHER_RATE:
+        defer<RateValidator>(call, props.mVocalMorpher.flRate);
+        break;
+
+    default:
+        fail_unknown_property_id();
+    }
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/error.cpp b/al/error.cpp
new file mode 100644 (file)
index 0000000..afa7019
--- /dev/null
@@ -0,0 +1,106 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 1999-2000 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"
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#include <atomic>
+#include <csignal>
+#include <cstdarg>
+#include <cstdio>
+#include <cstring>
+#include <mutex>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+
+#include "alc/context.h"
+#include "almalloc.h"
+#include "core/except.h"
+#include "core/logging.h"
+#include "opthelpers.h"
+#include "vector.h"
+
+
+bool TrapALError{false};
+
+void ALCcontext::setError(ALenum errorCode, const char *msg, ...)
+{
+    auto message = al::vector<char>(256);
+
+    va_list args, args2;
+    va_start(args, msg);
+    va_copy(args2, args);
+    int msglen{std::vsnprintf(message.data(), message.size(), msg, args)};
+    if(msglen >= 0 && static_cast<size_t>(msglen) >= message.size())
+    {
+        message.resize(static_cast<size_t>(msglen) + 1u);
+        msglen = std::vsnprintf(message.data(), message.size(), msg, args2);
+    }
+    va_end(args2);
+    va_end(args);
+
+    if(msglen >= 0) msg = message.data();
+    else msg = "<internal error constructing message>";
+
+    WARN("Error generated on context %p, code 0x%04x, \"%s\"\n",
+        decltype(std::declval<void*>()){this}, errorCode, msg);
+    if(TrapALError)
+    {
+#ifdef _WIN32
+        /* DebugBreak will cause an exception if there is no debugger */
+        if(IsDebuggerPresent())
+            DebugBreak();
+#elif defined(SIGTRAP)
+        raise(SIGTRAP);
+#endif
+    }
+
+    ALenum curerr{AL_NO_ERROR};
+    mLastError.compare_exchange_strong(curerr, errorCode);
+}
+
+AL_API ALenum AL_APIENTRY alGetError(void)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY
+    {
+        static constexpr ALenum deferror{AL_INVALID_OPERATION};
+        WARN("Querying error state on null context (implicitly 0x%04x)\n", deferror);
+        if(TrapALError)
+        {
+#ifdef _WIN32
+            if(IsDebuggerPresent())
+                DebugBreak();
+#elif defined(SIGTRAP)
+            raise(SIGTRAP);
+#endif
+        }
+        return deferror;
+    }
+
+    return context->mLastError.exchange(AL_NO_ERROR);
+}
+END_API_FUNC
diff --git a/al/event.cpp b/al/event.cpp
new file mode 100644 (file)
index 0000000..1bc39d1
--- /dev/null
@@ -0,0 +1,215 @@
+
+#include "config.h"
+
+#include "event.h"
+
+#include <algorithm>
+#include <atomic>
+#include <cstring>
+#include <exception>
+#include <memory>
+#include <mutex>
+#include <new>
+#include <string>
+#include <thread>
+#include <utility>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+
+#include "albyte.h"
+#include "alc/context.h"
+#include "alc/effects/base.h"
+#include "alc/inprogext.h"
+#include "almalloc.h"
+#include "core/async_event.h"
+#include "core/except.h"
+#include "core/logging.h"
+#include "core/voice_change.h"
+#include "opthelpers.h"
+#include "ringbuffer.h"
+#include "threads.h"
+
+
+static int EventThread(ALCcontext *context)
+{
+    RingBuffer *ring{context->mAsyncEvents.get()};
+    bool quitnow{false};
+    while(!quitnow)
+    {
+        auto evt_data = ring->getReadVector().first;
+        if(evt_data.len == 0)
+        {
+            context->mEventSem.wait();
+            continue;
+        }
+
+        std::lock_guard<std::mutex> _{context->mEventCbLock};
+        do {
+            auto *evt_ptr = reinterpret_cast<AsyncEvent*>(evt_data.buf);
+            evt_data.buf += sizeof(AsyncEvent);
+            evt_data.len -= 1;
+
+            AsyncEvent evt{*evt_ptr};
+            al::destroy_at(evt_ptr);
+            ring->readAdvance(1);
+
+            quitnow = evt.EnumType == AsyncEvent::KillThread;
+            if(quitnow) UNLIKELY break;
+
+            if(evt.EnumType == AsyncEvent::ReleaseEffectState)
+            {
+                al::intrusive_ptr<EffectState>{evt.u.mEffectState};
+                continue;
+            }
+
+            auto enabledevts = context->mEnabledEvts.load(std::memory_order_acquire);
+            if(!context->mEventCb || !enabledevts.test(evt.EnumType))
+                continue;
+
+            if(evt.EnumType == AsyncEvent::SourceStateChange)
+            {
+                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 AsyncEvent::SrcState::Reset:
+                    msg += "AL_INITIAL";
+                    state = AL_INITIAL;
+                    break;
+                case AsyncEvent::SrcState::Stop:
+                    msg += "AL_STOPPED";
+                    state = AL_STOPPED;
+                    break;
+                case AsyncEvent::SrcState::Play:
+                    msg += "AL_PLAYING";
+                    state = AL_PLAYING;
+                    break;
+                case AsyncEvent::SrcState::Pause:
+                    msg += "AL_PAUSED";
+                    state = AL_PAUSED;
+                    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 == AsyncEvent::BufferCompleted)
+            {
+                std::string msg{std::to_string(evt.u.bufcomp.count)};
+                if(evt.u.bufcomp.count == 1) msg += " buffer completed";
+                else msg += " buffers completed";
+                context->mEventCb(AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT, evt.u.bufcomp.id,
+                    evt.u.bufcomp.count, static_cast<ALsizei>(msg.length()), msg.c_str(),
+                    context->mEventParam);
+            }
+            else if(evt.EnumType == AsyncEvent::Disconnected)
+            {
+                context->mEventCb(AL_EVENT_TYPE_DISCONNECTED_SOFT, 0, 0,
+                    static_cast<ALsizei>(strlen(evt.u.disconnect.msg)), evt.u.disconnect.msg,
+                    context->mEventParam);
+            }
+        } while(evt_data.len != 0);
+    }
+    return 0;
+}
+
+void StartEventThrd(ALCcontext *ctx)
+{
+    try {
+        ctx->mEventThread = std::thread{EventThread, ctx};
+    }
+    catch(std::exception& e) {
+        ERR("Failed to start event thread: %s\n", e.what());
+    }
+    catch(...) {
+        ERR("Failed to start event thread! Expect problems.\n");
+    }
+}
+
+void StopEventThrd(ALCcontext *ctx)
+{
+    RingBuffer *ring{ctx->mAsyncEvents.get()};
+    auto evt_data = ring->getWriteVector().first;
+    if(evt_data.len == 0)
+    {
+        do {
+            std::this_thread::yield();
+            evt_data = ring->getWriteVector().first;
+        } while(evt_data.len == 0);
+    }
+    al::construct_at(reinterpret_cast<AsyncEvent*>(evt_data.buf), AsyncEvent::KillThread);
+    ring->writeAdvance(1);
+
+    ctx->mEventSem.post();
+    if(ctx->mEventThread.joinable())
+        ctx->mEventThread.join();
+}
+
+AL_API void AL_APIENTRY alEventControlSOFT(ALsizei count, const ALenum *types, ALboolean enable)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(count < 0) context->setError(AL_INVALID_VALUE, "Controlling %d events", count);
+    if(count <= 0) return;
+    if(!types) return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    ContextBase::AsyncEventBitset flags{};
+    const ALenum *types_end = types+count;
+    auto bad_type = std::find_if_not(types, types_end,
+        [&flags](ALenum type) noexcept -> bool
+        {
+            if(type == AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT)
+                flags.set(AsyncEvent::BufferCompleted);
+            else if(type == AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT)
+                flags.set(AsyncEvent::SourceStateChange);
+            else if(type == AL_EVENT_TYPE_DISCONNECTED_SOFT)
+                flags.set(AsyncEvent::Disconnected);
+            else
+                return false;
+            return true;
+        }
+    );
+    if(bad_type != types_end)
+        return context->setError(AL_INVALID_ENUM, "Invalid event type 0x%04x", *bad_type);
+
+    if(enable)
+    {
+        auto enabledevts = context->mEnabledEvts.load(std::memory_order_relaxed);
+        while(context->mEnabledEvts.compare_exchange_weak(enabledevts, enabledevts|flags,
+            std::memory_order_acq_rel, std::memory_order_acquire) == 0)
+        {
+            /* enabledevts is (re-)filled with the current value on failure, so
+             * just try again.
+             */
+        }
+    }
+    else
+    {
+        auto enabledevts = context->mEnabledEvts.load(std::memory_order_relaxed);
+        while(context->mEnabledEvts.compare_exchange_weak(enabledevts, enabledevts&~flags,
+            std::memory_order_acq_rel, std::memory_order_acquire) == 0)
+        {
+        }
+        /* Wait to ensure the event handler sees the changed flags before
+         * returning.
+         */
+        std::lock_guard<std::mutex> _{context->mEventCbLock};
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alEventCallbackSOFT(ALEVENTPROCSOFT callback, void *userParam)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mEventCbLock};
+    context->mEventCb = callback;
+    context->mEventParam = userParam;
+}
+END_API_FUNC
diff --git a/al/event.h b/al/event.h
new file mode 100644 (file)
index 0000000..83513c5
--- /dev/null
@@ -0,0 +1,9 @@
+#ifndef AL_EVENT_H
+#define AL_EVENT_H
+
+struct ALCcontext;
+
+void StartEventThrd(ALCcontext *ctx);
+void StopEventThrd(ALCcontext *ctx);
+
+#endif
diff --git a/al/extension.cpp b/al/extension.cpp
new file mode 100644 (file)
index 0000000..3ead0af
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * 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 <cctype>
+#include <cstdlib>
+#include <cstring>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+
+#include "alc/context.h"
+#include "alstring.h"
+#include "core/except.h"
+#include "opthelpers.h"
+
+
+AL_API ALboolean AL_APIENTRY alIsExtensionPresent(const ALchar *extName)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return AL_FALSE;
+
+    if(!extName) UNLIKELY
+    {
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+        return AL_FALSE;
+    }
+
+    size_t len{strlen(extName)};
+    const char *ptr{context->mExtensionList};
+    while(ptr && *ptr)
+    {
+        if(al::strncasecmp(ptr, extName, len) == 0 && (ptr[len] == '\0' || isspace(ptr[len])))
+            return AL_TRUE;
+
+        if((ptr=strchr(ptr, ' ')) != nullptr)
+        {
+            do {
+                ++ptr;
+            } while(isspace(*ptr));
+        }
+    }
+
+    return AL_FALSE;
+}
+END_API_FUNC
+
+
+AL_API ALvoid* AL_APIENTRY alGetProcAddress(const ALchar *funcName)
+START_API_FUNC
+{
+    if(!funcName) return nullptr;
+    return alcGetProcAddress(nullptr, funcName);
+}
+END_API_FUNC
+
+AL_API ALenum AL_APIENTRY alGetEnumValue(const ALchar *enumName)
+START_API_FUNC
+{
+    if(!enumName) return static_cast<ALenum>(0);
+    return alcGetEnumValue(nullptr, enumName);
+}
+END_API_FUNC
diff --git a/al/filter.cpp b/al/filter.cpp
new file mode 100644 (file)
index 0000000..73efa01
--- /dev/null
@@ -0,0 +1,722 @@
+/**
+ * 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 "filter.h"
+
+#include <algorithm>
+#include <cstdarg>
+#include <cstdint>
+#include <cstdio>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <new>
+#include <numeric>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/efx.h"
+
+#include "albit.h"
+#include "alc/context.h"
+#include "alc/device.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "core/except.h"
+#include "opthelpers.h"
+#include "vector.h"
+
+
+namespace {
+
+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, ...);
+    ~filter_exception() override;
+
+    ALenum errorCode() const noexcept { return mErrorCode; }
+};
+
+filter_exception::filter_exception(ALenum code, const char* msg, ...) : mErrorCode{code}
+{
+    std::va_list args;
+    va_start(args, msg);
+    setMessage(msg, args);
+    va_end(args);
+}
+filter_exception::~filter_exception() = default;
+
+
+#define DEFINE_ALFILTER_VTABLE(T)                                  \
+const ALfilter::Vtable T##_vtable = {                              \
+    T##_setParami, T##_setParamiv, T##_setParamf, T##_setParamfv,  \
+    T##_getParami, T##_getParamiv, T##_getParamf, T##_getParamfv,  \
+}
+
+void ALlowpass_setParami(ALfilter*, ALenum param, int)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid low-pass integer property 0x%04x", param}; }
+void ALlowpass_setParamiv(ALfilter*, ALenum param, const int*)
+{
+    throw filter_exception{AL_INVALID_ENUM, "Invalid low-pass integer-vector property 0x%04x",
+        param};
+}
+void ALlowpass_setParamf(ALfilter *filter, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_LOWPASS_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;
+
+    case AL_LOWPASS_GAINHF:
+        if(!(val >= AL_LOWPASS_MIN_GAINHF && val <= AL_LOWPASS_MAX_GAINHF))
+            throw filter_exception{AL_INVALID_VALUE, "Low-pass gainhf %f out of range", val};
+        filter->GainHF = val;
+        break;
+
+    default:
+        throw filter_exception{AL_INVALID_ENUM, "Invalid low-pass float property 0x%04x", param};
+    }
+}
+void ALlowpass_setParamfv(ALfilter *filter, ALenum param, const float *vals)
+{ ALlowpass_setParamf(filter, param, vals[0]); }
+
+void ALlowpass_getParami(const ALfilter*, ALenum param, int*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid low-pass integer property 0x%04x", param}; }
+void ALlowpass_getParamiv(const ALfilter*, ALenum param, int*)
+{
+    throw filter_exception{AL_INVALID_ENUM, "Invalid low-pass integer-vector property 0x%04x",
+        param};
+}
+void ALlowpass_getParamf(const ALfilter *filter, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_LOWPASS_GAIN:
+        *val = filter->Gain;
+        break;
+
+    case AL_LOWPASS_GAINHF:
+        *val = filter->GainHF;
+        break;
+
+    default:
+        throw filter_exception{AL_INVALID_ENUM, "Invalid low-pass float property 0x%04x", param};
+    }
+}
+void ALlowpass_getParamfv(const ALfilter *filter, ALenum param, float *vals)
+{ ALlowpass_getParamf(filter, param, vals); }
+
+DEFINE_ALFILTER_VTABLE(ALlowpass);
+
+
+void ALhighpass_setParami(ALfilter*, ALenum param, int)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid high-pass integer property 0x%04x", param}; }
+void ALhighpass_setParamiv(ALfilter*, ALenum param, const int*)
+{
+    throw filter_exception{AL_INVALID_ENUM, "Invalid high-pass integer-vector property 0x%04x",
+        param};
+}
+void ALhighpass_setParamf(ALfilter *filter, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_HIGHPASS_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;
+
+    case AL_HIGHPASS_GAINLF:
+        if(!(val >= AL_HIGHPASS_MIN_GAINLF && val <= AL_HIGHPASS_MAX_GAINLF))
+            throw filter_exception{AL_INVALID_VALUE, "High-pass gainlf %f out of range", val};
+        filter->GainLF = val;
+        break;
+
+    default:
+        throw filter_exception{AL_INVALID_ENUM, "Invalid high-pass float property 0x%04x", param};
+    }
+}
+void ALhighpass_setParamfv(ALfilter *filter, ALenum param, const float *vals)
+{ ALhighpass_setParamf(filter, param, vals[0]); }
+
+void ALhighpass_getParami(const ALfilter*, ALenum param, int*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid high-pass integer property 0x%04x", param}; }
+void ALhighpass_getParamiv(const ALfilter*, ALenum param, int*)
+{
+    throw filter_exception{AL_INVALID_ENUM, "Invalid high-pass integer-vector property 0x%04x",
+        param};
+}
+void ALhighpass_getParamf(const ALfilter *filter, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_HIGHPASS_GAIN:
+        *val = filter->Gain;
+        break;
+
+    case AL_HIGHPASS_GAINLF:
+        *val = filter->GainLF;
+        break;
+
+    default:
+        throw filter_exception{AL_INVALID_ENUM, "Invalid high-pass float property 0x%04x", param};
+    }
+}
+void ALhighpass_getParamfv(const ALfilter *filter, ALenum param, float *vals)
+{ ALhighpass_getParamf(filter, param, vals); }
+
+DEFINE_ALFILTER_VTABLE(ALhighpass);
+
+
+void ALbandpass_setParami(ALfilter*, ALenum param, int)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid band-pass integer property 0x%04x", param}; }
+void ALbandpass_setParamiv(ALfilter*, ALenum param, const int*)
+{
+    throw filter_exception{AL_INVALID_ENUM, "Invalid band-pass integer-vector property 0x%04x",
+        param};
+}
+void ALbandpass_setParamf(ALfilter *filter, ALenum param, float val)
+{
+    switch(param)
+    {
+    case AL_BANDPASS_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;
+
+    case AL_BANDPASS_GAINHF:
+        if(!(val >= AL_BANDPASS_MIN_GAINHF && val <= AL_BANDPASS_MAX_GAINHF))
+            throw filter_exception{AL_INVALID_VALUE, "Band-pass gainhf %f out of range", val};
+        filter->GainHF = val;
+        break;
+
+    case AL_BANDPASS_GAINLF:
+        if(!(val >= AL_BANDPASS_MIN_GAINLF && val <= AL_BANDPASS_MAX_GAINLF))
+            throw filter_exception{AL_INVALID_VALUE, "Band-pass gainlf %f out of range", val};
+        filter->GainLF = val;
+        break;
+
+    default:
+        throw filter_exception{AL_INVALID_ENUM, "Invalid band-pass float property 0x%04x", param};
+    }
+}
+void ALbandpass_setParamfv(ALfilter *filter, ALenum param, const float *vals)
+{ ALbandpass_setParamf(filter, param, vals[0]); }
+
+void ALbandpass_getParami(const ALfilter*, ALenum param, int*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid band-pass integer property 0x%04x", param}; }
+void ALbandpass_getParamiv(const ALfilter*, ALenum param, int*)
+{
+    throw filter_exception{AL_INVALID_ENUM, "Invalid band-pass integer-vector property 0x%04x",
+        param};
+}
+void ALbandpass_getParamf(const ALfilter *filter, ALenum param, float *val)
+{
+    switch(param)
+    {
+    case AL_BANDPASS_GAIN:
+        *val = filter->Gain;
+        break;
+
+    case AL_BANDPASS_GAINHF:
+        *val = filter->GainHF;
+        break;
+
+    case AL_BANDPASS_GAINLF:
+        *val = filter->GainLF;
+        break;
+
+    default:
+        throw filter_exception{AL_INVALID_ENUM, "Invalid band-pass float property 0x%04x", param};
+    }
+}
+void ALbandpass_getParamfv(const ALfilter *filter, ALenum param, float *vals)
+{ ALbandpass_getParamf(filter, param, vals); }
+
+DEFINE_ALFILTER_VTABLE(ALbandpass);
+
+
+void ALnullfilter_setParami(ALfilter*, ALenum param, int)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+void ALnullfilter_setParamiv(ALfilter*, ALenum param, const int*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+void ALnullfilter_setParamf(ALfilter*, ALenum param, float)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+void ALnullfilter_setParamfv(ALfilter*, ALenum param, const float*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+
+void ALnullfilter_getParami(const ALfilter*, ALenum param, int*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+void ALnullfilter_getParamiv(const ALfilter*, ALenum param, int*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+void ALnullfilter_getParamf(const ALfilter*, ALenum param, float*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+void ALnullfilter_getParamfv(const ALfilter*, ALenum param, float*)
+{ throw filter_exception{AL_INVALID_ENUM, "Invalid null filter property 0x%04x", param}; }
+
+DEFINE_ALFILTER_VTABLE(ALnullfilter);
+
+
+void InitFilterParams(ALfilter *filter, ALenum type)
+{
+    if(type == AL_FILTER_LOWPASS)
+    {
+        filter->Gain = AL_LOWPASS_DEFAULT_GAIN;
+        filter->GainHF = AL_LOWPASS_DEFAULT_GAINHF;
+        filter->HFReference = LOWPASSFREQREF;
+        filter->GainLF = 1.0f;
+        filter->LFReference = HIGHPASSFREQREF;
+        filter->vtab = &ALlowpass_vtable;
+    }
+    else if(type == AL_FILTER_HIGHPASS)
+    {
+        filter->Gain = AL_HIGHPASS_DEFAULT_GAIN;
+        filter->GainHF = 1.0f;
+        filter->HFReference = LOWPASSFREQREF;
+        filter->GainLF = AL_HIGHPASS_DEFAULT_GAINLF;
+        filter->LFReference = HIGHPASSFREQREF;
+        filter->vtab = &ALhighpass_vtable;
+    }
+    else if(type == AL_FILTER_BANDPASS)
+    {
+        filter->Gain = AL_BANDPASS_DEFAULT_GAIN;
+        filter->GainHF = AL_BANDPASS_DEFAULT_GAINHF;
+        filter->HFReference = LOWPASSFREQREF;
+        filter->GainLF = AL_BANDPASS_DEFAULT_GAINLF;
+        filter->LFReference = HIGHPASSFREQREF;
+        filter->vtab = &ALbandpass_vtable;
+    }
+    else
+    {
+        filter->Gain = 1.0f;
+        filter->GainHF = 1.0f;
+        filter->HFReference = LOWPASSFREQREF;
+        filter->GainLF = 1.0f;
+        filter->LFReference = HIGHPASSFREQREF;
+        filter->vtab = &ALnullfilter_vtable;
+    }
+    filter->type = type;
+}
+
+bool EnsureFilters(ALCdevice *device, size_t needed)
+{
+    size_t count{std::accumulate(device->FilterList.cbegin(), device->FilterList.cend(), size_t{0},
+        [](size_t cur, const FilterSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<ALuint>(al::popcount(sublist.FreeMask)); })};
+
+    while(needed > count)
+    {
+        if(device->FilterList.size() >= 1<<25) UNLIKELY
+            return false;
+
+        device->FilterList.emplace_back();
+        auto sublist = device->FilterList.end() - 1;
+        sublist->FreeMask = ~0_u64;
+        sublist->Filters = static_cast<ALfilter*>(al_calloc(alignof(ALfilter), sizeof(ALfilter)*64));
+        if(!sublist->Filters) UNLIKELY
+        {
+            device->FilterList.pop_back();
+            return false;
+        }
+        count += 64;
+    }
+    return true;
+}
+
+
+ALfilter *AllocFilter(ALCdevice *device)
+{
+    auto sublist = std::find_if(device->FilterList.begin(), device->FilterList.end(),
+        [](const FilterSubList &entry) noexcept -> bool
+        { 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{al::construct_at(sublist->Filters + slidx)};
+    InitFilterParams(filter, AL_FILTER_NULL);
+
+    /* Add 1 to avoid filter ID 0. */
+    filter->id = ((lidx<<6) | slidx) + 1;
+
+    sublist->FreeMask &= ~(1_u64 << slidx);
+
+    return filter;
+}
+
+void FreeFilter(ALCdevice *device, ALfilter *filter)
+{
+    const ALuint id{filter->id - 1};
+    const size_t lidx{id >> 6};
+    const ALuint slidx{id & 0x3f};
+
+    al::destroy_at(filter);
+
+    device->FilterList[lidx].FreeMask |= 1_u64 << slidx;
+}
+
+
+inline ALfilter *LookupFilter(ALCdevice *device, ALuint id)
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= device->FilterList.size()) UNLIKELY
+        return nullptr;
+    FilterSubList &sublist = device->FilterList[lidx];
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Filters + slidx;
+}
+
+} // namespace
+
+AL_API void AL_APIENTRY alGenFilters(ALsizei n, ALuint *filters)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Generating %d filters", n);
+    if(n <= 0) UNLIKELY return;
+
+    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");
+        return;
+    }
+
+    if(n == 1) LIKELY
+    {
+        /* Special handling for the easy and normal case. */
+        ALfilter *filter{AllocFilter(device)};
+        if(filter) filters[0] = filter->id;
+    }
+    else
+    {
+        /* Store the allocated buffer IDs in a separate local list, to avoid
+         * modifying the user storage in case of failure.
+         */
+        al::vector<ALuint> ids;
+        ids.reserve(static_cast<ALuint>(n));
+        do {
+            ALfilter *filter{AllocFilter(device)};
+            ids.emplace_back(filter->id);
+        } while(--n);
+        std::copy(ids.begin(), ids.end(), filters);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDeleteFilters(ALsizei n, const ALuint *filters)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Deleting %d filters", n);
+    if(n <= 0) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    /* First try to find any filters that are invalid. */
+    auto validate_filter = [device](const ALuint fid) -> bool
+    { return !fid || LookupFilter(device, fid) != nullptr; };
+
+    const ALuint *filters_end = filters + n;
+    auto invflt = std::find_if_not(filters, filters_end, validate_filter);
+    if(invflt != filters_end) UNLIKELY
+    {
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", *invflt);
+        return;
+    }
+
+    /* All good. Delete non-0 filter IDs. */
+    auto delete_filter = [device](const ALuint fid) -> void
+    {
+        ALfilter *filter{fid ? LookupFilter(device, fid) : nullptr};
+        if(filter) FreeFilter(device, filter);
+    };
+    std::for_each(filters, filters_end, delete_filter);
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alIsFilter(ALuint filter)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(context) LIKELY
+    {
+        ALCdevice *device{context->mALDevice.get()};
+        std::lock_guard<std::mutex> _{device->FilterLock};
+        if(!filter || LookupFilter(device, filter))
+            return AL_TRUE;
+    }
+    return AL_FALSE;
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alFilteri(ALuint filter, ALenum param, ALint value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else
+    {
+        if(param == AL_FILTER_TYPE)
+        {
+            if(value == AL_FILTER_NULL || value == AL_FILTER_LOWPASS
+                || value == AL_FILTER_HIGHPASS || value == AL_FILTER_BANDPASS)
+                InitFilterParams(alfilt, value);
+            else
+                context->setError(AL_INVALID_VALUE, "Invalid filter type 0x%04x", value);
+        }
+        else try
+        {
+            /* Call the appropriate handler */
+            alfilt->setParami(param, value);
+        }
+        catch(filter_exception &e) {
+            context->setError(e.errorCode(), "%s", e.what());
+        }
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alFilteriv(ALuint filter, ALenum param, const ALint *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_FILTER_TYPE:
+        alFilteri(filter, param, values[0]);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else try
+    {
+        /* Call the appropriate handler */
+        alfilt->setParamiv(param, values);
+    }
+    catch(filter_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alFilterf(ALuint filter, ALenum param, ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else try
+    {
+        /* Call the appropriate handler */
+        alfilt->setParamf(param, value);
+    }
+    catch(filter_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alFilterfv(ALuint filter, ALenum param, const ALfloat *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else try
+    {
+        /* Call the appropriate handler */
+        alfilt->setParamfv(param, values);
+    }
+    catch(filter_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetFilteri(ALuint filter, ALenum param, ALint *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    const ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else
+    {
+        if(param == AL_FILTER_TYPE)
+            *value = alfilt->type;
+        else try
+        {
+            /* Call the appropriate handler */
+            alfilt->getParami(param, value);
+        }
+        catch(filter_exception &e) {
+            context->setError(e.errorCode(), "%s", e.what());
+        }
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetFilteriv(ALuint filter, ALenum param, ALint *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_FILTER_TYPE:
+        alGetFilteri(filter, param, values);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    const ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else try
+    {
+        /* Call the appropriate handler */
+        alfilt->getParamiv(param, values);
+    }
+    catch(filter_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetFilterf(ALuint filter, ALenum param, ALfloat *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    const ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else try
+    {
+        /* Call the appropriate handler */
+        alfilt->getParamf(param, value);
+    }
+    catch(filter_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetFilterfv(ALuint filter, ALenum param, ALfloat *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
+
+    const ALfilter *alfilt{LookupFilter(device, filter)};
+    if(!alfilt) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid filter ID %u", filter);
+    else try
+    {
+        /* Call the appropriate handler */
+        alfilt->getParamfv(param, values);
+    }
+    catch(filter_exception &e) {
+        context->setError(e.errorCode(), "%s", e.what());
+    }
+}
+END_API_FUNC
+
+
+FilterSubList::~FilterSubList()
+{
+    uint64_t usemask{~FreeMask};
+    while(usemask)
+    {
+        const int idx{al::countr_zero(usemask)};
+        al::destroy_at(Filters+idx);
+        usemask &= ~(1_u64 << idx);
+    }
+    FreeMask = ~usemask;
+    al_free(Filters);
+    Filters = nullptr;
+}
diff --git a/al/filter.h b/al/filter.h
new file mode 100644 (file)
index 0000000..65a9e30
--- /dev/null
@@ -0,0 +1,52 @@
+#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
+
+
+struct ALfilter {
+    ALenum type{AL_FILTER_NULL};
+
+    float Gain{1.0f};
+    float GainHF{1.0f};
+    float HFReference{LOWPASSFREQREF};
+    float GainLF{1.0f};
+    float LFReference{HIGHPASSFREQREF};
+
+    struct Vtable {
+        void (*const setParami )(ALfilter *filter, ALenum param, int val);
+        void (*const setParamiv)(ALfilter *filter, ALenum param, const int *vals);
+        void (*const setParamf )(ALfilter *filter, ALenum param, float val);
+        void (*const setParamfv)(ALfilter *filter, ALenum param, const float *vals);
+
+        void (*const getParami )(const ALfilter *filter, ALenum param, int *val);
+        void (*const getParamiv)(const ALfilter *filter, ALenum param, int *vals);
+        void (*const getParamf )(const ALfilter *filter, ALenum param, float *val);
+        void (*const getParamfv)(const ALfilter *filter, ALenum param, float *vals);
+    };
+    const Vtable *vtab{nullptr};
+
+    /* Self ID */
+    ALuint id{0};
+
+    void setParami(ALenum param, int value) { vtab->setParami(this, param, value); }
+    void setParamiv(ALenum param, const int *values) { vtab->setParamiv(this, param, values); }
+    void setParamf(ALenum param, float value) { vtab->setParamf(this, param, value); }
+    void setParamfv(ALenum param, const float *values) { vtab->setParamfv(this, param, values); }
+    void getParami(ALenum param, int *value) const { vtab->getParami(this, param, value); }
+    void getParamiv(ALenum param, int *values) const { vtab->getParamiv(this, param, values); }
+    void getParamf(ALenum param, float *value) const { vtab->getParamf(this, param, value); }
+    void getParamfv(ALenum param, float *values) const { vtab->getParamfv(this, param, values); }
+
+    DISABLE_ALLOC()
+};
+
+#endif
diff --git a/al/listener.cpp b/al/listener.cpp
new file mode 100644 (file)
index 0000000..06d7c37
--- /dev/null
@@ -0,0 +1,444 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 1999-2000 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 "listener.h"
+
+#include <cmath>
+#include <mutex>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/efx.h"
+
+#include "alc/context.h"
+#include "almalloc.h"
+#include "atomic.h"
+#include "core/except.h"
+#include "opthelpers.h"
+
+
+namespace {
+
+inline void UpdateProps(ALCcontext *context)
+{
+    if(!context->mDeferUpdates)
+    {
+        UpdateContextProps(context);
+        return;
+    }
+    context->mPropsDirty = true;
+}
+
+inline void CommitAndUpdateProps(ALCcontext *context)
+{
+    if(!context->mDeferUpdates)
+    {
+#ifdef ALSOFT_EAX
+        if(context->eaxNeedsCommit())
+        {
+            context->mPropsDirty = true;
+            context->applyAllUpdates();
+            return;
+        }
+#endif
+        UpdateContextProps(context);
+        return;
+    }
+    context->mPropsDirty = true;
+}
+
+} // namespace
+
+AL_API void AL_APIENTRY alListenerf(ALenum param, ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    switch(param)
+    {
+    case AL_GAIN:
+        if(!(value >= 0.0f && std::isfinite(value)))
+            return context->setError(AL_INVALID_VALUE, "Listener gain out of range");
+        listener.Gain = value;
+        UpdateProps(context.get());
+        break;
+
+    case AL_METERS_PER_UNIT:
+        if(!(value >= AL_MIN_METERS_PER_UNIT && value <= AL_MAX_METERS_PER_UNIT))
+            return context->setError(AL_INVALID_VALUE, "Listener meters per unit out of range");
+        listener.mMetersPerUnit = value;
+        UpdateProps(context.get());
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener float property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alListener3f(ALenum param, ALfloat value1, ALfloat value2, ALfloat value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    switch(param)
+    {
+    case AL_POSITION:
+        if(!(std::isfinite(value1) && std::isfinite(value2) && std::isfinite(value3)))
+            return context->setError(AL_INVALID_VALUE, "Listener position out of range");
+        listener.Position[0] = value1;
+        listener.Position[1] = value2;
+        listener.Position[2] = value3;
+        CommitAndUpdateProps(context.get());
+        break;
+
+    case AL_VELOCITY:
+        if(!(std::isfinite(value1) && std::isfinite(value2) && std::isfinite(value3)))
+            return context->setError(AL_INVALID_VALUE, "Listener velocity out of range");
+        listener.Velocity[0] = value1;
+        listener.Velocity[1] = value2;
+        listener.Velocity[2] = value3;
+        CommitAndUpdateProps(context.get());
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener 3-float property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alListenerfv(ALenum param, const ALfloat *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(param)
+        {
+        case AL_GAIN:
+        case AL_METERS_PER_UNIT:
+            alListenerf(param, values[0]);
+            return;
+
+        case AL_POSITION:
+        case AL_VELOCITY:
+            alListener3f(param, values[0], values[1], values[2]);
+            return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    switch(param)
+    {
+    case AL_ORIENTATION:
+        if(!(std::isfinite(values[0]) && std::isfinite(values[1]) && std::isfinite(values[2]) &&
+             std::isfinite(values[3]) && std::isfinite(values[4]) && std::isfinite(values[5])))
+            return context->setError(AL_INVALID_VALUE, "Listener orientation out of range");
+        /* AT then UP */
+        listener.OrientAt[0] = values[0];
+        listener.OrientAt[1] = values[1];
+        listener.OrientAt[2] = values[2];
+        listener.OrientUp[0] = values[3];
+        listener.OrientUp[1] = values[4];
+        listener.OrientUp[2] = values[5];
+        CommitAndUpdateProps(context.get());
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener float-vector property");
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alListeneri(ALenum param, ALint /*value*/)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener integer property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alListener3i(ALenum param, ALint value1, ALint value2, ALint value3)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_POSITION:
+    case AL_VELOCITY:
+        alListener3f(param, static_cast<ALfloat>(value1), static_cast<ALfloat>(value2),
+            static_cast<ALfloat>(value3));
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener 3-integer property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alListeneriv(ALenum param, const ALint *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        ALfloat fvals[6];
+        switch(param)
+        {
+        case AL_POSITION:
+        case AL_VELOCITY:
+            alListener3f(param, static_cast<ALfloat>(values[0]), static_cast<ALfloat>(values[1]),
+                static_cast<ALfloat>(values[2]));
+            return;
+
+        case AL_ORIENTATION:
+            fvals[0] = static_cast<ALfloat>(values[0]);
+            fvals[1] = static_cast<ALfloat>(values[1]);
+            fvals[2] = static_cast<ALfloat>(values[2]);
+            fvals[3] = static_cast<ALfloat>(values[3]);
+            fvals[4] = static_cast<ALfloat>(values[4]);
+            fvals[5] = static_cast<ALfloat>(values[5]);
+            alListenerfv(param, fvals);
+            return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    if(!values) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener integer-vector property");
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetListenerf(ALenum param, ALfloat *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    if(!value)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_GAIN:
+        *value = listener.Gain;
+        break;
+
+    case AL_METERS_PER_UNIT:
+        *value = listener.mMetersPerUnit;
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener float property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetListener3f(ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    if(!value1 || !value2 || !value3)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_POSITION:
+        *value1 = listener.Position[0];
+        *value2 = listener.Position[1];
+        *value3 = listener.Position[2];
+        break;
+
+    case AL_VELOCITY:
+        *value1 = listener.Velocity[0];
+        *value2 = listener.Velocity[1];
+        *value3 = listener.Velocity[2];
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener 3-float property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetListenerfv(ALenum param, ALfloat *values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_GAIN:
+    case AL_METERS_PER_UNIT:
+        alGetListenerf(param, values);
+        return;
+
+    case AL_POSITION:
+    case AL_VELOCITY:
+        alGetListener3f(param, values+0, values+1, values+2);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_ORIENTATION:
+        // AT then UP
+        values[0] = listener.OrientAt[0];
+        values[1] = listener.OrientAt[1];
+        values[2] = listener.OrientAt[2];
+        values[3] = listener.OrientUp[0];
+        values[4] = listener.OrientUp[1];
+        values[5] = listener.OrientUp[2];
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener float-vector property");
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetListeneri(ALenum param, ALint *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    if(!value)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener integer property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetListener3i(ALenum param, ALint *value1, ALint *value2, ALint *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    if(!value1 || !value2 || !value3)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_POSITION:
+        *value1 = static_cast<ALint>(listener.Position[0]);
+        *value2 = static_cast<ALint>(listener.Position[1]);
+        *value3 = static_cast<ALint>(listener.Position[2]);
+        break;
+
+    case AL_VELOCITY:
+        *value1 = static_cast<ALint>(listener.Velocity[0]);
+        *value2 = static_cast<ALint>(listener.Velocity[1]);
+        *value3 = static_cast<ALint>(listener.Velocity[2]);
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener 3-integer property");
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetListeneriv(ALenum param, ALint* values)
+START_API_FUNC
+{
+    switch(param)
+    {
+    case AL_POSITION:
+    case AL_VELOCITY:
+        alGetListener3i(param, values+0, values+1, values+2);
+        return;
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    ALlistener &listener = context->mListener;
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(param)
+    {
+    case AL_ORIENTATION:
+        // AT then UP
+        values[0] = static_cast<ALint>(listener.OrientAt[0]);
+        values[1] = static_cast<ALint>(listener.OrientAt[1]);
+        values[2] = static_cast<ALint>(listener.OrientAt[2]);
+        values[3] = static_cast<ALint>(listener.OrientUp[0]);
+        values[4] = static_cast<ALint>(listener.OrientUp[1]);
+        values[5] = static_cast<ALint>(listener.OrientUp[2]);
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, "Invalid listener integer-vector property");
+    }
+}
+END_API_FUNC
diff --git a/al/listener.h b/al/listener.h
new file mode 100644 (file)
index 0000000..8153287
--- /dev/null
@@ -0,0 +1,24 @@
+#ifndef AL_LISTENER_H
+#define AL_LISTENER_H
+
+#include <array>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/efx.h"
+
+#include "almalloc.h"
+
+
+struct ALlistener {
+    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> OrientAt{{0.0f, 0.0f, -1.0f}};
+    std::array<float,3> OrientUp{{0.0f, 1.0f, 0.0f}};
+    float Gain{1.0f};
+    float mMetersPerUnit{AL_DEFAULT_METERS_PER_UNIT};
+
+    DISABLE_ALLOC()
+};
+
+#endif
diff --git a/al/source.cpp b/al/source.cpp
new file mode 100644 (file)
index 0000000..cba3386
--- /dev/null
@@ -0,0 +1,5344 @@
+/**
+ * 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 "source.h"
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cassert>
+#include <chrono>
+#include <climits>
+#include <cmath>
+#include <cstdint>
+#include <functional>
+#include <inttypes.h>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <mutex>
+#include <new>
+#include <numeric>
+#include <stdexcept>
+#include <thread>
+#include <utility>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+#include "AL/efx.h"
+
+#include "albit.h"
+#include "alc/alu.h"
+#include "alc/backends/base.h"
+#include "alc/context.h"
+#include "alc/device.h"
+#include "alc/inprogext.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "atomic.h"
+#include "auxeffectslot.h"
+#include "buffer.h"
+#include "core/ambidefs.h"
+#include "core/bformatdec.h"
+#include "core/except.h"
+#include "core/filters/nfc.h"
+#include "core/filters/splitter.h"
+#include "core/logging.h"
+#include "core/voice_change.h"
+#include "event.h"
+#include "filter.h"
+#include "opthelpers.h"
+#include "ringbuffer.h"
+#include "threads.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+#endif // ALSOFT_EAX
+
+bool sBufferSubDataCompat{false};
+
+namespace {
+
+using namespace std::placeholders;
+using std::chrono::nanoseconds;
+
+Voice *GetSourceVoice(ALsource *source, ALCcontext *context)
+{
+    auto voicelist = context->getVoicesSpan();
+    ALuint idx{source->VoiceIdx};
+    if(idx < voicelist.size())
+    {
+        ALuint sid{source->id};
+        Voice *voice = voicelist[idx];
+        if(voice->mSourceID.load(std::memory_order_acquire) == sid)
+            return voice;
+    }
+    source->VoiceIdx = INVALID_VOICE_IDX;
+    return nullptr;
+}
+
+
+void UpdateSourceProps(const ALsource *source, Voice *voice, ALCcontext *context)
+{
+    /* Get an unused property container, or allocate a new one as needed. */
+    VoicePropsItem *props{context->mFreeVoiceProps.load(std::memory_order_acquire)};
+    if(!props)
+    {
+        context->allocVoiceProps();
+        props = context->mFreeVoiceProps.load(std::memory_order_acquire);
+    }
+    VoicePropsItem *next;
+    do {
+        next = props->next.load(std::memory_order_relaxed);
+    } while(context->mFreeVoiceProps.compare_exchange_weak(props, next,
+        std::memory_order_acq_rel, std::memory_order_acquire) == false);
+
+    props->Pitch = source->Pitch;
+    props->Gain = source->Gain;
+    props->OuterGain = source->OuterGain;
+    props->MinGain = source->MinGain;
+    props->MaxGain = source->MaxGain;
+    props->InnerAngle = source->InnerAngle;
+    props->OuterAngle = source->OuterAngle;
+    props->RefDistance = source->RefDistance;
+    props->MaxDistance = source->MaxDistance;
+    props->RolloffFactor = source->RolloffFactor
+#ifdef ALSOFT_EAX
+        + source->RolloffFactor2
+#endif
+    ;
+    props->Position = source->Position;
+    props->Velocity = source->Velocity;
+    props->Direction = source->Direction;
+    props->OrientAt = source->OrientAt;
+    props->OrientUp = source->OrientUp;
+    props->HeadRelative = source->HeadRelative;
+    props->mDistanceModel = source->mDistanceModel;
+    props->mResampler = source->mResampler;
+    props->DirectChannels = source->DirectChannels;
+    props->mSpatializeMode = source->mSpatialize;
+
+    props->DryGainHFAuto = source->DryGainHFAuto;
+    props->WetGainAuto = source->WetGainAuto;
+    props->WetGainHFAuto = source->WetGainHFAuto;
+    props->OuterGainHF = source->OuterGainHF;
+
+    props->AirAbsorptionFactor = source->AirAbsorptionFactor;
+    props->RoomRolloffFactor = source->RoomRolloffFactor;
+    props->DopplerFactor = source->DopplerFactor;
+
+    props->StereoPan = source->StereoPan;
+
+    props->Radius = source->Radius;
+    props->EnhWidth = source->EnhWidth;
+
+    props->Direct.Gain = source->Direct.Gain;
+    props->Direct.GainHF = source->Direct.GainHF;
+    props->Direct.HFReference = source->Direct.HFReference;
+    props->Direct.GainLF = source->Direct.GainLF;
+    props->Direct.LFReference = source->Direct.LFReference;
+
+    auto copy_send = [](const ALsource::SendData &srcsend) noexcept -> VoiceProps::SendData
+    {
+        VoiceProps::SendData ret{};
+        ret.Slot = srcsend.Slot ? srcsend.Slot->mSlot : nullptr;
+        ret.Gain = srcsend.Gain;
+        ret.GainHF = srcsend.GainHF;
+        ret.HFReference = srcsend.HFReference;
+        ret.GainLF = srcsend.GainLF;
+        ret.LFReference = srcsend.LFReference;
+        return ret;
+    };
+    std::transform(source->Send.cbegin(), source->Send.cend(), props->Send, copy_send);
+    if(!props->Send[0].Slot && context->mDefaultSlot)
+        props->Send[0].Slot = context->mDefaultSlot->mSlot;
+
+    /* Set the new container for updating internal parameters. */
+    props = voice->mUpdate.exchange(props, std::memory_order_acq_rel);
+    if(props)
+    {
+        /* If there was an unused update container, put it back in the
+         * freelist.
+         */
+        AtomicReplaceHead(context->mFreeVoiceProps, props);
+    }
+}
+
+/* GetSourceSampleOffset
+ *
+ * Gets the current read offset for the given Source, in 32.32 fixed-point
+ * samples. The offset is relative to the start of the queue (not the start of
+ * the current buffer).
+ */
+int64_t GetSourceSampleOffset(ALsource *Source, ALCcontext *context, nanoseconds *clocktime)
+{
+    ALCdevice *device{context->mALDevice.get()};
+    const VoiceBufferItem *Current{};
+    int64_t readPos{};
+    uint refcount{};
+    Voice *voice{};
+
+    do {
+        refcount = device->waitForMix();
+        *clocktime = GetDeviceClockTime(device);
+        voice = GetSourceVoice(Source, context);
+        if(voice)
+        {
+            Current = voice->mCurrentBuffer.load(std::memory_order_relaxed);
+
+            readPos  = int64_t{voice->mPosition.load(std::memory_order_relaxed)} << MixerFracBits;
+            readPos += voice->mPositionFrac.load(std::memory_order_relaxed);
+        }
+        std::atomic_thread_fence(std::memory_order_acquire);
+    } while(refcount != device->MixCount.load(std::memory_order_relaxed));
+
+    if(!voice)
+        return 0;
+
+    for(auto &item : Source->mQueue)
+    {
+        if(&item == Current) break;
+        readPos += int64_t{item.mSampleLen} << MixerFracBits;
+    }
+    if(readPos > std::numeric_limits<int64_t>::max() >> (32-MixerFracBits))
+        return std::numeric_limits<int64_t>::max();
+    return readPos << (32-MixerFracBits);
+}
+
+/* GetSourceSecOffset
+ *
+ * Gets the current read offset for the given Source, in seconds. The offset is
+ * relative to the start of the queue (not the start of the current buffer).
+ */
+double GetSourceSecOffset(ALsource *Source, ALCcontext *context, nanoseconds *clocktime)
+{
+    ALCdevice *device{context->mALDevice.get()};
+    const VoiceBufferItem *Current{};
+    int64_t readPos{};
+    uint refcount{};
+    Voice *voice{};
+
+    do {
+        refcount = device->waitForMix();
+        *clocktime = GetDeviceClockTime(device);
+        voice = GetSourceVoice(Source, context);
+        if(voice)
+        {
+            Current = voice->mCurrentBuffer.load(std::memory_order_relaxed);
+
+            readPos  = int64_t{voice->mPosition.load(std::memory_order_relaxed)} << MixerFracBits;
+            readPos += voice->mPositionFrac.load(std::memory_order_relaxed);
+        }
+        std::atomic_thread_fence(std::memory_order_acquire);
+    } while(refcount != device->MixCount.load(std::memory_order_relaxed));
+
+    if(!voice)
+        return 0.0f;
+
+    const ALbuffer *BufferFmt{nullptr};
+    auto BufferList = Source->mQueue.cbegin();
+    while(BufferList != Source->mQueue.cend() && al::to_address(BufferList) != Current)
+    {
+        if(!BufferFmt) BufferFmt = BufferList->mBuffer;
+        readPos += int64_t{BufferList->mSampleLen} << MixerFracBits;
+        ++BufferList;
+    }
+    while(BufferList != Source->mQueue.cend() && !BufferFmt)
+    {
+        BufferFmt = BufferList->mBuffer;
+        ++BufferList;
+    }
+    ASSUME(BufferFmt != nullptr);
+
+    return static_cast<double>(readPos) / double{MixerFracOne} / BufferFmt->mSampleRate;
+}
+
+/* GetSourceOffset
+ *
+ * Gets the current read offset for the given Source, in the appropriate format
+ * (Bytes, Samples or Seconds). The offset is relative to the start of the
+ * queue (not the start of the current buffer).
+ */
+double GetSourceOffset(ALsource *Source, ALenum name, ALCcontext *context)
+{
+    ALCdevice *device{context->mALDevice.get()};
+    const VoiceBufferItem *Current{};
+    int64_t readPos{};
+    uint readPosFrac{};
+    uint refcount;
+    Voice *voice;
+
+    do {
+        refcount = device->waitForMix();
+        voice = GetSourceVoice(Source, context);
+        if(voice)
+        {
+            Current = voice->mCurrentBuffer.load(std::memory_order_relaxed);
+
+            readPos = voice->mPosition.load(std::memory_order_relaxed);
+            readPosFrac = voice->mPositionFrac.load(std::memory_order_relaxed);
+        }
+        std::atomic_thread_fence(std::memory_order_acquire);
+    } while(refcount != device->MixCount.load(std::memory_order_relaxed));
+
+    if(!voice)
+        return 0.0;
+
+    const ALbuffer *BufferFmt{nullptr};
+    auto BufferList = Source->mQueue.cbegin();
+    while(BufferList != Source->mQueue.cend() && al::to_address(BufferList) != Current)
+    {
+        if(!BufferFmt) BufferFmt = BufferList->mBuffer;
+        readPos += BufferList->mSampleLen;
+        ++BufferList;
+    }
+    while(BufferList != Source->mQueue.cend() && !BufferFmt)
+    {
+        BufferFmt = BufferList->mBuffer;
+        ++BufferList;
+    }
+    ASSUME(BufferFmt != nullptr);
+
+    double offset{};
+    switch(name)
+    {
+    case AL_SEC_OFFSET:
+        offset  = static_cast<double>(readPos) + readPosFrac/double{MixerFracOne};
+        offset /= BufferFmt->mSampleRate;
+        break;
+
+    case AL_SAMPLE_OFFSET:
+        offset = static_cast<double>(readPos) + readPosFrac/double{MixerFracOne};
+        break;
+
+    case AL_BYTE_OFFSET:
+        const ALuint BlockSamples{BufferFmt->mBlockAlign};
+        const ALuint BlockSize{BufferFmt->blockSizeFromFmt()};
+
+        /* Round down to the block boundary. */
+        offset = static_cast<double>(readPos / BlockSamples) * BlockSize;
+        break;
+    }
+    return offset;
+}
+
+/* GetSourceLength
+ *
+ * Gets the length of the given Source's buffer queue, in the appropriate
+ * format (Bytes, Samples or Seconds).
+ */
+double GetSourceLength(const ALsource *source, ALenum name)
+{
+    uint64_t length{0};
+    const ALbuffer *BufferFmt{nullptr};
+    for(auto &listitem : source->mQueue)
+    {
+        if(!BufferFmt)
+            BufferFmt = listitem.mBuffer;
+        length += listitem.mSampleLen;
+    }
+    if(length == 0)
+        return 0.0;
+
+    ASSUME(BufferFmt != nullptr);
+    switch(name)
+    {
+    case AL_SEC_LENGTH_SOFT:
+        return static_cast<double>(length) / BufferFmt->mSampleRate;
+
+    case AL_SAMPLE_LENGTH_SOFT:
+        return static_cast<double>(length);
+
+    case AL_BYTE_LENGTH_SOFT:
+        const ALuint BlockSamples{BufferFmt->mBlockAlign};
+        const ALuint BlockSize{BufferFmt->blockSizeFromFmt()};
+
+        /* Round down to the block boundary. */
+        return static_cast<double>(length / BlockSamples) * BlockSize;
+    }
+    return 0.0;
+}
+
+
+struct VoicePos {
+    int pos;
+    uint frac;
+    ALbufferQueueItem *bufferitem;
+};
+
+/**
+ * GetSampleOffset
+ *
+ * Retrieves the voice position, fixed-point fraction, and bufferlist item
+ * using the givem offset type and offset. If the offset is out of range,
+ * returns an empty optional.
+ */
+al::optional<VoicePos> GetSampleOffset(al::deque<ALbufferQueueItem> &BufferList, ALenum OffsetType,
+    double Offset)
+{
+    /* Find the first valid Buffer in the Queue */
+    const ALbuffer *BufferFmt{nullptr};
+    for(auto &item : BufferList)
+    {
+        BufferFmt = item.mBuffer;
+        if(BufferFmt) break;
+    }
+    if(!BufferFmt) UNLIKELY
+        return al::nullopt;
+
+    /* Get sample frame offset */
+    int64_t offset{};
+    uint frac{};
+    double dbloff, dblfrac;
+    switch(OffsetType)
+    {
+    case AL_SEC_OFFSET:
+        dblfrac = std::modf(Offset*BufferFmt->mSampleRate, &dbloff);
+        if(dblfrac < 0.0)
+        {
+            /* If there's a negative fraction, reduce the offset to "floor" it,
+             * and convert the fraction to a percentage to the next value (e.g.
+             * -2.75 -> -3 + 0.25).
+             */
+            dbloff -= 1.0;
+            dblfrac += 1.0;
+        }
+        offset = static_cast<int64_t>(dbloff);
+        frac = static_cast<uint>(mind(dblfrac*MixerFracOne, MixerFracOne-1.0));
+        break;
+
+    case AL_SAMPLE_OFFSET:
+        dblfrac = std::modf(Offset, &dbloff);
+        if(dblfrac < 0.0)
+        {
+            dbloff -= 1.0;
+            dblfrac += 1.0;
+        }
+        offset = static_cast<int64_t>(dbloff);
+        frac = static_cast<uint>(mind(dblfrac*MixerFracOne, MixerFracOne-1.0));
+        break;
+
+    case AL_BYTE_OFFSET:
+        /* Determine the ByteOffset (and ensure it is block aligned) */
+        Offset = std::floor(Offset / BufferFmt->blockSizeFromFmt());
+        offset = static_cast<int64_t>(Offset) * BufferFmt->mBlockAlign;
+        frac = 0;
+        break;
+    }
+
+    /* Find the bufferlist item this offset belongs to. */
+    if(offset < 0)
+    {
+        if(offset < std::numeric_limits<int>::min())
+            return al::nullopt;
+        return VoicePos{static_cast<int>(offset), frac, &BufferList.front()};
+    }
+
+    if(BufferFmt->mCallback)
+        return al::nullopt;
+
+    int64_t totalBufferLen{0};
+    for(auto &item : BufferList)
+    {
+        if(totalBufferLen > offset)
+            break;
+        if(item.mSampleLen > offset-totalBufferLen)
+        {
+            /* Offset is in this buffer */
+            return VoicePos{static_cast<int>(offset-totalBufferLen), frac, &item};
+        }
+        totalBufferLen += item.mSampleLen;
+    }
+
+    /* Offset is out of range of the queue */
+    return al::nullopt;
+}
+
+
+void InitVoice(Voice *voice, ALsource *source, ALbufferQueueItem *BufferList, ALCcontext *context,
+    ALCdevice *device)
+{
+    voice->mLoopBuffer.store(source->Looping ? &source->mQueue.front() : nullptr,
+        std::memory_order_relaxed);
+
+    ALbuffer *buffer{BufferList->mBuffer};
+    voice->mFrequency = buffer->mSampleRate;
+    voice->mFmtChannels =
+        (buffer->mChannels == FmtStereo && source->mStereoMode == SourceStereo::Enhanced) ?
+        FmtSuperStereo : buffer->mChannels;
+    voice->mFmtType = buffer->mType;
+    voice->mFrameStep = buffer->channelsFromFmt();
+    voice->mBytesPerBlock = buffer->blockSizeFromFmt();
+    voice->mSamplesPerBlock = buffer->mBlockAlign;
+    voice->mAmbiLayout = IsUHJ(voice->mFmtChannels) ? AmbiLayout::FuMa : buffer->mAmbiLayout;
+    voice->mAmbiScaling = IsUHJ(voice->mFmtChannels) ? AmbiScaling::UHJ : buffer->mAmbiScaling;
+    voice->mAmbiOrder = (voice->mFmtChannels == FmtSuperStereo) ? 1 : buffer->mAmbiOrder;
+
+    if(buffer->mCallback) voice->mFlags.set(VoiceIsCallback);
+    else if(source->SourceType == AL_STATIC) voice->mFlags.set(VoiceIsStatic);
+    voice->mNumCallbackBlocks = 0;
+    voice->mCallbackBlockBase = 0;
+
+    voice->prepare(device);
+
+    source->mPropsDirty = false;
+    UpdateSourceProps(source, voice, context);
+
+    voice->mSourceID.store(source->id, std::memory_order_release);
+}
+
+
+VoiceChange *GetVoiceChanger(ALCcontext *ctx)
+{
+    VoiceChange *vchg{ctx->mVoiceChangeTail};
+    if(vchg == ctx->mCurrentVoiceChange.load(std::memory_order_acquire)) UNLIKELY
+    {
+        ctx->allocVoiceChanges();
+        vchg = ctx->mVoiceChangeTail;
+    }
+
+    ctx->mVoiceChangeTail = vchg->mNext.exchange(nullptr, std::memory_order_relaxed);
+
+    return vchg;
+}
+
+void SendVoiceChanges(ALCcontext *ctx, VoiceChange *tail)
+{
+    ALCdevice *device{ctx->mALDevice.get()};
+
+    VoiceChange *oldhead{ctx->mCurrentVoiceChange.load(std::memory_order_acquire)};
+    while(VoiceChange *next{oldhead->mNext.load(std::memory_order_relaxed)})
+        oldhead = next;
+    oldhead->mNext.store(tail, std::memory_order_release);
+
+    const bool connected{device->Connected.load(std::memory_order_acquire)};
+    device->waitForMix();
+    if(!connected) UNLIKELY
+    {
+        if(ctx->mStopVoicesOnDisconnect.load(std::memory_order_acquire))
+        {
+            /* If the device is disconnected and voices are stopped, just
+             * ignore all pending changes.
+             */
+            VoiceChange *cur{ctx->mCurrentVoiceChange.load(std::memory_order_acquire)};
+            while(VoiceChange *next{cur->mNext.load(std::memory_order_acquire)})
+            {
+                cur = next;
+                if(Voice *voice{cur->mVoice})
+                    voice->mSourceID.store(0, std::memory_order_relaxed);
+            }
+            ctx->mCurrentVoiceChange.store(cur, std::memory_order_release);
+        }
+    }
+}
+
+
+bool SetVoiceOffset(Voice *oldvoice, const VoicePos &vpos, ALsource *source, ALCcontext *context,
+    ALCdevice *device)
+{
+    /* First, get a free voice to start at the new offset. */
+    auto voicelist = context->getVoicesSpan();
+    Voice *newvoice{};
+    ALuint vidx{0};
+    for(Voice *voice : voicelist)
+    {
+        if(voice->mPlayState.load(std::memory_order_acquire) == Voice::Stopped
+            && voice->mSourceID.load(std::memory_order_relaxed) == 0u
+            && voice->mPendingChange.load(std::memory_order_relaxed) == false)
+        {
+            newvoice = voice;
+            break;
+        }
+        ++vidx;
+    }
+    if(!newvoice) UNLIKELY
+    {
+        auto &allvoices = *context->mVoices.load(std::memory_order_relaxed);
+        if(allvoices.size() == voicelist.size())
+            context->allocVoices(1);
+        context->mActiveVoiceCount.fetch_add(1, std::memory_order_release);
+        voicelist = context->getVoicesSpan();
+
+        vidx = 0;
+        for(Voice *voice : voicelist)
+        {
+            if(voice->mPlayState.load(std::memory_order_acquire) == Voice::Stopped
+                && voice->mSourceID.load(std::memory_order_relaxed) == 0u
+                && voice->mPendingChange.load(std::memory_order_relaxed) == false)
+            {
+                newvoice = voice;
+                break;
+            }
+            ++vidx;
+        }
+        ASSUME(newvoice != nullptr);
+    }
+
+    /* Initialize the new voice and set its starting offset.
+     * TODO: It might be better to have the VoiceChange processing copy the old
+     * voice's mixing parameters (and pending update) insead of initializing it
+     * all here. This would just need to set the minimum properties to link the
+     * voice to the source and its position-dependent properties (including the
+     * fading flag).
+     */
+    newvoice->mPlayState.store(Voice::Pending, std::memory_order_relaxed);
+    newvoice->mPosition.store(vpos.pos, std::memory_order_relaxed);
+    newvoice->mPositionFrac.store(vpos.frac, std::memory_order_relaxed);
+    newvoice->mCurrentBuffer.store(vpos.bufferitem, std::memory_order_relaxed);
+    newvoice->mStartTime = oldvoice->mStartTime;
+    newvoice->mFlags.reset();
+    if(vpos.pos > 0 || (vpos.pos == 0 && vpos.frac > 0)
+        || vpos.bufferitem != &source->mQueue.front())
+        newvoice->mFlags.set(VoiceIsFading);
+    InitVoice(newvoice, source, vpos.bufferitem, context, device);
+    source->VoiceIdx = vidx;
+
+    /* Set the old voice as having a pending change, and send it off with the
+     * new one with a new offset voice change.
+     */
+    oldvoice->mPendingChange.store(true, std::memory_order_relaxed);
+
+    VoiceChange *vchg{GetVoiceChanger(context)};
+    vchg->mOldVoice = oldvoice;
+    vchg->mVoice = newvoice;
+    vchg->mSourceID = source->id;
+    vchg->mState = VChangeState::Restart;
+    SendVoiceChanges(context, vchg);
+
+    /* If the old voice still has a sourceID, it's still active and the change-
+     * over will work on the next update.
+     */
+    if(oldvoice->mSourceID.load(std::memory_order_acquire) != 0u) LIKELY
+        return true;
+
+    /* Otherwise, if the new voice's state is not pending, the change-over
+     * already happened.
+     */
+    if(newvoice->mPlayState.load(std::memory_order_acquire) != Voice::Pending)
+        return true;
+
+    /* Otherwise, wait for any current mix to finish and check one last time. */
+    device->waitForMix();
+    if(newvoice->mPlayState.load(std::memory_order_acquire) != Voice::Pending)
+        return true;
+    /* The change-over failed because the old voice stopped before the new
+     * voice could start at the new offset. Let go of the new voice and have
+     * the caller store the source offset since it's stopped.
+     */
+    newvoice->mCurrentBuffer.store(nullptr, std::memory_order_relaxed);
+    newvoice->mLoopBuffer.store(nullptr, std::memory_order_relaxed);
+    newvoice->mSourceID.store(0u, std::memory_order_relaxed);
+    newvoice->mPlayState.store(Voice::Stopped, std::memory_order_relaxed);
+    return false;
+}
+
+
+/**
+ * Returns if the last known state for the source was playing or paused. Does
+ * not sync with the mixer voice.
+ */
+inline bool IsPlayingOrPaused(ALsource *source)
+{ return source->state == AL_PLAYING || source->state == AL_PAUSED; }
+
+/**
+ * Returns an updated source state using the matching voice's status (or lack
+ * thereof).
+ */
+inline ALenum GetSourceState(ALsource *source, Voice *voice)
+{
+    if(!voice && source->state == AL_PLAYING)
+        source->state = AL_STOPPED;
+    return source->state;
+}
+
+
+bool EnsureSources(ALCcontext *context, size_t needed)
+{
+    size_t count{std::accumulate(context->mSourceList.cbegin(), context->mSourceList.cend(),
+        size_t{0},
+        [](size_t cur, const SourceSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<ALuint>(al::popcount(sublist.FreeMask)); })};
+
+    while(needed > count)
+    {
+        if(context->mSourceList.size() >= 1<<25) UNLIKELY
+            return false;
+
+        context->mSourceList.emplace_back();
+        auto sublist = context->mSourceList.end() - 1;
+        sublist->FreeMask = ~0_u64;
+        sublist->Sources = static_cast<ALsource*>(al_calloc(alignof(ALsource), sizeof(ALsource)*64));
+        if(!sublist->Sources) UNLIKELY
+        {
+            context->mSourceList.pop_back();
+            return false;
+        }
+        count += 64;
+    }
+    return true;
+}
+
+ALsource *AllocSource(ALCcontext *context)
+{
+    auto sublist = std::find_if(context->mSourceList.begin(), context->mSourceList.end(),
+        [](const SourceSubList &entry) noexcept -> bool
+        { return entry.FreeMask != 0; });
+    auto lidx = static_cast<ALuint>(std::distance(context->mSourceList.begin(), sublist));
+    auto slidx = static_cast<ALuint>(al::countr_zero(sublist->FreeMask));
+    ASSUME(slidx < 64);
+
+    ALsource *source{al::construct_at(sublist->Sources + slidx)};
+
+    /* Add 1 to avoid source ID 0. */
+    source->id = ((lidx<<6) | slidx) + 1;
+
+    context->mNumSources += 1;
+    sublist->FreeMask &= ~(1_u64 << slidx);
+
+    return source;
+}
+
+void FreeSource(ALCcontext *context, ALsource *source)
+{
+    const ALuint id{source->id - 1};
+    const size_t lidx{id >> 6};
+    const ALuint slidx{id & 0x3f};
+
+    if(Voice *voice{GetSourceVoice(source, context)})
+    {
+        VoiceChange *vchg{GetVoiceChanger(context)};
+
+        voice->mPendingChange.store(true, std::memory_order_relaxed);
+        vchg->mVoice = voice;
+        vchg->mSourceID = source->id;
+        vchg->mState = VChangeState::Stop;
+
+        SendVoiceChanges(context, vchg);
+    }
+
+    al::destroy_at(source);
+
+    context->mSourceList[lidx].FreeMask |= 1_u64 << slidx;
+    context->mNumSources--;
+}
+
+
+inline ALsource *LookupSource(ALCcontext *context, ALuint id) noexcept
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= context->mSourceList.size()) UNLIKELY
+        return nullptr;
+    SourceSubList &sublist{context->mSourceList[lidx]};
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Sources + slidx;
+}
+
+inline ALbuffer *LookupBuffer(ALCdevice *device, ALuint id) noexcept
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= device->BufferList.size()) UNLIKELY
+        return nullptr;
+    BufferSubList &sublist = device->BufferList[lidx];
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Buffers + slidx;
+}
+
+inline ALfilter *LookupFilter(ALCdevice *device, ALuint id) noexcept
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= device->FilterList.size()) UNLIKELY
+        return nullptr;
+    FilterSubList &sublist = device->FilterList[lidx];
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.Filters + slidx;
+}
+
+inline ALeffectslot *LookupEffectSlot(ALCcontext *context, ALuint id) noexcept
+{
+    const size_t lidx{(id-1) >> 6};
+    const ALuint slidx{(id-1) & 0x3f};
+
+    if(lidx >= context->mEffectSlotList.size()) UNLIKELY
+        return nullptr;
+    EffectSlotSubList &sublist{context->mEffectSlotList[lidx]};
+    if(sublist.FreeMask & (1_u64 << slidx)) UNLIKELY
+        return nullptr;
+    return sublist.EffectSlots + slidx;
+}
+
+
+al::optional<SourceStereo> StereoModeFromEnum(ALenum mode)
+{
+    switch(mode)
+    {
+    case AL_NORMAL_SOFT: return SourceStereo::Normal;
+    case AL_SUPER_STEREO_SOFT: return SourceStereo::Enhanced;
+    }
+    WARN("Unsupported stereo mode: 0x%04x\n", mode);
+    return al::nullopt;
+}
+ALenum EnumFromStereoMode(SourceStereo mode)
+{
+    switch(mode)
+    {
+    case SourceStereo::Normal: return AL_NORMAL_SOFT;
+    case SourceStereo::Enhanced: return AL_SUPER_STEREO_SOFT;
+    }
+    throw std::runtime_error{"Invalid SourceStereo: "+std::to_string(int(mode))};
+}
+
+al::optional<SpatializeMode> SpatializeModeFromEnum(ALenum mode)
+{
+    switch(mode)
+    {
+    case AL_FALSE: return SpatializeMode::Off;
+    case AL_TRUE: return SpatializeMode::On;
+    case AL_AUTO_SOFT: return SpatializeMode::Auto;
+    }
+    WARN("Unsupported spatialize mode: 0x%04x\n", mode);
+    return al::nullopt;
+}
+ALenum EnumFromSpatializeMode(SpatializeMode mode)
+{
+    switch(mode)
+    {
+    case SpatializeMode::Off: return AL_FALSE;
+    case SpatializeMode::On: return AL_TRUE;
+    case SpatializeMode::Auto: return AL_AUTO_SOFT;
+    }
+    throw std::runtime_error{"Invalid SpatializeMode: "+std::to_string(int(mode))};
+}
+
+al::optional<DirectMode> DirectModeFromEnum(ALenum mode)
+{
+    switch(mode)
+    {
+    case AL_FALSE: return DirectMode::Off;
+    case AL_DROP_UNMATCHED_SOFT: return DirectMode::DropMismatch;
+    case AL_REMIX_UNMATCHED_SOFT: return DirectMode::RemixMismatch;
+    }
+    WARN("Unsupported direct mode: 0x%04x\n", mode);
+    return al::nullopt;
+}
+ALenum EnumFromDirectMode(DirectMode mode)
+{
+    switch(mode)
+    {
+    case DirectMode::Off: return AL_FALSE;
+    case DirectMode::DropMismatch: return AL_DROP_UNMATCHED_SOFT;
+    case DirectMode::RemixMismatch: return AL_REMIX_UNMATCHED_SOFT;
+    }
+    throw std::runtime_error{"Invalid DirectMode: "+std::to_string(int(mode))};
+}
+
+al::optional<DistanceModel> DistanceModelFromALenum(ALenum model)
+{
+    switch(model)
+    {
+    case AL_NONE: return DistanceModel::Disable;
+    case AL_INVERSE_DISTANCE: return DistanceModel::Inverse;
+    case AL_INVERSE_DISTANCE_CLAMPED: return DistanceModel::InverseClamped;
+    case AL_LINEAR_DISTANCE: return DistanceModel::Linear;
+    case AL_LINEAR_DISTANCE_CLAMPED: return DistanceModel::LinearClamped;
+    case AL_EXPONENT_DISTANCE: return DistanceModel::Exponent;
+    case AL_EXPONENT_DISTANCE_CLAMPED: return DistanceModel::ExponentClamped;
+    }
+    return al::nullopt;
+}
+ALenum ALenumFromDistanceModel(DistanceModel model)
+{
+    switch(model)
+    {
+    case DistanceModel::Disable: return AL_NONE;
+    case DistanceModel::Inverse: return AL_INVERSE_DISTANCE;
+    case DistanceModel::InverseClamped: return AL_INVERSE_DISTANCE_CLAMPED;
+    case DistanceModel::Linear: return AL_LINEAR_DISTANCE;
+    case DistanceModel::LinearClamped: return AL_LINEAR_DISTANCE_CLAMPED;
+    case DistanceModel::Exponent: return AL_EXPONENT_DISTANCE;
+    case DistanceModel::ExponentClamped: return AL_EXPONENT_DISTANCE_CLAMPED;
+    }
+    throw std::runtime_error{"Unexpected distance model "+std::to_string(static_cast<int>(model))};
+}
+
+enum SourceProp : ALenum {
+    srcPitch = AL_PITCH,
+    srcGain = AL_GAIN,
+    srcMinGain = AL_MIN_GAIN,
+    srcMaxGain = AL_MAX_GAIN,
+    srcMaxDistance = AL_MAX_DISTANCE,
+    srcRolloffFactor = AL_ROLLOFF_FACTOR,
+    srcDopplerFactor = AL_DOPPLER_FACTOR,
+    srcConeOuterGain = AL_CONE_OUTER_GAIN,
+    srcSecOffset = AL_SEC_OFFSET,
+    srcSampleOffset = AL_SAMPLE_OFFSET,
+    srcByteOffset = AL_BYTE_OFFSET,
+    srcConeInnerAngle = AL_CONE_INNER_ANGLE,
+    srcConeOuterAngle = AL_CONE_OUTER_ANGLE,
+    srcRefDistance = AL_REFERENCE_DISTANCE,
+
+    srcPosition = AL_POSITION,
+    srcVelocity = AL_VELOCITY,
+    srcDirection = AL_DIRECTION,
+
+    srcSourceRelative = AL_SOURCE_RELATIVE,
+    srcLooping = AL_LOOPING,
+    srcBuffer = AL_BUFFER,
+    srcSourceState = AL_SOURCE_STATE,
+    srcBuffersQueued = AL_BUFFERS_QUEUED,
+    srcBuffersProcessed = AL_BUFFERS_PROCESSED,
+    srcSourceType = AL_SOURCE_TYPE,
+
+    /* ALC_EXT_EFX */
+    srcConeOuterGainHF = AL_CONE_OUTER_GAINHF,
+    srcAirAbsorptionFactor = AL_AIR_ABSORPTION_FACTOR,
+    srcRoomRolloffFactor =  AL_ROOM_ROLLOFF_FACTOR,
+    srcDirectFilterGainHFAuto = AL_DIRECT_FILTER_GAINHF_AUTO,
+    srcAuxSendFilterGainAuto = AL_AUXILIARY_SEND_FILTER_GAIN_AUTO,
+    srcAuxSendFilterGainHFAuto = AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO,
+    srcDirectFilter = AL_DIRECT_FILTER,
+    srcAuxSendFilter = AL_AUXILIARY_SEND_FILTER,
+
+    /* AL_SOFT_direct_channels */
+    srcDirectChannelsSOFT = AL_DIRECT_CHANNELS_SOFT,
+
+    /* AL_EXT_source_distance_model */
+    srcDistanceModel = AL_DISTANCE_MODEL,
+
+    /* AL_SOFT_source_latency */
+    srcSampleOffsetLatencySOFT = AL_SAMPLE_OFFSET_LATENCY_SOFT,
+    srcSecOffsetLatencySOFT = AL_SEC_OFFSET_LATENCY_SOFT,
+
+    /* AL_EXT_STEREO_ANGLES */
+    srcAngles = AL_STEREO_ANGLES,
+
+    /* AL_EXT_SOURCE_RADIUS */
+    srcRadius = AL_SOURCE_RADIUS,
+
+    /* AL_EXT_BFORMAT */
+    srcOrientation = AL_ORIENTATION,
+
+    /* AL_SOFT_source_length */
+    srcByteLength = AL_BYTE_LENGTH_SOFT,
+    srcSampleLength = AL_SAMPLE_LENGTH_SOFT,
+    srcSecLength = AL_SEC_LENGTH_SOFT,
+
+    /* AL_SOFT_source_resampler */
+    srcResampler = AL_SOURCE_RESAMPLER_SOFT,
+
+    /* AL_SOFT_source_spatialize */
+    srcSpatialize = AL_SOURCE_SPATIALIZE_SOFT,
+
+    /* ALC_SOFT_device_clock */
+    srcSampleOffsetClockSOFT = AL_SAMPLE_OFFSET_CLOCK_SOFT,
+    srcSecOffsetClockSOFT = AL_SEC_OFFSET_CLOCK_SOFT,
+
+    /* AL_SOFT_UHJ */
+    srcStereoMode = AL_STEREO_MODE_SOFT,
+    srcSuperStereoWidth = AL_SUPER_STEREO_WIDTH_SOFT,
+
+    /* AL_SOFT_buffer_sub_data */
+    srcByteRWOffsetsSOFT = AL_BYTE_RW_OFFSETS_SOFT,
+    srcSampleRWOffsetsSOFT = AL_SAMPLE_RW_OFFSETS_SOFT,
+};
+
+
+constexpr size_t MaxValues{6u};
+
+constexpr ALuint IntValsByProp(ALenum prop)
+{
+    switch(static_cast<SourceProp>(prop))
+    {
+    case AL_SOURCE_STATE:
+    case AL_SOURCE_TYPE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_BUFFER:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+    case AL_DIRECT_FILTER:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_DISTANCE_MODEL:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_STEREO_MODE_SOFT:
+        return 1;
+
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+            return 2;
+        /*fall-through*/
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_REFERENCE_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_SEC_OFFSET:
+    case AL_DOPPLER_FACTOR:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_SEC_LENGTH_SOFT:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        return 1; /* 1x float */
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        if(sBufferSubDataCompat)
+            return 2;
+        break;
+
+    case AL_AUXILIARY_SEND_FILTER:
+        return 3;
+
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        return 3; /* 3x float */
+
+    case AL_ORIENTATION:
+        return 6; /* 6x float */
+
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+    case AL_STEREO_ANGLES:
+        break; /* i64 only */
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+        break; /* double only */
+    }
+
+    return 0;
+}
+
+constexpr ALuint Int64ValsByProp(ALenum prop)
+{
+    switch(static_cast<SourceProp>(prop))
+    {
+    case AL_SOURCE_STATE:
+    case AL_SOURCE_TYPE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_BUFFER:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+    case AL_DIRECT_FILTER:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_DISTANCE_MODEL:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_STEREO_MODE_SOFT:
+        return 1;
+
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+            return 2;
+        /*fall-through*/
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_REFERENCE_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_SEC_OFFSET:
+    case AL_DOPPLER_FACTOR:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_SEC_LENGTH_SOFT:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        return 1; /* 1x float */
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        if(sBufferSubDataCompat)
+            return 2;
+        break;
+
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+    case AL_STEREO_ANGLES:
+        return 2;
+
+    case AL_AUXILIARY_SEND_FILTER:
+        return 3;
+
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        return 3; /* 3x float */
+
+    case AL_ORIENTATION:
+        return 6; /* 6x float */
+
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+        break; /* double only */
+    }
+
+    return 0;
+}
+
+constexpr ALuint FloatValsByProp(ALenum prop)
+{
+    switch(static_cast<SourceProp>(prop))
+    {
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_DOPPLER_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_REFERENCE_DISTANCE:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_DISTANCE_MODEL:
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_SOURCE_STATE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_SOURCE_TYPE:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SEC_LENGTH_SOFT:
+    case AL_STEREO_MODE_SOFT:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        return 1;
+
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(!sBufferSubDataCompat)
+            return 1;
+        /*fall-through*/
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        break;
+
+    case AL_STEREO_ANGLES:
+        return 2;
+
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        return 3;
+
+    case AL_ORIENTATION:
+        return 6;
+
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+        break; /* Double only */
+
+    case AL_BUFFER:
+    case AL_DIRECT_FILTER:
+    case AL_AUXILIARY_SEND_FILTER:
+        break; /* i/i64 only */
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+        break; /* i64 only */
+    }
+    return 0;
+}
+constexpr ALuint DoubleValsByProp(ALenum prop)
+{
+    switch(static_cast<SourceProp>(prop))
+    {
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_DOPPLER_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_REFERENCE_DISTANCE:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_DISTANCE_MODEL:
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_SOURCE_STATE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_SOURCE_TYPE:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SEC_LENGTH_SOFT:
+    case AL_STEREO_MODE_SOFT:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        return 1;
+
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(!sBufferSubDataCompat)
+            return 1;
+        /*fall-through*/
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        break;
+
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+    case AL_STEREO_ANGLES:
+        return 2;
+
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        return 3;
+
+    case AL_ORIENTATION:
+        return 6;
+
+    case AL_BUFFER:
+    case AL_DIRECT_FILTER:
+    case AL_AUXILIARY_SEND_FILTER:
+        break; /* i/i64 only */
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+        break; /* i64 only */
+    }
+    return 0;
+}
+
+
+void SetSourcefv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop, const al::span<const float> values);
+void SetSourceiv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop, const al::span<const int> values);
+void SetSourcei64v(ALsource *const Source, ALCcontext *const Context, const SourceProp prop, const al::span<const int64_t> values);
+
+struct check_exception : std::exception {
+};
+struct check_size_exception final : check_exception {
+    const char *what() const noexcept override
+    { return "check_size_exception"; }
+};
+struct check_value_exception final : check_exception {
+    const char *what() const noexcept override
+    { return "check_value_exception"; }
+};
+
+
+void UpdateSourceProps(ALsource *source, ALCcontext *context)
+{
+    if(!context->mDeferUpdates)
+    {
+        if(Voice *voice{GetSourceVoice(source, context)})
+        {
+            UpdateSourceProps(source, voice, context);
+            return;
+        }
+    }
+    source->mPropsDirty = true;
+}
+#ifdef ALSOFT_EAX
+void CommitAndUpdateSourceProps(ALsource *source, ALCcontext *context)
+{
+    if(!context->mDeferUpdates)
+    {
+        if(context->hasEax())
+            source->eaxCommit();
+        if(Voice *voice{GetSourceVoice(source, context)})
+        {
+            UpdateSourceProps(source, voice, context);
+            return;
+        }
+    }
+    source->mPropsDirty = true;
+}
+
+#else
+
+inline void CommitAndUpdateSourceProps(ALsource *source, ALCcontext *context)
+{ UpdateSourceProps(source, context); }
+#endif
+
+
+/**
+ * Returns a pair of lambdas to check the following setters and getters.
+ *
+ * The first lambda checks the size of the span is valid for its given size,
+ * setting the proper context error and throwing a check_size_exception if it
+ * fails.
+ *
+ * The second lambda tests the validity of the value check, setting the proper
+ * context error and throwing a check_value_exception if it failed.
+ */
+template<typename T, size_t N>
+auto GetCheckers(ALCcontext *const Context, const SourceProp prop, const al::span<T,N> values)
+{
+    return std::make_pair(
+        [=](size_t expect) -> void
+        {
+            if(values.size() == expect) LIKELY return;
+            Context->setError(AL_INVALID_ENUM, "Property 0x%04x expects %zu value(s), got %zu",
+                prop, expect, values.size());
+            throw check_size_exception{};
+        },
+        [Context](bool passed) -> void
+        {
+            if(passed) LIKELY return;
+            Context->setError(AL_INVALID_VALUE, "Value out of range");
+            throw check_value_exception{};
+        }
+    );
+}
+
+void SetSourcefv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop,
+    const al::span<const float> values)
+try {
+    /* Structured bindings would be nice (C++17). */
+    auto Checkers = GetCheckers(Context, prop, values);
+    auto &CheckSize = Checkers.first;
+    auto &CheckValue = Checkers.second;
+    int ival;
+
+    switch(prop)
+    {
+    case AL_SEC_LENGTH_SOFT:
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+        /* Query only */
+        return Context->setError(AL_INVALID_OPERATION,
+            "Setting read-only source property 0x%04x", prop);
+
+    case AL_PITCH:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f);
+
+        Source->Pitch = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_CONE_INNER_ANGLE:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 360.0f);
+
+        Source->InnerAngle = values[0];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_CONE_OUTER_ANGLE:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 360.0f);
+
+        Source->OuterAngle = values[0];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_GAIN:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f);
+
+        Source->Gain = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_MAX_DISTANCE:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f);
+
+        Source->MaxDistance = values[0];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_ROLLOFF_FACTOR:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f);
+
+        Source->RolloffFactor = values[0];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_REFERENCE_DISTANCE:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f);
+
+        Source->RefDistance = values[0];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_MIN_GAIN:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f);
+
+        Source->MinGain = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_MAX_GAIN:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f);
+
+        Source->MaxGain = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_CONE_OUTER_GAIN:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 1.0f);
+
+        Source->OuterGain = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_CONE_OUTER_GAINHF:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 1.0f);
+
+        Source->OuterGainHF = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_AIR_ABSORPTION_FACTOR:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 10.0f);
+
+        Source->AirAbsorptionFactor = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_ROOM_ROLLOFF_FACTOR:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 10.0f);
+
+        Source->RoomRolloffFactor = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_DOPPLER_FACTOR:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 1.0f);
+
+        Source->DopplerFactor = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+        CheckSize(1);
+        CheckValue(std::isfinite(values[0]));
+
+        if(Voice *voice{GetSourceVoice(Source, Context)})
+        {
+            auto vpos = GetSampleOffset(Source->mQueue, prop, values[0]);
+            if(!vpos) return Context->setError(AL_INVALID_VALUE, "Invalid offset");
+
+            if(SetVoiceOffset(voice, *vpos, Source, Context, Context->mALDevice.get()))
+                return;
+        }
+        Source->OffsetType = prop;
+        Source->Offset = values[0];
+        return;
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        break;
+
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+            break;
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && std::isfinite(values[0]));
+
+        Source->Radius = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        CheckSize(1);
+        CheckValue(values[0] >= 0.0f && values[0] <= 1.0f);
+
+        Source->EnhWidth = values[0];
+        return UpdateSourceProps(Source, Context);
+
+    case AL_STEREO_ANGLES:
+        CheckSize(2);
+        CheckValue(std::isfinite(values[0]) && std::isfinite(values[1]));
+
+        Source->StereoPan[0] = values[0];
+        Source->StereoPan[1] = values[1];
+        return UpdateSourceProps(Source, Context);
+
+
+    case AL_POSITION:
+        CheckSize(3);
+        CheckValue(std::isfinite(values[0]) && std::isfinite(values[1]) && std::isfinite(values[2]));
+
+        Source->Position[0] = values[0];
+        Source->Position[1] = values[1];
+        Source->Position[2] = values[2];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_VELOCITY:
+        CheckSize(3);
+        CheckValue(std::isfinite(values[0]) && std::isfinite(values[1]) && std::isfinite(values[2]));
+
+        Source->Velocity[0] = values[0];
+        Source->Velocity[1] = values[1];
+        Source->Velocity[2] = values[2];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_DIRECTION:
+        CheckSize(3);
+        CheckValue(std::isfinite(values[0]) && std::isfinite(values[1]) && std::isfinite(values[2]));
+
+        Source->Direction[0] = values[0];
+        Source->Direction[1] = values[1];
+        Source->Direction[2] = values[2];
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_ORIENTATION:
+        CheckSize(6);
+        CheckValue(std::isfinite(values[0]) && std::isfinite(values[1]) && std::isfinite(values[2])
+            && std::isfinite(values[3]) && std::isfinite(values[4]) && std::isfinite(values[5]));
+
+        Source->OrientAt[0] = values[0];
+        Source->OrientAt[1] = values[1];
+        Source->OrientAt[2] = values[2];
+        Source->OrientUp[0] = values[3];
+        Source->OrientUp[1] = values[4];
+        Source->OrientUp[2] = values[5];
+        return UpdateSourceProps(Source, Context);
+
+
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_SOURCE_STATE:
+    case AL_SOURCE_TYPE:
+    case AL_DISTANCE_MODEL:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_STEREO_MODE_SOFT:
+        CheckSize(1);
+        ival = static_cast<int>(values[0]);
+        return SetSourceiv(Source, Context, prop, {&ival, 1u});
+
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+        CheckSize(1);
+        ival = static_cast<int>(static_cast<ALuint>(values[0]));
+        return SetSourceiv(Source, Context, prop, {&ival, 1u});
+
+    case AL_BUFFER:
+    case AL_DIRECT_FILTER:
+    case AL_AUXILIARY_SEND_FILTER:
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+        break;
+    }
+
+    ERR("Unexpected property: 0x%04x\n", prop);
+    Context->setError(AL_INVALID_ENUM, "Invalid source float property 0x%04x", prop);
+}
+catch(check_exception&) {
+}
+
+void SetSourceiv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop,
+    const al::span<const int> values)
+try {
+    auto Checkers = GetCheckers(Context, prop, values);
+    auto &CheckSize = Checkers.first;
+    auto &CheckValue = Checkers.second;
+    ALCdevice *device{Context->mALDevice.get()};
+    ALeffectslot *slot{nullptr};
+    al::deque<ALbufferQueueItem> oldlist;
+    std::unique_lock<std::mutex> slotlock;
+    float fvals[6];
+
+    switch(prop)
+    {
+    case AL_SOURCE_STATE:
+    case AL_SOURCE_TYPE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+        /* Query only */
+        return Context->setError(AL_INVALID_OPERATION,
+            "Setting read-only source property 0x%04x", prop);
+
+    case AL_SOURCE_RELATIVE:
+        CheckSize(1);
+        CheckValue(values[0] == AL_FALSE || values[0] == AL_TRUE);
+
+        Source->HeadRelative = values[0] != AL_FALSE;
+        return CommitAndUpdateSourceProps(Source, Context);
+
+    case AL_LOOPING:
+        CheckSize(1);
+        CheckValue(values[0] == AL_FALSE || values[0] == AL_TRUE);
+
+        Source->Looping = values[0] != AL_FALSE;
+        if(Voice *voice{GetSourceVoice(Source, Context)})
+        {
+            if(Source->Looping)
+                voice->mLoopBuffer.store(&Source->mQueue.front(), std::memory_order_release);
+            else
+                voice->mLoopBuffer.store(nullptr, std::memory_order_release);
+
+            /* If the source is playing, wait for the current mix to finish to
+             * ensure it isn't currently looping back or reaching the end.
+             */
+            device->waitForMix();
+        }
+        return;
+
+    case AL_BUFFER:
+        CheckSize(1);
+        {
+            const ALenum state{GetSourceState(Source, GetSourceVoice(Source, Context))};
+            if(state == AL_PLAYING || state == AL_PAUSED)
+                return Context->setError(AL_INVALID_OPERATION,
+                    "Setting buffer on playing or paused source %u", Source->id);
+        }
+        if(values[0])
+        {
+            std::lock_guard<std::mutex> _{device->BufferLock};
+            ALbuffer *buffer{LookupBuffer(device, static_cast<ALuint>(values[0]))};
+            if(!buffer)
+                return Context->setError(AL_INVALID_VALUE, "Invalid buffer ID %u",
+                    static_cast<ALuint>(values[0]));
+            if(buffer->MappedAccess && !(buffer->MappedAccess&AL_MAP_PERSISTENT_BIT_SOFT))
+                return Context->setError(AL_INVALID_OPERATION,
+                    "Setting non-persistently mapped buffer %u", buffer->id);
+            if(buffer->mCallback && ReadRef(buffer->ref) != 0)
+                return Context->setError(AL_INVALID_OPERATION,
+                    "Setting already-set callback buffer %u", buffer->id);
+
+            /* Add the selected buffer to a one-item queue */
+            al::deque<ALbufferQueueItem> newlist;
+            newlist.emplace_back();
+            newlist.back().mCallback = buffer->mCallback;
+            newlist.back().mUserData = buffer->mUserData;
+            newlist.back().mBlockAlign = buffer->mBlockAlign;
+            newlist.back().mSampleLen = buffer->mSampleLen;
+            newlist.back().mLoopStart = buffer->mLoopStart;
+            newlist.back().mLoopEnd = buffer->mLoopEnd;
+            newlist.back().mSamples = buffer->mData.data();
+            newlist.back().mBuffer = buffer;
+            IncrementRef(buffer->ref);
+
+            /* Source is now Static */
+            Source->SourceType = AL_STATIC;
+            Source->mQueue.swap(oldlist);
+            Source->mQueue.swap(newlist);
+        }
+        else
+        {
+            /* Source is now Undetermined */
+            Source->SourceType = AL_UNDETERMINED;
+            Source->mQueue.swap(oldlist);
+        }
+
+        /* Delete all elements in the previous queue */
+        for(auto &item : oldlist)
+        {
+            if(ALbuffer *buffer{item.mBuffer})
+                DecrementRef(buffer->ref);
+        }
+        return;
+
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+        CheckSize(1);
+
+        if(Voice *voice{GetSourceVoice(Source, Context)})
+        {
+            auto vpos = GetSampleOffset(Source->mQueue, prop, values[0]);
+            if(!vpos) return Context->setError(AL_INVALID_VALUE, "Invalid source offset");
+
+            if(SetVoiceOffset(voice, *vpos, Source, Context, device))
+                return;
+        }
+        Source->OffsetType = prop;
+        Source->Offset = values[0];
+        return;
+
+    case AL_DIRECT_FILTER:
+        CheckSize(1);
+        if(values[0])
+        {
+            std::lock_guard<std::mutex> _{device->FilterLock};
+            ALfilter *filter{LookupFilter(device, static_cast<ALuint>(values[0]))};
+            if(!filter)
+                return Context->setError(AL_INVALID_VALUE, "Invalid filter ID %u",
+                    static_cast<ALuint>(values[0]));
+            Source->Direct.Gain = filter->Gain;
+            Source->Direct.GainHF = filter->GainHF;
+            Source->Direct.HFReference = filter->HFReference;
+            Source->Direct.GainLF = filter->GainLF;
+            Source->Direct.LFReference = filter->LFReference;
+        }
+        else
+        {
+            Source->Direct.Gain = 1.0f;
+            Source->Direct.GainHF = 1.0f;
+            Source->Direct.HFReference = LOWPASSFREQREF;
+            Source->Direct.GainLF = 1.0f;
+            Source->Direct.LFReference = HIGHPASSFREQREF;
+        }
+        return UpdateSourceProps(Source, Context);
+
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+        CheckSize(1);
+        CheckValue(values[0] == AL_FALSE || values[0] == AL_TRUE);
+
+        Source->DryGainHFAuto = values[0] != AL_FALSE;
+        return UpdateSourceProps(Source, Context);
+
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+        CheckSize(1);
+        CheckValue(values[0] == AL_FALSE || values[0] == AL_TRUE);
+
+        Source->WetGainAuto = values[0] != AL_FALSE;
+        return UpdateSourceProps(Source, Context);
+
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+        CheckSize(1);
+        CheckValue(values[0] == AL_FALSE || values[0] == AL_TRUE);
+
+        Source->WetGainHFAuto = values[0] != AL_FALSE;
+        return UpdateSourceProps(Source, Context);
+
+    case AL_DIRECT_CHANNELS_SOFT:
+        CheckSize(1);
+        if(auto mode = DirectModeFromEnum(values[0]))
+        {
+            Source->DirectChannels = *mode;
+            return UpdateSourceProps(Source, Context);
+        }
+        Context->setError(AL_INVALID_VALUE, "Unsupported AL_DIRECT_CHANNELS_SOFT: 0x%04x\n",
+            values[0]);
+        return;
+
+    case AL_DISTANCE_MODEL:
+        CheckSize(1);
+        if(auto model = DistanceModelFromALenum(values[0]))
+        {
+            Source->mDistanceModel = *model;
+            if(Context->mSourceDistanceModel)
+                UpdateSourceProps(Source, Context);
+            return;
+        }
+        Context->setError(AL_INVALID_VALUE, "Distance model out of range: 0x%04x", values[0]);
+        return;
+
+    case AL_SOURCE_RESAMPLER_SOFT:
+        CheckSize(1);
+        CheckValue(values[0] >= 0 && values[0] <= static_cast<int>(Resampler::Max));
+
+        Source->mResampler = static_cast<Resampler>(values[0]);
+        return UpdateSourceProps(Source, Context);
+
+    case AL_SOURCE_SPATIALIZE_SOFT:
+        CheckSize(1);
+        if(auto mode = SpatializeModeFromEnum(values[0]))
+        {
+            Source->mSpatialize = *mode;
+            return UpdateSourceProps(Source, Context);
+        }
+        Context->setError(AL_INVALID_VALUE, "Unsupported AL_SOURCE_SPATIALIZE_SOFT: 0x%04x\n",
+            values[0]);
+        return;
+
+    case AL_STEREO_MODE_SOFT:
+        CheckSize(1);
+        {
+            const ALenum state{GetSourceState(Source, GetSourceVoice(Source, Context))};
+            if(state == AL_PLAYING || state == AL_PAUSED)
+                return Context->setError(AL_INVALID_OPERATION,
+                    "Modifying stereo mode on playing or paused source %u", Source->id);
+        }
+        if(auto mode = StereoModeFromEnum(values[0]))
+        {
+            Source->mStereoMode = *mode;
+            return;
+        }
+        Context->setError(AL_INVALID_VALUE, "Unsupported AL_STEREO_MODE_SOFT: 0x%04x\n",
+            values[0]);
+        return;
+
+    case AL_AUXILIARY_SEND_FILTER:
+        CheckSize(3);
+        slotlock = std::unique_lock<std::mutex>{Context->mEffectSlotLock};
+        if(values[0] && (slot=LookupEffectSlot(Context, static_cast<ALuint>(values[0]))) == nullptr)
+            return Context->setError(AL_INVALID_VALUE, "Invalid effect ID %u", values[0]);
+        if(static_cast<ALuint>(values[1]) >= device->NumAuxSends)
+            return Context->setError(AL_INVALID_VALUE, "Invalid send %u", values[1]);
+
+        if(values[2])
+        {
+            std::lock_guard<std::mutex> _{device->FilterLock};
+            ALfilter *filter{LookupFilter(device, static_cast<ALuint>(values[2]))};
+            if(!filter)
+                return Context->setError(AL_INVALID_VALUE, "Invalid filter ID %u", values[2]);
+
+            auto &send = Source->Send[static_cast<ALuint>(values[1])];
+            send.Gain = filter->Gain;
+            send.GainHF = filter->GainHF;
+            send.HFReference = filter->HFReference;
+            send.GainLF = filter->GainLF;
+            send.LFReference = filter->LFReference;
+        }
+        else
+        {
+            /* Disable filter */
+            auto &send = Source->Send[static_cast<ALuint>(values[1])];
+            send.Gain = 1.0f;
+            send.GainHF = 1.0f;
+            send.HFReference = LOWPASSFREQREF;
+            send.GainLF = 1.0f;
+            send.LFReference = HIGHPASSFREQREF;
+        }
+
+        /* We must force an update if the current auxiliary slot is valid and
+         * about to be changed on an active source, in case the old slot is
+         * about to be deleted.
+         */
+        if(Source->Send[static_cast<ALuint>(values[1])].Slot
+            && slot != Source->Send[static_cast<ALuint>(values[1])].Slot
+            && IsPlayingOrPaused(Source))
+        {
+            /* Add refcount on the new slot, and release the previous slot */
+            if(slot) IncrementRef(slot->ref);
+            if(auto *oldslot = Source->Send[static_cast<ALuint>(values[1])].Slot)
+                DecrementRef(oldslot->ref);
+            Source->Send[static_cast<ALuint>(values[1])].Slot = slot;
+
+            Voice *voice{GetSourceVoice(Source, Context)};
+            if(voice) UpdateSourceProps(Source, voice, Context);
+            else Source->mPropsDirty = true;
+        }
+        else
+        {
+            if(slot) IncrementRef(slot->ref);
+            if(auto *oldslot = Source->Send[static_cast<ALuint>(values[1])].Slot)
+                DecrementRef(oldslot->ref);
+            Source->Send[static_cast<ALuint>(values[1])].Slot = slot;
+            UpdateSourceProps(Source, Context);
+        }
+        return;
+
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        if(sBufferSubDataCompat)
+            /* Query only */
+            return Context->setError(AL_INVALID_OPERATION,
+                "Setting read-only source property 0x%04x", prop);
+        break;
+
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+            return Context->setError(AL_INVALID_OPERATION,
+                "Setting read-only source property 0x%04x", prop);
+        /*fall-through*/
+
+    /* 1x float */
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_REFERENCE_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_DOPPLER_FACTOR:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_SEC_LENGTH_SOFT:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        CheckSize(1);
+        fvals[0] = static_cast<float>(values[0]);
+        return SetSourcefv(Source, Context, prop, {fvals, 1u});
+
+    /* 3x float */
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        CheckSize(3);
+        fvals[0] = static_cast<float>(values[0]);
+        fvals[1] = static_cast<float>(values[1]);
+        fvals[2] = static_cast<float>(values[2]);
+        return SetSourcefv(Source, Context, prop, {fvals, 3u});
+
+    /* 6x float */
+    case AL_ORIENTATION:
+        CheckSize(6);
+        fvals[0] = static_cast<float>(values[0]);
+        fvals[1] = static_cast<float>(values[1]);
+        fvals[2] = static_cast<float>(values[2]);
+        fvals[3] = static_cast<float>(values[3]);
+        fvals[4] = static_cast<float>(values[4]);
+        fvals[5] = static_cast<float>(values[5]);
+        return SetSourcefv(Source, Context, prop, {fvals, 6u});
+
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+    case AL_STEREO_ANGLES:
+        break;
+    }
+
+    ERR("Unexpected property: 0x%04x\n", prop);
+    Context->setError(AL_INVALID_ENUM, "Invalid source integer property 0x%04x", prop);
+}
+catch(check_exception&) {
+}
+
+void SetSourcei64v(ALsource *const Source, ALCcontext *const Context, const SourceProp prop,
+    const al::span<const int64_t> values)
+try {
+    auto Checkers = GetCheckers(Context, prop, values);
+    auto &CheckSize = Checkers.first;
+    auto &CheckValue = Checkers.second;
+    float fvals[MaxValues];
+    int   ivals[MaxValues];
+
+    switch(prop)
+    {
+    case AL_SOURCE_TYPE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_SOURCE_STATE:
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+        /* Query only */
+        return Context->setError(AL_INVALID_OPERATION,
+            "Setting read-only source property 0x%04x", prop);
+
+    /* 1x int */
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_DISTANCE_MODEL:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_STEREO_MODE_SOFT:
+        CheckSize(1);
+        CheckValue(values[0] <= INT_MAX && values[0] >= INT_MIN);
+
+        ivals[0] = static_cast<int>(values[0]);
+        return SetSourceiv(Source, Context, prop, {ivals, 1u});
+
+    /* 1x uint */
+    case AL_BUFFER:
+    case AL_DIRECT_FILTER:
+        CheckSize(1);
+        CheckValue(values[0] <= UINT_MAX && values[0] >= 0);
+
+        ivals[0] = static_cast<int>(values[0]);
+        return SetSourceiv(Source, Context, prop, {ivals, 1u});
+
+    /* 3x uint */
+    case AL_AUXILIARY_SEND_FILTER:
+        CheckSize(3);
+        CheckValue(values[0] <= UINT_MAX && values[0] >= 0 && values[1] <= UINT_MAX
+            && values[1] >= 0 && values[2] <= UINT_MAX && values[2] >= 0);
+
+        ivals[0] = static_cast<int>(values[0]);
+        ivals[1] = static_cast<int>(values[1]);
+        ivals[2] = static_cast<int>(values[2]);
+        return SetSourceiv(Source, Context, prop, {ivals, 3u});
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        if(sBufferSubDataCompat)
+        {
+            /* Query only */
+            return Context->setError(AL_INVALID_OPERATION,
+                "Setting read-only source property 0x%04x", prop);
+        }
+        break;
+
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+            return Context->setError(AL_INVALID_OPERATION,
+                "Setting read-only source property 0x%04x", prop);
+        /*fall-through*/
+
+    /* 1x float */
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_REFERENCE_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_DOPPLER_FACTOR:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_SEC_LENGTH_SOFT:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        CheckSize(1);
+        fvals[0] = static_cast<float>(values[0]);
+        return SetSourcefv(Source, Context, prop, {fvals, 1u});
+
+    /* 3x float */
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        CheckSize(3);
+        fvals[0] = static_cast<float>(values[0]);
+        fvals[1] = static_cast<float>(values[1]);
+        fvals[2] = static_cast<float>(values[2]);
+        return SetSourcefv(Source, Context, prop, {fvals, 3u});
+
+    /* 6x float */
+    case AL_ORIENTATION:
+        CheckSize(6);
+        fvals[0] = static_cast<float>(values[0]);
+        fvals[1] = static_cast<float>(values[1]);
+        fvals[2] = static_cast<float>(values[2]);
+        fvals[3] = static_cast<float>(values[3]);
+        fvals[4] = static_cast<float>(values[4]);
+        fvals[5] = static_cast<float>(values[5]);
+        return SetSourcefv(Source, Context, prop, {fvals, 6u});
+
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+    case AL_STEREO_ANGLES:
+        break;
+    }
+
+    ERR("Unexpected property: 0x%04x\n", prop);
+    Context->setError(AL_INVALID_ENUM, "Invalid source integer64 property 0x%04x", prop);
+}
+catch(check_exception&) {
+}
+
+
+template<typename T, size_t N>
+auto GetSizeChecker(ALCcontext *const Context, const SourceProp prop, const al::span<T,N> values)
+{
+    return [=](size_t expect) -> void
+    {
+        if(values.size() == expect) LIKELY return;
+        Context->setError(AL_INVALID_ENUM, "Property 0x%04x expects %zu value(s), got %zu",
+            prop, expect, values.size());
+        throw check_size_exception{};
+    };
+}
+
+bool GetSourcedv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop, const al::span<double> values);
+bool GetSourceiv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop, const al::span<int> values);
+bool GetSourcei64v(ALsource *const Source, ALCcontext *const Context, const SourceProp prop, const al::span<int64_t> values);
+
+bool GetSourcedv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop,
+    const al::span<double> values)
+try {
+    auto CheckSize = GetSizeChecker(Context, prop, values);
+    ALCdevice *device{Context->mALDevice.get()};
+    ClockLatency clocktime;
+    nanoseconds srcclock;
+    int ivals[MaxValues];
+    bool err;
+
+    switch(prop)
+    {
+    case AL_GAIN:
+        CheckSize(1);
+        values[0] = Source->Gain;
+        return true;
+
+    case AL_PITCH:
+        CheckSize(1);
+        values[0] = Source->Pitch;
+        return true;
+
+    case AL_MAX_DISTANCE:
+        CheckSize(1);
+        values[0] = Source->MaxDistance;
+        return true;
+
+    case AL_ROLLOFF_FACTOR:
+        CheckSize(1);
+        values[0] = Source->RolloffFactor;
+        return true;
+
+    case AL_REFERENCE_DISTANCE:
+        CheckSize(1);
+        values[0] = Source->RefDistance;
+        return true;
+
+    case AL_CONE_INNER_ANGLE:
+        CheckSize(1);
+        values[0] = Source->InnerAngle;
+        return true;
+
+    case AL_CONE_OUTER_ANGLE:
+        CheckSize(1);
+        values[0] = Source->OuterAngle;
+        return true;
+
+    case AL_MIN_GAIN:
+        CheckSize(1);
+        values[0] = Source->MinGain;
+        return true;
+
+    case AL_MAX_GAIN:
+        CheckSize(1);
+        values[0] = Source->MaxGain;
+        return true;
+
+    case AL_CONE_OUTER_GAIN:
+        CheckSize(1);
+        values[0] = Source->OuterGain;
+        return true;
+
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+        CheckSize(1);
+        values[0] = GetSourceOffset(Source, prop, Context);
+        return true;
+
+    case AL_CONE_OUTER_GAINHF:
+        CheckSize(1);
+        values[0] = Source->OuterGainHF;
+        return true;
+
+    case AL_AIR_ABSORPTION_FACTOR:
+        CheckSize(1);
+        values[0] = Source->AirAbsorptionFactor;
+        return true;
+
+    case AL_ROOM_ROLLOFF_FACTOR:
+        CheckSize(1);
+        values[0] = Source->RoomRolloffFactor;
+        return true;
+
+    case AL_DOPPLER_FACTOR:
+        CheckSize(1);
+        values[0] = Source->DopplerFactor;
+        return true;
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        break;
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+            break;
+
+        CheckSize(1);
+        values[0] = Source->Radius;
+        return true;
+
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        CheckSize(1);
+        values[0] = Source->EnhWidth;
+        return true;
+
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SEC_LENGTH_SOFT:
+        CheckSize(1);
+        values[0] = GetSourceLength(Source, prop);
+        return true;
+
+    case AL_STEREO_ANGLES:
+        CheckSize(2);
+        values[0] = Source->StereoPan[0];
+        values[1] = Source->StereoPan[1];
+        return true;
+
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+        CheckSize(2);
+        /* Get the source offset with the clock time first. Then get the clock
+         * time with the device latency. Order is important.
+         */
+        values[0] = GetSourceSecOffset(Source, Context, &srcclock);
+        {
+            std::lock_guard<std::mutex> _{device->StateLock};
+            clocktime = GetClockLatency(device, device->Backend.get());
+        }
+        if(srcclock == clocktime.ClockTime)
+            values[1] = static_cast<double>(clocktime.Latency.count()) / 1000000000.0;
+        else
+        {
+            /* If the clock time incremented, reduce the latency by that much
+             * since it's that much closer to the source offset it got earlier.
+             */
+            const nanoseconds diff{clocktime.ClockTime - srcclock};
+            const nanoseconds latency{clocktime.Latency - std::min(clocktime.Latency, diff)};
+            values[1] = static_cast<double>(latency.count()) / 1000000000.0;
+        }
+        return true;
+
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+        CheckSize(2);
+        values[0] = GetSourceSecOffset(Source, Context, &srcclock);
+        values[1] = static_cast<double>(srcclock.count()) / 1000000000.0;
+        return true;
+
+    case AL_POSITION:
+        CheckSize(3);
+        values[0] = Source->Position[0];
+        values[1] = Source->Position[1];
+        values[2] = Source->Position[2];
+        return true;
+
+    case AL_VELOCITY:
+        CheckSize(3);
+        values[0] = Source->Velocity[0];
+        values[1] = Source->Velocity[1];
+        values[2] = Source->Velocity[2];
+        return true;
+
+    case AL_DIRECTION:
+        CheckSize(3);
+        values[0] = Source->Direction[0];
+        values[1] = Source->Direction[1];
+        values[2] = Source->Direction[2];
+        return true;
+
+    case AL_ORIENTATION:
+        CheckSize(6);
+        values[0] = Source->OrientAt[0];
+        values[1] = Source->OrientAt[1];
+        values[2] = Source->OrientAt[2];
+        values[3] = Source->OrientUp[0];
+        values[4] = Source->OrientUp[1];
+        values[5] = Source->OrientUp[2];
+        return true;
+
+    /* 1x int */
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_SOURCE_STATE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_SOURCE_TYPE:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_DISTANCE_MODEL:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_STEREO_MODE_SOFT:
+        CheckSize(1);
+        if((err=GetSourceiv(Source, Context, prop, {ivals, 1u})) != false)
+            values[0] = static_cast<double>(ivals[0]);
+        return err;
+
+    case AL_BUFFER:
+    case AL_DIRECT_FILTER:
+    case AL_AUXILIARY_SEND_FILTER:
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+        break;
+    }
+
+    ERR("Unexpected property: 0x%04x\n", prop);
+    Context->setError(AL_INVALID_ENUM, "Invalid source double property 0x%04x", prop);
+    return false;
+}
+catch(check_exception&) {
+    return false;
+}
+
+bool GetSourceiv(ALsource *const Source, ALCcontext *const Context, const SourceProp prop,
+    const al::span<int> values)
+try {
+    auto CheckSize = GetSizeChecker(Context, prop, values);
+    double dvals[MaxValues];
+    bool err;
+
+    switch(prop)
+    {
+    case AL_SOURCE_RELATIVE:
+        CheckSize(1);
+        values[0] = Source->HeadRelative;
+        return true;
+
+    case AL_LOOPING:
+        CheckSize(1);
+        values[0] = Source->Looping;
+        return true;
+
+    case AL_BUFFER:
+        CheckSize(1);
+        {
+            ALbufferQueueItem *BufferList{};
+            /* HACK: This query should technically only return the buffer set
+             * on a static source. However, some apps had used it to detect
+             * when a streaming source changed buffers, so report the current
+             * buffer's ID when playing.
+             */
+            if(Source->SourceType == AL_STATIC || Source->state == AL_INITIAL)
+            {
+                if(!Source->mQueue.empty())
+                    BufferList = &Source->mQueue.front();
+            }
+            else if(Voice *voice{GetSourceVoice(Source, Context)})
+            {
+                VoiceBufferItem *Current{voice->mCurrentBuffer.load(std::memory_order_relaxed)};
+                BufferList = static_cast<ALbufferQueueItem*>(Current);
+            }
+            ALbuffer *buffer{BufferList ? BufferList->mBuffer : nullptr};
+            values[0] = buffer ? static_cast<int>(buffer->id) : 0;
+        }
+        return true;
+
+    case AL_SOURCE_STATE:
+        CheckSize(1);
+        values[0] = GetSourceState(Source, GetSourceVoice(Source, Context));
+        return true;
+
+    case AL_BUFFERS_QUEUED:
+        CheckSize(1);
+        values[0] = static_cast<int>(Source->mQueue.size());
+        return true;
+
+    case AL_BUFFERS_PROCESSED:
+        CheckSize(1);
+        if(Source->Looping || Source->SourceType != AL_STREAMING)
+        {
+            /* Buffers on a looping source are in a perpetual state of PENDING,
+             * so don't report any as PROCESSED
+             */
+            values[0] = 0;
+        }
+        else
+        {
+            int played{0};
+            if(Source->state != AL_INITIAL)
+            {
+                const VoiceBufferItem *Current{nullptr};
+                if(Voice *voice{GetSourceVoice(Source, Context)})
+                    Current = voice->mCurrentBuffer.load(std::memory_order_relaxed);
+                for(auto &item : Source->mQueue)
+                {
+                    if(&item == Current)
+                        break;
+                    ++played;
+                }
+            }
+            values[0] = played;
+        }
+        return true;
+
+    case AL_SOURCE_TYPE:
+        CheckSize(1);
+        values[0] = Source->SourceType;
+        return true;
+
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+        CheckSize(1);
+        values[0] = Source->DryGainHFAuto;
+        return true;
+
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+        CheckSize(1);
+        values[0] = Source->WetGainAuto;
+        return true;
+
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+        CheckSize(1);
+        values[0] = Source->WetGainHFAuto;
+        return true;
+
+    case AL_DIRECT_CHANNELS_SOFT:
+        CheckSize(1);
+        values[0] = EnumFromDirectMode(Source->DirectChannels);
+        return true;
+
+    case AL_DISTANCE_MODEL:
+        CheckSize(1);
+        values[0] = ALenumFromDistanceModel(Source->mDistanceModel);
+        return true;
+
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SEC_LENGTH_SOFT:
+        CheckSize(1);
+        values[0] = static_cast<int>(mind(GetSourceLength(Source, prop),
+            std::numeric_limits<int>::max()));
+        return true;
+
+    case AL_SOURCE_RESAMPLER_SOFT:
+        CheckSize(1);
+        values[0] = static_cast<int>(Source->mResampler);
+        return true;
+
+    case AL_SOURCE_SPATIALIZE_SOFT:
+        CheckSize(1);
+        values[0] = EnumFromSpatializeMode(Source->mSpatialize);
+        return true;
+
+    case AL_STEREO_MODE_SOFT:
+        CheckSize(1);
+        values[0] = EnumFromStereoMode(Source->mStereoMode);
+        return true;
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        if(sBufferSubDataCompat)
+        {
+            CheckSize(2);
+            const auto offset = GetSourceOffset(Source, AL_SAMPLE_OFFSET, Context);
+            /* FIXME: values[1] should be ahead of values[0] by the device
+             * update time. It needs to clamp or wrap the length of the buffer
+             * queue.
+             */
+            values[0] = static_cast<int>(mind(offset, std::numeric_limits<int>::max()));
+            values[1] = values[0];
+            return true;
+        }
+        break;
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+        {
+            CheckSize(2);
+            const auto offset = GetSourceOffset(Source, AL_BYTE_OFFSET, Context);
+            /* FIXME: values[1] should be ahead of values[0] by the device
+             * update time. It needs to clamp or wrap the length of the buffer
+             * queue.
+             */
+            values[0] = static_cast<int>(mind(offset, std::numeric_limits<int>::max()));
+            values[1] = values[0];
+            return true;
+        }
+        /*fall-through*/
+
+    /* 1x float/double */
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_REFERENCE_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+    case AL_DOPPLER_FACTOR:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        CheckSize(1);
+        if((err=GetSourcedv(Source, Context, prop, {dvals, 1u})) != false)
+            values[0] = static_cast<int>(dvals[0]);
+        return err;
+
+    /* 3x float/double */
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        CheckSize(3);
+        if((err=GetSourcedv(Source, Context, prop, {dvals, 3u})) != false)
+        {
+            values[0] = static_cast<int>(dvals[0]);
+            values[1] = static_cast<int>(dvals[1]);
+            values[2] = static_cast<int>(dvals[2]);
+        }
+        return err;
+
+    /* 6x float/double */
+    case AL_ORIENTATION:
+        CheckSize(6);
+        if((err=GetSourcedv(Source, Context, prop, {dvals, 6u})) != false)
+        {
+            values[0] = static_cast<int>(dvals[0]);
+            values[1] = static_cast<int>(dvals[1]);
+            values[2] = static_cast<int>(dvals[2]);
+            values[3] = static_cast<int>(dvals[3]);
+            values[4] = static_cast<int>(dvals[4]);
+            values[5] = static_cast<int>(dvals[5]);
+        }
+        return err;
+
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+        break; /* i64 only */
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+        break; /* Double only */
+    case AL_STEREO_ANGLES:
+        break; /* Float/double only */
+
+    case AL_DIRECT_FILTER:
+    case AL_AUXILIARY_SEND_FILTER:
+        break; /* ??? */
+    }
+
+    ERR("Unexpected property: 0x%04x\n", prop);
+    Context->setError(AL_INVALID_ENUM, "Invalid source integer property 0x%04x", prop);
+    return false;
+}
+catch(check_exception&) {
+    return false;
+}
+
+bool GetSourcei64v(ALsource *const Source, ALCcontext *const Context, const SourceProp prop,
+    const al::span<int64_t> values)
+try {
+    auto CheckSize = GetSizeChecker(Context, prop, values);
+    ALCdevice *device{Context->mALDevice.get()};
+    ClockLatency clocktime;
+    nanoseconds srcclock;
+    double dvals[MaxValues];
+    int ivals[MaxValues];
+    bool err;
+
+    switch(prop)
+    {
+    case AL_BYTE_LENGTH_SOFT:
+    case AL_SAMPLE_LENGTH_SOFT:
+    case AL_SEC_LENGTH_SOFT:
+        CheckSize(1);
+        values[0] = static_cast<int64_t>(GetSourceLength(Source, prop));
+        return true;
+
+    case AL_SAMPLE_OFFSET_LATENCY_SOFT:
+        CheckSize(2);
+        /* Get the source offset with the clock time first. Then get the clock
+         * time with the device latency. Order is important.
+         */
+        values[0] = GetSourceSampleOffset(Source, Context, &srcclock);
+        {
+            std::lock_guard<std::mutex> _{device->StateLock};
+            clocktime = GetClockLatency(device, device->Backend.get());
+        }
+        if(srcclock == clocktime.ClockTime)
+            values[1] = clocktime.Latency.count();
+        else
+        {
+            /* If the clock time incremented, reduce the latency by that much
+             * since it's that much closer to the source offset it got earlier.
+             */
+            const nanoseconds diff{clocktime.ClockTime - srcclock};
+            values[1] = nanoseconds{clocktime.Latency - std::min(clocktime.Latency, diff)}.count();
+        }
+        return true;
+
+    case AL_SAMPLE_OFFSET_CLOCK_SOFT:
+        CheckSize(2);
+        values[0] = GetSourceSampleOffset(Source, Context, &srcclock);
+        values[1] = srcclock.count();
+        return true;
+
+    case AL_SAMPLE_RW_OFFSETS_SOFT:
+        if(sBufferSubDataCompat)
+        {
+            CheckSize(2);
+            /* FIXME: values[1] should be ahead of values[0] by the device
+             * update time. It needs to clamp or wrap the length of the buffer
+             * queue.
+             */
+            values[0] = static_cast<int64_t>(GetSourceOffset(Source, AL_SAMPLE_OFFSET, Context));
+            values[1] = values[0];
+            return true;
+        }
+        break;
+    case AL_SOURCE_RADIUS: /*AL_BYTE_RW_OFFSETS_SOFT:*/
+        if(sBufferSubDataCompat)
+        {
+            CheckSize(2);
+            /* FIXME: values[1] should be ahead of values[0] by the device
+             * update time. It needs to clamp or wrap the length of the buffer
+             * queue.
+             */
+            values[0] = static_cast<int64_t>(GetSourceOffset(Source, AL_BYTE_OFFSET, Context));
+            values[1] = values[0];
+            return true;
+        }
+        /*fall-through*/
+
+    /* 1x float/double */
+    case AL_CONE_INNER_ANGLE:
+    case AL_CONE_OUTER_ANGLE:
+    case AL_PITCH:
+    case AL_GAIN:
+    case AL_MIN_GAIN:
+    case AL_MAX_GAIN:
+    case AL_REFERENCE_DISTANCE:
+    case AL_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAIN:
+    case AL_MAX_DISTANCE:
+    case AL_SEC_OFFSET:
+    case AL_SAMPLE_OFFSET:
+    case AL_BYTE_OFFSET:
+    case AL_DOPPLER_FACTOR:
+    case AL_AIR_ABSORPTION_FACTOR:
+    case AL_ROOM_ROLLOFF_FACTOR:
+    case AL_CONE_OUTER_GAINHF:
+    case AL_SUPER_STEREO_WIDTH_SOFT:
+        CheckSize(1);
+        if((err=GetSourcedv(Source, Context, prop, {dvals, 1u})) != false)
+            values[0] = static_cast<int64_t>(dvals[0]);
+        return err;
+
+    /* 3x float/double */
+    case AL_POSITION:
+    case AL_VELOCITY:
+    case AL_DIRECTION:
+        CheckSize(3);
+        if((err=GetSourcedv(Source, Context, prop, {dvals, 3u})) != false)
+        {
+            values[0] = static_cast<int64_t>(dvals[0]);
+            values[1] = static_cast<int64_t>(dvals[1]);
+            values[2] = static_cast<int64_t>(dvals[2]);
+        }
+        return err;
+
+    /* 6x float/double */
+    case AL_ORIENTATION:
+        CheckSize(6);
+        if((err=GetSourcedv(Source, Context, prop, {dvals, 6u})) != false)
+        {
+            values[0] = static_cast<int64_t>(dvals[0]);
+            values[1] = static_cast<int64_t>(dvals[1]);
+            values[2] = static_cast<int64_t>(dvals[2]);
+            values[3] = static_cast<int64_t>(dvals[3]);
+            values[4] = static_cast<int64_t>(dvals[4]);
+            values[5] = static_cast<int64_t>(dvals[5]);
+        }
+        return err;
+
+    /* 1x int */
+    case AL_SOURCE_RELATIVE:
+    case AL_LOOPING:
+    case AL_SOURCE_STATE:
+    case AL_BUFFERS_QUEUED:
+    case AL_BUFFERS_PROCESSED:
+    case AL_SOURCE_TYPE:
+    case AL_DIRECT_FILTER_GAINHF_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAIN_AUTO:
+    case AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO:
+    case AL_DIRECT_CHANNELS_SOFT:
+    case AL_DISTANCE_MODEL:
+    case AL_SOURCE_RESAMPLER_SOFT:
+    case AL_SOURCE_SPATIALIZE_SOFT:
+    case AL_STEREO_MODE_SOFT:
+        CheckSize(1);
+        if((err=GetSourceiv(Source, Context, prop, {ivals, 1u})) != false)
+            values[0] = ivals[0];
+        return err;
+
+    /* 1x uint */
+    case AL_BUFFER:
+    case AL_DIRECT_FILTER:
+        CheckSize(1);
+        if((err=GetSourceiv(Source, Context, prop, {ivals, 1u})) != false)
+            values[0] = static_cast<ALuint>(ivals[0]);
+        return err;
+
+    /* 3x uint */
+    case AL_AUXILIARY_SEND_FILTER:
+        CheckSize(3);
+        if((err=GetSourceiv(Source, Context, prop, {ivals, 3u})) != false)
+        {
+            values[0] = static_cast<ALuint>(ivals[0]);
+            values[1] = static_cast<ALuint>(ivals[1]);
+            values[2] = static_cast<ALuint>(ivals[2]);
+        }
+        return err;
+
+    case AL_SEC_OFFSET_LATENCY_SOFT:
+    case AL_SEC_OFFSET_CLOCK_SOFT:
+        break; /* Double only */
+    case AL_STEREO_ANGLES:
+        break; /* Float/double only */
+    }
+
+    ERR("Unexpected property: 0x%04x\n", prop);
+    Context->setError(AL_INVALID_ENUM, "Invalid source integer64 property 0x%04x", prop);
+    return false;
+}
+catch(check_exception&) {
+    return false;
+}
+
+
+void StartSources(ALCcontext *const context, const al::span<ALsource*> srchandles,
+    const nanoseconds start_time=nanoseconds::min())
+{
+    ALCdevice *device{context->mALDevice.get()};
+    /* If the device is disconnected, and voices stop on disconnect, go right
+     * to stopped.
+     */
+    if(!device->Connected.load(std::memory_order_acquire)) UNLIKELY
+    {
+        if(context->mStopVoicesOnDisconnect.load(std::memory_order_acquire))
+        {
+            for(ALsource *source : srchandles)
+            {
+                /* TODO: Send state change event? */
+                source->Offset = 0.0;
+                source->OffsetType = AL_NONE;
+                source->state = AL_STOPPED;
+            }
+            return;
+        }
+    }
+
+    /* Count the number of reusable voices. */
+    auto voicelist = context->getVoicesSpan();
+    size_t free_voices{0};
+    for(const Voice *voice : voicelist)
+    {
+        free_voices += (voice->mPlayState.load(std::memory_order_acquire) == Voice::Stopped
+            && voice->mSourceID.load(std::memory_order_relaxed) == 0u
+            && voice->mPendingChange.load(std::memory_order_relaxed) == false);
+        if(free_voices == srchandles.size())
+            break;
+    }
+    if(srchandles.size() != free_voices) UNLIKELY
+    {
+        const size_t inc_amount{srchandles.size() - free_voices};
+        auto &allvoices = *context->mVoices.load(std::memory_order_relaxed);
+        if(inc_amount > allvoices.size() - voicelist.size())
+        {
+            /* Increase the number of voices to handle the request. */
+            context->allocVoices(inc_amount - (allvoices.size() - voicelist.size()));
+        }
+        context->mActiveVoiceCount.fetch_add(inc_amount, std::memory_order_release);
+        voicelist = context->getVoicesSpan();
+    }
+
+    auto voiceiter = voicelist.begin();
+    ALuint vidx{0};
+    VoiceChange *tail{}, *cur{};
+    for(ALsource *source : srchandles)
+    {
+        /* Check that there is a queue containing at least one valid, non zero
+         * length buffer.
+         */
+        auto find_buffer = [](ALbufferQueueItem &entry) noexcept
+        { return entry.mSampleLen != 0 || entry.mCallback != nullptr; };
+        auto BufferList = std::find_if(source->mQueue.begin(), source->mQueue.end(), find_buffer);
+
+        /* If there's nothing to play, go right to stopped. */
+        if(BufferList == source->mQueue.end()) UNLIKELY
+        {
+            /* NOTE: A source without any playable buffers should not have a
+             * Voice since it shouldn't be in a playing or paused state. So
+             * there's no need to look up its voice and clear the source.
+             */
+            source->Offset = 0.0;
+            source->OffsetType = AL_NONE;
+            source->state = AL_STOPPED;
+            continue;
+        }
+
+        if(!cur)
+            cur = tail = GetVoiceChanger(context);
+        else
+        {
+            cur->mNext.store(GetVoiceChanger(context), std::memory_order_relaxed);
+            cur = cur->mNext.load(std::memory_order_relaxed);
+        }
+
+        Voice *voice{GetSourceVoice(source, context)};
+        switch(GetSourceState(source, voice))
+        {
+        case AL_PAUSED:
+            /* A source that's paused simply resumes. If there's no voice, it
+             * was lost from a disconnect, so just start over with a new one.
+             */
+            cur->mOldVoice = nullptr;
+            if(!voice) break;
+            cur->mVoice = voice;
+            cur->mSourceID = source->id;
+            cur->mState = VChangeState::Play;
+            source->state = AL_PLAYING;
+#ifdef ALSOFT_EAX
+            if(context->hasEax())
+                source->eaxCommit();
+#endif // ALSOFT_EAX
+            continue;
+
+        case AL_PLAYING:
+            /* A source that's already playing is restarted from the beginning.
+             * Stop the current voice and start a new one so it properly cross-
+             * fades back to the beginning.
+             */
+            if(voice)
+                voice->mPendingChange.store(true, std::memory_order_relaxed);
+            cur->mOldVoice = voice;
+            voice = nullptr;
+            break;
+
+        default:
+            assert(voice == nullptr);
+            cur->mOldVoice = nullptr;
+#ifdef ALSOFT_EAX
+            if(context->hasEax())
+                source->eaxCommit();
+#endif // ALSOFT_EAX
+            break;
+        }
+
+        /* Find the next unused voice to play this source with. */
+        for(;voiceiter != voicelist.end();++voiceiter,++vidx)
+        {
+            Voice *v{*voiceiter};
+            if(v->mPlayState.load(std::memory_order_acquire) == Voice::Stopped
+                && v->mSourceID.load(std::memory_order_relaxed) == 0u
+                && v->mPendingChange.load(std::memory_order_relaxed) == false)
+            {
+                voice = v;
+                break;
+            }
+        }
+        ASSUME(voice != nullptr);
+
+        voice->mPosition.store(0, std::memory_order_relaxed);
+        voice->mPositionFrac.store(0, std::memory_order_relaxed);
+        voice->mCurrentBuffer.store(&source->mQueue.front(), std::memory_order_relaxed);
+        voice->mStartTime = start_time;
+        voice->mFlags.reset();
+        /* A source that's not playing or paused has any offset applied when it
+         * starts playing.
+         */
+        if(const ALenum offsettype{source->OffsetType})
+        {
+            const double offset{source->Offset};
+            source->OffsetType = AL_NONE;
+            source->Offset = 0.0;
+            if(auto vpos = GetSampleOffset(source->mQueue, offsettype, offset))
+            {
+                voice->mPosition.store(vpos->pos, std::memory_order_relaxed);
+                voice->mPositionFrac.store(vpos->frac, std::memory_order_relaxed);
+                voice->mCurrentBuffer.store(vpos->bufferitem, std::memory_order_relaxed);
+                if(vpos->pos > 0 || (vpos->pos == 0 && vpos->frac > 0)
+                    || vpos->bufferitem != &source->mQueue.front())
+                    voice->mFlags.set(VoiceIsFading);
+            }
+        }
+        InitVoice(voice, source, al::to_address(BufferList), context, device);
+
+        source->VoiceIdx = vidx;
+        source->state = AL_PLAYING;
+
+        cur->mVoice = voice;
+        cur->mSourceID = source->id;
+        cur->mState = VChangeState::Play;
+    }
+    if(tail) LIKELY
+        SendVoiceChanges(context, tail);
+}
+
+} // namespace
+
+AL_API void AL_APIENTRY alGenSources(ALsizei n, ALuint *sources)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Generating %d sources", n);
+    if(n <= 0) UNLIKELY return;
+
+    std::unique_lock<std::mutex> srclock{context->mSourceLock};
+    ALCdevice *device{context->mALDevice.get()};
+    if(static_cast<ALuint>(n) > device->SourcesMax-context->mNumSources)
+    {
+        context->setError(AL_OUT_OF_MEMORY, "Exceeding %u source limit (%u + %d)",
+            device->SourcesMax, context->mNumSources, n);
+        return;
+    }
+    if(!EnsureSources(context.get(), static_cast<ALuint>(n)))
+    {
+        context->setError(AL_OUT_OF_MEMORY, "Failed to allocate %d source%s", n, (n==1)?"":"s");
+        return;
+    }
+
+    if(n == 1)
+    {
+        ALsource *source{AllocSource(context.get())};
+        sources[0] = source->id;
+
+#ifdef ALSOFT_EAX
+        source->eaxInitialize(context.get());
+#endif // ALSOFT_EAX
+    }
+    else
+    {
+        al::vector<ALuint> ids;
+        ids.reserve(static_cast<ALuint>(n));
+        do {
+            ALsource *source{AllocSource(context.get())};
+            ids.emplace_back(source->id);
+
+#ifdef ALSOFT_EAX
+            source->eaxInitialize(context.get());
+#endif // ALSOFT_EAX
+        } while(--n);
+        std::copy(ids.cbegin(), ids.cend(), sources);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDeleteSources(ALsizei n, const ALuint *sources)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Deleting %d sources", n);
+    if(n <= 0) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+
+    /* Check that all Sources are valid */
+    auto validate_source = [&context](const ALuint sid) -> bool
+    { return LookupSource(context.get(), sid) != nullptr; };
+
+    const ALuint *sources_end = sources + n;
+    auto invsrc = std::find_if_not(sources, sources_end, validate_source);
+    if(invsrc != sources_end) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", *invsrc);
+
+    /* All good. Delete source IDs. */
+    auto delete_source = [&context](const ALuint sid) -> void
+    {
+        ALsource *src{LookupSource(context.get(), sid)};
+        if(src) FreeSource(context.get(), src);
+    };
+    std::for_each(sources, sources_end, delete_source);
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alIsSource(ALuint source)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(context) LIKELY
+    {
+        std::lock_guard<std::mutex> _{context->mSourceLock};
+        if(LookupSource(context.get(), source) != nullptr)
+            return AL_TRUE;
+    }
+    return AL_FALSE;
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourcef(ALuint source, ALenum param, ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+        SetSourcefv(Source, context.get(), static_cast<SourceProp>(param), {&value, 1u});
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSource3f(ALuint source, ALenum param, ALfloat value1, ALfloat value2, ALfloat value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+    {
+        const float fvals[3]{ value1, value2, value3 };
+        SetSourcefv(Source, context.get(), static_cast<SourceProp>(param), fvals);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourcefv(ALuint source, ALenum param, const ALfloat *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{FloatValsByProp(param)};
+    SetSourcefv(Source, context.get(), static_cast<SourceProp>(param), {values, count});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourcedSOFT(ALuint source, ALenum param, ALdouble value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+    {
+        const float fval[1]{static_cast<float>(value)};
+        SetSourcefv(Source, context.get(), static_cast<SourceProp>(param), fval);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSource3dSOFT(ALuint source, ALenum param, ALdouble value1, ALdouble value2, ALdouble value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+    {
+        const float fvals[3]{static_cast<float>(value1), static_cast<float>(value2),
+            static_cast<float>(value3)};
+        SetSourcefv(Source, context.get(), static_cast<SourceProp>(param), fvals);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourcedvSOFT(ALuint source, ALenum param, const ALdouble *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{DoubleValsByProp(param)};
+    float fvals[MaxValues];
+    std::copy_n(values, count, fvals);
+    SetSourcefv(Source, context.get(), static_cast<SourceProp>(param), {fvals, count});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourcei(ALuint source, ALenum param, ALint value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+        SetSourceiv(Source, context.get(), static_cast<SourceProp>(param), {&value, 1u});
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSource3i(ALuint source, ALenum param, ALint value1, ALint value2, ALint value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+    {
+        const int ivals[3]{ value1, value2, value3 };
+        SetSourceiv(Source, context.get(), static_cast<SourceProp>(param), ivals);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourceiv(ALuint source, ALenum param, const ALint *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source = LookupSource(context.get(), source);
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{IntValsByProp(param)};
+    SetSourceiv(Source, context.get(), static_cast<SourceProp>(param), {values, count});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+        SetSourcei64v(Source, context.get(), static_cast<SourceProp>(param), {&value, 1u});
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT value1, ALint64SOFT value2, ALint64SOFT value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else
+    {
+        const int64_t i64vals[3]{ value1, value2, value3 };
+        SetSourcei64v(Source, context.get(), static_cast<SourceProp>(param), i64vals);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourcei64vSOFT(ALuint source, ALenum param, const ALint64SOFT *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    std::lock_guard<std::mutex> __{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{Int64ValsByProp(param)};
+    SetSourcei64v(Source, context.get(), static_cast<SourceProp>(param), {values, count});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetSourcef(ALuint source, ALenum param, ALfloat *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!value) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+    {
+        double dval[1];
+        if(GetSourcedv(Source, context.get(), static_cast<SourceProp>(param), dval))
+            *value = static_cast<float>(dval[0]);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSource3f(ALuint source, ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!(value1 && value2 && value3)) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+    {
+        double dvals[3];
+        if(GetSourcedv(Source, context.get(), static_cast<SourceProp>(param), dvals))
+        {
+            *value1 = static_cast<float>(dvals[0]);
+            *value2 = static_cast<float>(dvals[1]);
+            *value3 = static_cast<float>(dvals[2]);
+        }
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSourcefv(ALuint source, ALenum param, ALfloat *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{FloatValsByProp(param)};
+    double dvals[MaxValues];
+    if(GetSourcedv(Source, context.get(), static_cast<SourceProp>(param), {dvals, count}))
+        std::copy_n(dvals, count, values);
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetSourcedSOFT(ALuint source, ALenum param, ALdouble *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!value) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+        GetSourcedv(Source, context.get(), static_cast<SourceProp>(param), {value, 1u});
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSource3dSOFT(ALuint source, ALenum param, ALdouble *value1, ALdouble *value2, ALdouble *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!(value1 && value2 && value3)) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+    {
+        double dvals[3];
+        if(GetSourcedv(Source, context.get(), static_cast<SourceProp>(param), dvals))
+        {
+            *value1 = dvals[0];
+            *value2 = dvals[1];
+            *value3 = dvals[2];
+        }
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSourcedvSOFT(ALuint source, ALenum param, ALdouble *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{DoubleValsByProp(param)};
+    GetSourcedv(Source, context.get(), static_cast<SourceProp>(param), {values, count});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetSourcei(ALuint source, ALenum param, ALint *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!value) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+        GetSourceiv(Source, context.get(), static_cast<SourceProp>(param), {value, 1u});
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSource3i(ALuint source, ALenum param, ALint *value1, ALint *value2, ALint *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!(value1 && value2 && value3)) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+    {
+        int ivals[3];
+        if(GetSourceiv(Source, context.get(), static_cast<SourceProp>(param), ivals))
+        {
+            *value1 = ivals[0];
+            *value2 = ivals[1];
+            *value3 = ivals[2];
+        }
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSourceiv(ALuint source, ALenum param, ALint *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{IntValsByProp(param)};
+    GetSourceiv(Source, context.get(), static_cast<SourceProp>(param), {values, count});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alGetSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT *value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!value) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+        GetSourcei64v(Source, context.get(), static_cast<SourceProp>(param), {value, 1u});
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT *value1, ALint64SOFT *value2, ALint64SOFT *value3)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    else if(!(value1 && value2 && value3)) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else
+    {
+        int64_t i64vals[3];
+        if(GetSourcei64v(Source, context.get(), static_cast<SourceProp>(param), i64vals))
+        {
+            *value1 = i64vals[0];
+            *value2 = i64vals[1];
+            *value3 = i64vals[2];
+        }
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetSourcei64vSOFT(ALuint source, ALenum param, ALint64SOFT *values)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *Source{LookupSource(context.get(), source)};
+    if(!Source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+    if(!values) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "NULL pointer");
+
+    const ALuint count{Int64ValsByProp(param)};
+    GetSourcei64v(Source, context.get(), static_cast<SourceProp>(param), {values, count});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourcePlay(ALuint source)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *srchandle{LookupSource(context.get(), source)};
+    if(!srchandle)
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+
+    StartSources(context.get(), {&srchandle, 1});
+}
+END_API_FUNC
+
+void AL_APIENTRY alSourcePlayAtTimeSOFT(ALuint source, ALint64SOFT start_time)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(start_time < 0) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Invalid time point %" PRId64, start_time);
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *srchandle{LookupSource(context.get(), source)};
+    if(!srchandle)
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", source);
+
+    StartSources(context.get(), {&srchandle, 1}, nanoseconds{start_time});
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourcePlayv(ALsizei n, const ALuint *sources)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Playing %d sources", n);
+    if(n <= 0) UNLIKELY return;
+
+    al::vector<ALsource*> extra_sources;
+    std::array<ALsource*,8> source_storage;
+    al::span<ALsource*> srchandles;
+    if(static_cast<ALuint>(n) <= source_storage.size()) LIKELY
+        srchandles = {source_storage.data(), static_cast<ALuint>(n)};
+    else
+    {
+        extra_sources.resize(static_cast<ALuint>(n));
+        srchandles = {extra_sources.data(), extra_sources.size()};
+    }
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    for(auto &srchdl : srchandles)
+    {
+        srchdl = LookupSource(context.get(), *sources);
+        if(!srchdl) UNLIKELY
+            return context->setError(AL_INVALID_NAME, "Invalid source ID %u", *sources);
+        ++sources;
+    }
+
+    StartSources(context.get(), srchandles);
+}
+END_API_FUNC
+
+void AL_APIENTRY alSourcePlayAtTimevSOFT(ALsizei n, const ALuint *sources, ALint64SOFT start_time)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Playing %d sources", n);
+    if(n <= 0) UNLIKELY return;
+
+    if(start_time < 0) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Invalid time point %" PRId64, start_time);
+
+    al::vector<ALsource*> extra_sources;
+    std::array<ALsource*,8> source_storage;
+    al::span<ALsource*> srchandles;
+    if(static_cast<ALuint>(n) <= source_storage.size()) LIKELY
+        srchandles = {source_storage.data(), static_cast<ALuint>(n)};
+    else
+    {
+        extra_sources.resize(static_cast<ALuint>(n));
+        srchandles = {extra_sources.data(), extra_sources.size()};
+    }
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    for(auto &srchdl : srchandles)
+    {
+        srchdl = LookupSource(context.get(), *sources);
+        if(!srchdl)
+            return context->setError(AL_INVALID_NAME, "Invalid source ID %u", *sources);
+        ++sources;
+    }
+
+    StartSources(context.get(), srchandles, nanoseconds{start_time});
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourcePause(ALuint source)
+START_API_FUNC
+{ alSourcePausev(1, &source); }
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourcePausev(ALsizei n, const ALuint *sources)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Pausing %d sources", n);
+    if(n <= 0) UNLIKELY return;
+
+    al::vector<ALsource*> extra_sources;
+    std::array<ALsource*,8> source_storage;
+    al::span<ALsource*> srchandles;
+    if(static_cast<ALuint>(n) <= source_storage.size()) LIKELY
+        srchandles = {source_storage.data(), static_cast<ALuint>(n)};
+    else
+    {
+        extra_sources.resize(static_cast<ALuint>(n));
+        srchandles = {extra_sources.data(), extra_sources.size()};
+    }
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    for(auto &srchdl : srchandles)
+    {
+        srchdl = LookupSource(context.get(), *sources);
+        if(!srchdl)
+            return context->setError(AL_INVALID_NAME, "Invalid source ID %u", *sources);
+        ++sources;
+    }
+
+    /* Pausing has to be done in two steps. First, for each source that's
+     * detected to be playing, chamge the voice (asynchronously) to
+     * stopping/paused.
+     */
+    VoiceChange *tail{}, *cur{};
+    for(ALsource *source : srchandles)
+    {
+        Voice *voice{GetSourceVoice(source, context.get())};
+        if(GetSourceState(source, voice) == AL_PLAYING)
+        {
+            if(!cur)
+                cur = tail = GetVoiceChanger(context.get());
+            else
+            {
+                cur->mNext.store(GetVoiceChanger(context.get()), std::memory_order_relaxed);
+                cur = cur->mNext.load(std::memory_order_relaxed);
+            }
+            cur->mVoice = voice;
+            cur->mSourceID = source->id;
+            cur->mState = VChangeState::Pause;
+        }
+    }
+    if(tail) LIKELY
+    {
+        SendVoiceChanges(context.get(), tail);
+        /* Second, now that the voice changes have been sent, because it's
+         * possible that the voice stopped after it was detected playing and
+         * before the voice got paused, recheck that the source is still
+         * considered playing and set it to paused if so.
+         */
+        for(ALsource *source : srchandles)
+        {
+            Voice *voice{GetSourceVoice(source, context.get())};
+            if(GetSourceState(source, voice) == AL_PLAYING)
+                source->state = AL_PAUSED;
+        }
+    }
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourceStop(ALuint source)
+START_API_FUNC
+{ alSourceStopv(1, &source); }
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourceStopv(ALsizei n, const ALuint *sources)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Stopping %d sources", n);
+    if(n <= 0) UNLIKELY return;
+
+    al::vector<ALsource*> extra_sources;
+    std::array<ALsource*,8> source_storage;
+    al::span<ALsource*> srchandles;
+    if(static_cast<ALuint>(n) <= source_storage.size()) LIKELY
+        srchandles = {source_storage.data(), static_cast<ALuint>(n)};
+    else
+    {
+        extra_sources.resize(static_cast<ALuint>(n));
+        srchandles = {extra_sources.data(), extra_sources.size()};
+    }
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    for(auto &srchdl : srchandles)
+    {
+        srchdl = LookupSource(context.get(), *sources);
+        if(!srchdl)
+            return context->setError(AL_INVALID_NAME, "Invalid source ID %u", *sources);
+        ++sources;
+    }
+
+    VoiceChange *tail{}, *cur{};
+    for(ALsource *source : srchandles)
+    {
+        if(Voice *voice{GetSourceVoice(source, context.get())})
+        {
+            if(!cur)
+                cur = tail = GetVoiceChanger(context.get());
+            else
+            {
+                cur->mNext.store(GetVoiceChanger(context.get()), std::memory_order_relaxed);
+                cur = cur->mNext.load(std::memory_order_relaxed);
+            }
+            voice->mPendingChange.store(true, std::memory_order_relaxed);
+            cur->mVoice = voice;
+            cur->mSourceID = source->id;
+            cur->mState = VChangeState::Stop;
+            source->state = AL_STOPPED;
+        }
+        source->Offset = 0.0;
+        source->OffsetType = AL_NONE;
+        source->VoiceIdx = INVALID_VOICE_IDX;
+    }
+    if(tail) LIKELY
+        SendVoiceChanges(context.get(), tail);
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourceRewind(ALuint source)
+START_API_FUNC
+{ alSourceRewindv(1, &source); }
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourceRewindv(ALsizei n, const ALuint *sources)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(n < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Rewinding %d sources", n);
+    if(n <= 0) UNLIKELY return;
+
+    al::vector<ALsource*> extra_sources;
+    std::array<ALsource*,8> source_storage;
+    al::span<ALsource*> srchandles;
+    if(static_cast<ALuint>(n) <= source_storage.size()) LIKELY
+        srchandles = {source_storage.data(), static_cast<ALuint>(n)};
+    else
+    {
+        extra_sources.resize(static_cast<ALuint>(n));
+        srchandles = {extra_sources.data(), extra_sources.size()};
+    }
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    for(auto &srchdl : srchandles)
+    {
+        srchdl = LookupSource(context.get(), *sources);
+        if(!srchdl)
+            return context->setError(AL_INVALID_NAME, "Invalid source ID %u", *sources);
+        ++sources;
+    }
+
+    VoiceChange *tail{}, *cur{};
+    for(ALsource *source : srchandles)
+    {
+        Voice *voice{GetSourceVoice(source, context.get())};
+        if(source->state != AL_INITIAL)
+        {
+            if(!cur)
+                cur = tail = GetVoiceChanger(context.get());
+            else
+            {
+                cur->mNext.store(GetVoiceChanger(context.get()), std::memory_order_relaxed);
+                cur = cur->mNext.load(std::memory_order_relaxed);
+            }
+            if(voice)
+                voice->mPendingChange.store(true, std::memory_order_relaxed);
+            cur->mVoice = voice;
+            cur->mSourceID = source->id;
+            cur->mState = VChangeState::Reset;
+            source->state = AL_INITIAL;
+        }
+        source->Offset = 0.0;
+        source->OffsetType = AL_NONE;
+        source->VoiceIdx = INVALID_VOICE_IDX;
+    }
+    if(tail) LIKELY
+        SendVoiceChanges(context.get(), tail);
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourceQueueBuffers(ALuint src, ALsizei nb, const ALuint *buffers)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(nb < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Queueing %d buffers", nb);
+    if(nb <= 0) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *source{LookupSource(context.get(),src)};
+    if(!source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", src);
+
+    /* Can't queue on a Static Source */
+    if(source->SourceType == AL_STATIC) UNLIKELY
+        return context->setError(AL_INVALID_OPERATION, "Queueing onto static source %u", src);
+
+    /* Check for a valid Buffer, for its frequency and format */
+    ALCdevice *device{context->mALDevice.get()};
+    ALbuffer *BufferFmt{nullptr};
+    for(auto &item : source->mQueue)
+    {
+        BufferFmt = item.mBuffer;
+        if(BufferFmt) break;
+    }
+
+    std::unique_lock<std::mutex> buflock{device->BufferLock};
+    const size_t NewListStart{source->mQueue.size()};
+    ALbufferQueueItem *BufferList{nullptr};
+    for(ALsizei i{0};i < nb;i++)
+    {
+        bool fmt_mismatch{false};
+        ALbuffer *buffer{nullptr};
+        if(buffers[i] && (buffer=LookupBuffer(device, buffers[i])) == nullptr)
+        {
+            context->setError(AL_INVALID_NAME, "Queueing invalid buffer ID %u", buffers[i]);
+            goto buffer_error;
+        }
+        if(buffer)
+        {
+            if(buffer->mSampleRate < 1)
+            {
+                context->setError(AL_INVALID_OPERATION, "Queueing buffer %u with no format",
+                    buffer->id);
+                goto buffer_error;
+            }
+            if(buffer->mCallback)
+            {
+                context->setError(AL_INVALID_OPERATION, "Queueing callback buffer %u", buffer->id);
+                goto buffer_error;
+            }
+            if(buffer->MappedAccess != 0 && !(buffer->MappedAccess&AL_MAP_PERSISTENT_BIT_SOFT))
+            {
+                context->setError(AL_INVALID_OPERATION,
+                    "Queueing non-persistently mapped buffer %u", buffer->id);
+                goto buffer_error;
+            }
+        }
+
+        source->mQueue.emplace_back();
+        if(!BufferList)
+            BufferList = &source->mQueue.back();
+        else
+        {
+            auto &item = source->mQueue.back();
+            BufferList->mNext.store(&item, std::memory_order_relaxed);
+            BufferList = &item;
+        }
+        if(!buffer) continue;
+        BufferList->mBlockAlign = buffer->mBlockAlign;
+        BufferList->mSampleLen = buffer->mSampleLen;
+        BufferList->mLoopEnd = buffer->mSampleLen;
+        BufferList->mSamples = buffer->mData.data();
+        BufferList->mBuffer = buffer;
+        IncrementRef(buffer->ref);
+
+        if(BufferFmt == nullptr)
+            BufferFmt = buffer;
+        else
+        {
+            fmt_mismatch |= BufferFmt->mSampleRate != buffer->mSampleRate;
+            fmt_mismatch |= BufferFmt->mChannels != buffer->mChannels;
+            fmt_mismatch |= BufferFmt->mType != buffer->mType;
+            if(BufferFmt->isBFormat())
+            {
+                fmt_mismatch |= BufferFmt->mAmbiLayout != buffer->mAmbiLayout;
+                fmt_mismatch |= BufferFmt->mAmbiScaling != buffer->mAmbiScaling;
+            }
+            fmt_mismatch |= BufferFmt->mAmbiOrder != buffer->mAmbiOrder;
+        }
+        if(fmt_mismatch) UNLIKELY
+        {
+            context->setError(AL_INVALID_OPERATION, "Queueing buffer with mismatched format\n"
+                "  Expected: %uhz, %s, %s ; Got: %uhz, %s, %s\n", BufferFmt->mSampleRate,
+                NameFromFormat(BufferFmt->mType), NameFromFormat(BufferFmt->mChannels),
+                buffer->mSampleRate, NameFromFormat(buffer->mType),
+                NameFromFormat(buffer->mChannels));
+
+        buffer_error:
+            /* A buffer failed (invalid ID or format), so unlock and release
+             * each buffer we had.
+             */
+            auto iter = source->mQueue.begin() + ptrdiff_t(NewListStart);
+            for(;iter != source->mQueue.end();++iter)
+            {
+                if(ALbuffer *buf{iter->mBuffer})
+                    DecrementRef(buf->ref);
+            }
+            source->mQueue.resize(NewListStart);
+            return;
+        }
+    }
+    /* All buffers good. */
+    buflock.unlock();
+
+    /* Source is now streaming */
+    source->SourceType = AL_STREAMING;
+
+    if(NewListStart != 0)
+    {
+        auto iter = source->mQueue.begin() + ptrdiff_t(NewListStart);
+        (iter-1)->mNext.store(al::to_address(iter), std::memory_order_release);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSourceUnqueueBuffers(ALuint src, ALsizei nb, ALuint *buffers)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(nb < 0) UNLIKELY
+        context->setError(AL_INVALID_VALUE, "Unqueueing %d buffers", nb);
+    if(nb <= 0) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    ALsource *source{LookupSource(context.get(),src)};
+    if(!source) UNLIKELY
+        return context->setError(AL_INVALID_NAME, "Invalid source ID %u", src);
+
+    if(source->SourceType != AL_STREAMING) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Unqueueing from a non-streaming source %u",
+            src);
+    if(source->Looping) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Unqueueing from looping source %u", src);
+
+    /* Make sure enough buffers have been processed to unqueue. */
+    uint processed{0u};
+    if(source->state != AL_INITIAL) LIKELY
+    {
+        VoiceBufferItem *Current{nullptr};
+        if(Voice *voice{GetSourceVoice(source, context.get())})
+            Current = voice->mCurrentBuffer.load(std::memory_order_relaxed);
+        for(auto &item : source->mQueue)
+        {
+            if(&item == Current)
+                break;
+            ++processed;
+        }
+    }
+    if(processed < static_cast<ALuint>(nb)) UNLIKELY
+        return context->setError(AL_INVALID_VALUE, "Unqueueing %d buffer%s (only %u processed)",
+            nb, (nb==1)?"":"s", processed);
+
+    do {
+        auto &head = source->mQueue.front();
+        if(ALbuffer *buffer{head.mBuffer})
+        {
+            *(buffers++) = buffer->id;
+            DecrementRef(buffer->ref);
+        }
+        else
+            *(buffers++) = 0;
+        source->mQueue.pop_front();
+    } while(--nb);
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alSourceQueueBufferLayersSOFT(ALuint, ALsizei, const ALuint*)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    context->setError(AL_INVALID_OPERATION, "alSourceQueueBufferLayersSOFT not supported");
+}
+END_API_FUNC
+
+
+ALsource::ALsource()
+{
+    Direct.Gain = 1.0f;
+    Direct.GainHF = 1.0f;
+    Direct.HFReference = LOWPASSFREQREF;
+    Direct.GainLF = 1.0f;
+    Direct.LFReference = HIGHPASSFREQREF;
+    for(auto &send : Send)
+    {
+        send.Slot = nullptr;
+        send.Gain = 1.0f;
+        send.GainHF = 1.0f;
+        send.HFReference = LOWPASSFREQREF;
+        send.GainLF = 1.0f;
+        send.LFReference = HIGHPASSFREQREF;
+    }
+}
+
+ALsource::~ALsource()
+{
+    for(auto &item : mQueue)
+    {
+        if(ALbuffer *buffer{item.mBuffer})
+            DecrementRef(buffer->ref);
+    }
+
+    auto clear_send = [](ALsource::SendData &send) -> void
+    { if(send.Slot) DecrementRef(send.Slot->ref); };
+    std::for_each(Send.begin(), Send.end(), clear_send);
+}
+
+void UpdateAllSourceProps(ALCcontext *context)
+{
+    std::lock_guard<std::mutex> _{context->mSourceLock};
+    auto voicelist = context->getVoicesSpan();
+    ALuint vidx{0u};
+    for(Voice *voice : voicelist)
+    {
+        ALuint sid{voice->mSourceID.load(std::memory_order_acquire)};
+        ALsource *source = sid ? LookupSource(context, sid) : nullptr;
+        if(source && source->VoiceIdx == vidx)
+        {
+            if(std::exchange(source->mPropsDirty, false))
+                UpdateSourceProps(source, voice, context);
+        }
+        ++vidx;
+    }
+}
+
+SourceSubList::~SourceSubList()
+{
+    uint64_t usemask{~FreeMask};
+    while(usemask)
+    {
+        const int idx{al::countr_zero(usemask)};
+        usemask &= ~(1_u64 << idx);
+        al::destroy_at(Sources+idx);
+    }
+    FreeMask = ~usemask;
+    al_free(Sources);
+    Sources = nullptr;
+}
+
+
+#ifdef ALSOFT_EAX
+constexpr const ALsource::EaxFxSlotIds ALsource::eax4_fx_slot_ids;
+constexpr const ALsource::EaxFxSlotIds ALsource::eax5_fx_slot_ids;
+
+void ALsource::eaxInitialize(ALCcontext *context) noexcept
+{
+    assert(context != nullptr);
+    mEaxAlContext = context;
+
+    mEaxPrimaryFxSlotId = context->eaxGetPrimaryFxSlotIndex();
+    eax_set_defaults();
+
+    eax1_translate(mEax1.i, mEax);
+    mEaxVersion = 1;
+    mEaxChanged = true;
+}
+
+void ALsource::eaxDispatch(const EaxCall& call)
+{
+    call.is_get() ? eax_get(call) : eax_set(call);
+}
+
+ALsource* ALsource::EaxLookupSource(ALCcontext& al_context, ALuint source_id) noexcept
+{
+    return LookupSource(&al_context, source_id);
+}
+
+[[noreturn]] void ALsource::eax_fail(const char* message)
+{
+    throw Exception{message};
+}
+
+[[noreturn]] void ALsource::eax_fail_unknown_property_id()
+{
+    eax_fail("Unknown property id.");
+}
+
+[[noreturn]] void ALsource::eax_fail_unknown_version()
+{
+    eax_fail("Unknown version.");
+}
+
+[[noreturn]] void ALsource::eax_fail_unknown_active_fx_slot_id()
+{
+    eax_fail("Unknown active FX slot ID.");
+}
+
+[[noreturn]] void ALsource::eax_fail_unknown_receiving_fx_slot_id()
+{
+    eax_fail("Unknown receiving FX slot ID.");
+}
+
+void ALsource::eax_set_sends_defaults(EaxSends& sends, const EaxFxSlotIds& ids) noexcept
+{
+    for (auto i = size_t{}; i < EAX_MAX_FXSLOTS; ++i) {
+        auto& send = sends[i];
+        send.guidReceivingFXSlotID = *(ids[i]);
+        send.lSend = EAXSOURCE_DEFAULTSEND;
+        send.lSendHF = EAXSOURCE_DEFAULTSENDHF;
+        send.lOcclusion = EAXSOURCE_DEFAULTOCCLUSION;
+        send.flOcclusionLFRatio = EAXSOURCE_DEFAULTOCCLUSIONLFRATIO;
+        send.flOcclusionRoomRatio = EAXSOURCE_DEFAULTOCCLUSIONROOMRATIO;
+        send.flOcclusionDirectRatio = EAXSOURCE_DEFAULTOCCLUSIONDIRECTRATIO;
+        send.lExclusion = EAXSOURCE_DEFAULTEXCLUSION;
+        send.flExclusionLFRatio = EAXSOURCE_DEFAULTEXCLUSIONLFRATIO;
+    }
+}
+
+void ALsource::eax1_set_defaults(Eax1Props& props) noexcept
+{
+    props.fMix = EAX_REVERBMIX_USEDISTANCE;
+}
+
+void ALsource::eax1_set_defaults() noexcept
+{
+    eax1_set_defaults(mEax1.i);
+    mEax1.d = mEax1.i;
+}
+
+void ALsource::eax2_set_defaults(Eax2Props& props) noexcept
+{
+    props.lDirect = EAXSOURCE_DEFAULTDIRECT;
+    props.lDirectHF = EAXSOURCE_DEFAULTDIRECTHF;
+    props.lRoom = EAXSOURCE_DEFAULTROOM;
+    props.lRoomHF = EAXSOURCE_DEFAULTROOMHF;
+    props.flRoomRolloffFactor = EAXSOURCE_DEFAULTROOMROLLOFFFACTOR;
+    props.lObstruction = EAXSOURCE_DEFAULTOBSTRUCTION;
+    props.flObstructionLFRatio = EAXSOURCE_DEFAULTOBSTRUCTIONLFRATIO;
+    props.lOcclusion = EAXSOURCE_DEFAULTOCCLUSION;
+    props.flOcclusionLFRatio = EAXSOURCE_DEFAULTOCCLUSIONLFRATIO;
+    props.flOcclusionRoomRatio = EAXSOURCE_DEFAULTOCCLUSIONROOMRATIO;
+    props.lOutsideVolumeHF = EAXSOURCE_DEFAULTOUTSIDEVOLUMEHF;
+    props.flAirAbsorptionFactor = EAXSOURCE_DEFAULTAIRABSORPTIONFACTOR;
+    props.dwFlags = EAXSOURCE_DEFAULTFLAGS;
+}
+
+void ALsource::eax2_set_defaults() noexcept
+{
+    eax2_set_defaults(mEax2.i);
+    mEax2.d = mEax2.i;
+}
+
+void ALsource::eax3_set_defaults(Eax3Props& props) noexcept
+{
+    props.lDirect = EAXSOURCE_DEFAULTDIRECT;
+    props.lDirectHF = EAXSOURCE_DEFAULTDIRECTHF;
+    props.lRoom = EAXSOURCE_DEFAULTROOM;
+    props.lRoomHF = EAXSOURCE_DEFAULTROOMHF;
+    props.lObstruction = EAXSOURCE_DEFAULTOBSTRUCTION;
+    props.flObstructionLFRatio = EAXSOURCE_DEFAULTOBSTRUCTIONLFRATIO;
+    props.lOcclusion = EAXSOURCE_DEFAULTOCCLUSION;
+    props.flOcclusionLFRatio = EAXSOURCE_DEFAULTOCCLUSIONLFRATIO;
+    props.flOcclusionRoomRatio = EAXSOURCE_DEFAULTOCCLUSIONROOMRATIO;
+    props.flOcclusionDirectRatio = EAXSOURCE_DEFAULTOCCLUSIONDIRECTRATIO;
+    props.lExclusion = EAXSOURCE_DEFAULTEXCLUSION;
+    props.flExclusionLFRatio = EAXSOURCE_DEFAULTEXCLUSIONLFRATIO;
+    props.lOutsideVolumeHF = EAXSOURCE_DEFAULTOUTSIDEVOLUMEHF;
+    props.flDopplerFactor = EAXSOURCE_DEFAULTDOPPLERFACTOR;
+    props.flRolloffFactor = EAXSOURCE_DEFAULTROLLOFFFACTOR;
+    props.flRoomRolloffFactor = EAXSOURCE_DEFAULTROOMROLLOFFFACTOR;
+    props.flAirAbsorptionFactor = EAXSOURCE_DEFAULTAIRABSORPTIONFACTOR;
+    props.ulFlags = EAXSOURCE_DEFAULTFLAGS;
+}
+
+void ALsource::eax3_set_defaults() noexcept
+{
+    eax3_set_defaults(mEax3.i);
+    mEax3.d = mEax3.i;
+}
+
+void ALsource::eax4_set_sends_defaults(EaxSends& sends) noexcept
+{
+    eax_set_sends_defaults(sends, eax4_fx_slot_ids);
+}
+
+void ALsource::eax4_set_active_fx_slots_defaults(EAX40ACTIVEFXSLOTS& slots) noexcept
+{
+    slots = EAX40SOURCE_DEFAULTACTIVEFXSLOTID;
+}
+
+void ALsource::eax4_set_defaults() noexcept
+{
+    eax3_set_defaults(mEax4.i.source);
+    eax4_set_sends_defaults(mEax4.i.sends);
+    eax4_set_active_fx_slots_defaults(mEax4.i.active_fx_slots);
+    mEax4.d = mEax4.i;
+}
+
+void ALsource::eax5_set_source_defaults(EAX50SOURCEPROPERTIES& props) noexcept
+{
+    eax3_set_defaults(static_cast<Eax3Props&>(props));
+    props.flMacroFXFactor = EAXSOURCE_DEFAULTMACROFXFACTOR;
+}
+
+void ALsource::eax5_set_sends_defaults(EaxSends& sends) noexcept
+{
+    eax_set_sends_defaults(sends, eax5_fx_slot_ids);
+}
+
+void ALsource::eax5_set_active_fx_slots_defaults(EAX50ACTIVEFXSLOTS& slots) noexcept
+{
+    slots = EAX50SOURCE_3DDEFAULTACTIVEFXSLOTID;
+}
+
+void ALsource::eax5_set_speaker_levels_defaults(EaxSpeakerLevels& speaker_levels) noexcept
+{
+    for (auto i = size_t{}; i < eax_max_speakers; ++i) {
+        auto& speaker_level = speaker_levels[i];
+        speaker_level.lSpeakerID = static_cast<long>(EAXSPEAKER_FRONT_LEFT + i);
+        speaker_level.lLevel = EAXSOURCE_DEFAULTSPEAKERLEVEL;
+    }
+}
+
+void ALsource::eax5_set_defaults(Eax5Props& props) noexcept
+{
+    eax5_set_source_defaults(props.source);
+    eax5_set_sends_defaults(props.sends);
+    eax5_set_active_fx_slots_defaults(props.active_fx_slots);
+    eax5_set_speaker_levels_defaults(props.speaker_levels);
+}
+
+void ALsource::eax5_set_defaults() noexcept
+{
+    eax5_set_defaults(mEax5.i);
+    mEax5.d = mEax5.i;
+}
+
+void ALsource::eax_set_defaults() noexcept
+{
+    eax1_set_defaults();
+    eax2_set_defaults();
+    eax3_set_defaults();
+    eax4_set_defaults();
+    eax5_set_defaults();
+}
+
+void ALsource::eax1_translate(const Eax1Props& src, Eax5Props& dst) noexcept
+{
+    eax5_set_defaults(dst);
+
+    if (src.fMix == EAX_REVERBMIX_USEDISTANCE)
+    {
+        dst.source.ulFlags |= EAXSOURCEFLAGS_ROOMAUTO;
+        dst.sends[0].lSend = 0;
+    }
+    else
+    {
+        dst.source.ulFlags &= ~EAXSOURCEFLAGS_ROOMAUTO;
+        dst.sends[0].lSend = clamp(static_cast<long>(gain_to_level_mb(src.fMix)),
+            EAXSOURCE_MINSEND, EAXSOURCE_MAXSEND);
+    }
+}
+
+void ALsource::eax2_translate(const Eax2Props& src, Eax5Props& dst) noexcept
+{
+    // Source.
+    //
+    dst.source.lDirect = src.lDirect;
+    dst.source.lDirectHF = src.lDirectHF;
+    dst.source.lRoom = src.lRoom;
+    dst.source.lRoomHF = src.lRoomHF;
+    dst.source.lObstruction = src.lObstruction;
+    dst.source.flObstructionLFRatio = src.flObstructionLFRatio;
+    dst.source.lOcclusion = src.lOcclusion;
+    dst.source.flOcclusionLFRatio = src.flOcclusionLFRatio;
+    dst.source.flOcclusionRoomRatio = src.flOcclusionRoomRatio;
+    dst.source.flOcclusionDirectRatio = EAXSOURCE_DEFAULTOCCLUSIONDIRECTRATIO;
+    dst.source.lExclusion = EAXSOURCE_DEFAULTEXCLUSION;
+    dst.source.flExclusionLFRatio = EAXSOURCE_DEFAULTEXCLUSIONLFRATIO;
+    dst.source.lOutsideVolumeHF = src.lOutsideVolumeHF;
+    dst.source.flDopplerFactor = EAXSOURCE_DEFAULTDOPPLERFACTOR;
+    dst.source.flRolloffFactor = EAXSOURCE_DEFAULTROLLOFFFACTOR;
+    dst.source.flRoomRolloffFactor = src.flRoomRolloffFactor;
+    dst.source.flAirAbsorptionFactor = src.flAirAbsorptionFactor;
+    dst.source.ulFlags = src.dwFlags;
+    dst.source.flMacroFXFactor = EAXSOURCE_DEFAULTMACROFXFACTOR;
+
+    // Set everyting else to defaults.
+    //
+    eax5_set_sends_defaults(dst.sends);
+    eax5_set_active_fx_slots_defaults(dst.active_fx_slots);
+    eax5_set_speaker_levels_defaults(dst.speaker_levels);
+}
+
+void ALsource::eax3_translate(const Eax3Props& src, Eax5Props& dst) noexcept
+{
+    // Source.
+    //
+    static_cast<Eax3Props&>(dst.source) = src;
+    dst.source.flMacroFXFactor = EAXSOURCE_DEFAULTMACROFXFACTOR;
+
+    // Set everyting else to defaults.
+    //
+    eax5_set_sends_defaults(dst.sends);
+    eax5_set_active_fx_slots_defaults(dst.active_fx_slots);
+    eax5_set_speaker_levels_defaults(dst.speaker_levels);
+}
+
+void ALsource::eax4_translate(const Eax4Props& src, Eax5Props& dst) noexcept
+{
+    // Source.
+    //
+    static_cast<Eax3Props&>(dst.source) = src.source;
+    dst.source.flMacroFXFactor = EAXSOURCE_DEFAULTMACROFXFACTOR;
+
+    // Sends.
+    //
+    dst.sends = src.sends;
+
+    for (auto i = size_t{}; i < EAX_MAX_FXSLOTS; ++i)
+        dst.sends[i].guidReceivingFXSlotID = *(eax5_fx_slot_ids[i]);
+
+    // Active FX slots.
+    //
+    for (auto i = 0; i < EAX50_MAX_ACTIVE_FXSLOTS; ++i) {
+        auto& dst_id = dst.active_fx_slots.guidActiveFXSlots[i];
+
+        if (i < EAX40_MAX_ACTIVE_FXSLOTS) {
+            const auto& src_id = src.active_fx_slots.guidActiveFXSlots[i];
+
+            if (src_id == EAX_NULL_GUID)
+                dst_id = EAX_NULL_GUID;
+            else if (src_id == EAX_PrimaryFXSlotID)
+                dst_id = EAX_PrimaryFXSlotID;
+            else if (src_id == EAXPROPERTYID_EAX40_FXSlot0)
+                dst_id = EAXPROPERTYID_EAX50_FXSlot0;
+            else if (src_id == EAXPROPERTYID_EAX40_FXSlot1)
+                dst_id = EAXPROPERTYID_EAX50_FXSlot1;
+            else if (src_id == EAXPROPERTYID_EAX40_FXSlot2)
+                dst_id = EAXPROPERTYID_EAX50_FXSlot2;
+            else if (src_id == EAXPROPERTYID_EAX40_FXSlot3)
+                dst_id = EAXPROPERTYID_EAX50_FXSlot3;
+            else
+                assert(false && "Unknown active FX slot ID.");
+        } else
+            dst_id = EAX_NULL_GUID;
+    }
+
+    // Speaker levels.
+    //
+    eax5_set_speaker_levels_defaults(dst.speaker_levels);
+}
+
+float ALsource::eax_calculate_dst_occlusion_mb(
+    long src_occlusion_mb,
+    float path_ratio,
+    float lf_ratio) noexcept
+{
+    const auto ratio_1 = path_ratio + lf_ratio - 1.0F;
+    const auto ratio_2 = path_ratio * lf_ratio;
+    const auto ratio = (ratio_2 > ratio_1) ? ratio_2 : ratio_1;
+    const auto dst_occlustion_mb = static_cast<float>(src_occlusion_mb) * ratio;
+    return dst_occlustion_mb;
+}
+
+EaxAlLowPassParam ALsource::eax_create_direct_filter_param() const noexcept
+{
+    auto gain_mb =
+        static_cast<float>(mEax.source.lDirect) +
+        (static_cast<float>(mEax.source.lObstruction) * mEax.source.flObstructionLFRatio) +
+        eax_calculate_dst_occlusion_mb(
+            mEax.source.lOcclusion,
+            mEax.source.flOcclusionDirectRatio,
+            mEax.source.flOcclusionLFRatio);
+
+    const auto has_source_occlusion = (mEax.source.lOcclusion != 0);
+
+    auto gain_hf_mb =
+        static_cast<float>(mEax.source.lDirectHF) +
+        static_cast<float>(mEax.source.lObstruction);
+
+    for (auto i = std::size_t{}; i < EAX_MAX_FXSLOTS; ++i)
+    {
+        if(!mEaxActiveFxSlots[i])
+            continue;
+
+        if(has_source_occlusion) {
+            const auto& fx_slot = mEaxAlContext->eaxGetFxSlot(i);
+            const auto& fx_slot_eax = fx_slot.eax_get_eax_fx_slot();
+            const auto is_environmental_fx = ((fx_slot_eax.ulFlags & EAXFXSLOTFLAGS_ENVIRONMENT) != 0);
+            const auto is_primary = (mEaxPrimaryFxSlotId.value_or(-1) == fx_slot.eax_get_index());
+            const auto is_listener_environment = (is_environmental_fx && is_primary);
+
+            if(is_listener_environment) {
+                gain_mb += eax_calculate_dst_occlusion_mb(
+                    mEax.source.lOcclusion,
+                    mEax.source.flOcclusionDirectRatio,
+                    mEax.source.flOcclusionLFRatio);
+
+                gain_hf_mb += static_cast<float>(mEax.source.lOcclusion) * mEax.source.flOcclusionDirectRatio;
+            }
+        }
+
+        const auto& send = mEax.sends[i];
+
+        if(send.lOcclusion != 0) {
+            gain_mb += eax_calculate_dst_occlusion_mb(
+                send.lOcclusion,
+                send.flOcclusionDirectRatio,
+                send.flOcclusionLFRatio);
+
+            gain_hf_mb += static_cast<float>(send.lOcclusion) * send.flOcclusionDirectRatio;
+        }
+    }
+
+    const auto al_low_pass_param = EaxAlLowPassParam{
+        level_mb_to_gain(gain_mb),
+        minf(level_mb_to_gain(gain_hf_mb), 1.0f)};
+
+    return al_low_pass_param;
+}
+
+EaxAlLowPassParam ALsource::eax_create_room_filter_param(
+    const ALeffectslot& fx_slot,
+    const EAXSOURCEALLSENDPROPERTIES& send) const noexcept
+{
+    const auto& fx_slot_eax = fx_slot.eax_get_eax_fx_slot();
+    const auto is_environmental_fx = ((fx_slot_eax.ulFlags & EAXFXSLOTFLAGS_ENVIRONMENT) != 0);
+    const auto is_primary = (mEaxPrimaryFxSlotId.value_or(-1) == fx_slot.eax_get_index());
+    const auto is_listener_environment = (is_environmental_fx && is_primary);
+
+    const auto gain_mb =
+        (static_cast<float>(fx_slot_eax.lOcclusion) * fx_slot_eax.flOcclusionLFRatio) +
+        static_cast<float>((is_environmental_fx ? mEax.source.lRoom : 0) + send.lSend) +
+        (is_listener_environment ?
+            eax_calculate_dst_occlusion_mb(
+                mEax.source.lOcclusion,
+                mEax.source.flOcclusionRoomRatio,
+                mEax.source.flOcclusionLFRatio) :
+            0.0f) +
+        eax_calculate_dst_occlusion_mb(
+            send.lOcclusion,
+            send.flOcclusionRoomRatio,
+            send.flOcclusionLFRatio) +
+        (is_listener_environment ?
+            (static_cast<float>(mEax.source.lExclusion) * mEax.source.flExclusionLFRatio) :
+            0.0f) +
+        (static_cast<float>(send.lExclusion) * send.flExclusionLFRatio);
+
+    const auto gain_hf_mb =
+        static_cast<float>(fx_slot_eax.lOcclusion) +
+        static_cast<float>((is_environmental_fx ? mEax.source.lRoomHF : 0) + send.lSendHF) +
+        (is_listener_environment ?
+            ((static_cast<float>(mEax.source.lOcclusion) * mEax.source.flOcclusionRoomRatio)) :
+            0.0f) +
+        (static_cast<float>(send.lOcclusion) * send.flOcclusionRoomRatio) +
+        (is_listener_environment ?
+            static_cast<float>(mEax.source.lExclusion + send.lExclusion) :
+            0.0f);
+
+    const auto al_low_pass_param = EaxAlLowPassParam{
+        level_mb_to_gain(gain_mb),
+        minf(level_mb_to_gain(gain_hf_mb), 1.0f)};
+
+    return al_low_pass_param;
+}
+
+void ALsource::eax_update_direct_filter()
+{
+    const auto& direct_param = eax_create_direct_filter_param();
+    Direct.Gain = direct_param.gain;
+    Direct.GainHF = direct_param.gain_hf;
+    Direct.HFReference = LOWPASSFREQREF;
+    Direct.GainLF = 1.0f;
+    Direct.LFReference = HIGHPASSFREQREF;
+    mPropsDirty = true;
+}
+
+void ALsource::eax_update_room_filters()
+{
+    for (auto i = size_t{}; i < EAX_MAX_FXSLOTS; ++i) {
+        if (!mEaxActiveFxSlots[i])
+            continue;
+
+        auto& fx_slot = mEaxAlContext->eaxGetFxSlot(i);
+        const auto& send = mEax.sends[i];
+        const auto& room_param = eax_create_room_filter_param(fx_slot, send);
+        eax_set_al_source_send(&fx_slot, i, room_param);
+    }
+}
+
+void ALsource::eax_set_efx_outer_gain_hf()
+{
+    OuterGainHF = clamp(
+        level_mb_to_gain(static_cast<float>(mEax.source.lOutsideVolumeHF)),
+        AL_MIN_CONE_OUTER_GAINHF,
+        AL_MAX_CONE_OUTER_GAINHF);
+}
+
+void ALsource::eax_set_efx_doppler_factor()
+{
+    DopplerFactor = mEax.source.flDopplerFactor;
+}
+
+void ALsource::eax_set_efx_rolloff_factor()
+{
+    RolloffFactor2 = mEax.source.flRolloffFactor;
+}
+
+void ALsource::eax_set_efx_room_rolloff_factor()
+{
+    RoomRolloffFactor = mEax.source.flRoomRolloffFactor;
+}
+
+void ALsource::eax_set_efx_air_absorption_factor()
+{
+    AirAbsorptionFactor = mEax.source.flAirAbsorptionFactor;
+}
+
+void ALsource::eax_set_efx_dry_gain_hf_auto()
+{
+    DryGainHFAuto = ((mEax.source.ulFlags & EAXSOURCEFLAGS_DIRECTHFAUTO) != 0);
+}
+
+void ALsource::eax_set_efx_wet_gain_auto()
+{
+    WetGainAuto = ((mEax.source.ulFlags & EAXSOURCEFLAGS_ROOMAUTO) != 0);
+}
+
+void ALsource::eax_set_efx_wet_gain_hf_auto()
+{
+    WetGainHFAuto = ((mEax.source.ulFlags & EAXSOURCEFLAGS_ROOMHFAUTO) != 0);
+}
+
+void ALsource::eax1_set(const EaxCall& call, Eax1Props& props)
+{
+    switch (call.get_property_id()) {
+        case DSPROPERTY_EAXBUFFER_ALL:
+            eax_defer<Eax1SourceAllValidator>(call, props);
+            break;
+
+        case DSPROPERTY_EAXBUFFER_REVERBMIX:
+            eax_defer<Eax1SourceReverbMixValidator>(call, props.fMix);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax2_set(const EaxCall& call, Eax2Props& props)
+{
+    switch (call.get_property_id()) {
+        case DSPROPERTY_EAX20BUFFER_NONE:
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ALLPARAMETERS:
+            eax_defer<Eax2SourceAllValidator>(call, props);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_DIRECT:
+            eax_defer<Eax2SourceDirectValidator>(call, props.lDirect);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_DIRECTHF:
+            eax_defer<Eax2SourceDirectHfValidator>(call, props.lDirectHF);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ROOM:
+            eax_defer<Eax2SourceRoomValidator>(call, props.lRoom);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ROOMHF:
+            eax_defer<Eax2SourceRoomHfValidator>(call, props.lRoomHF);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ROOMROLLOFFFACTOR:
+            eax_defer<Eax2SourceRoomRolloffFactorValidator>(call, props.flRoomRolloffFactor);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OBSTRUCTION:
+            eax_defer<Eax2SourceObstructionValidator>(call, props.lObstruction);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OBSTRUCTIONLFRATIO:
+            eax_defer<Eax2SourceObstructionLfRatioValidator>(call, props.flObstructionLFRatio);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSION:
+            eax_defer<Eax2SourceOcclusionValidator>(call, props.lOcclusion);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSIONLFRATIO:
+            eax_defer<Eax2SourceOcclusionLfRatioValidator>(call, props.flOcclusionLFRatio);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSIONROOMRATIO:
+            eax_defer<Eax2SourceOcclusionRoomRatioValidator>(call, props.flOcclusionRoomRatio);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OUTSIDEVOLUMEHF:
+            eax_defer<Eax2SourceOutsideVolumeHfValidator>(call, props.lOutsideVolumeHF);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_AIRABSORPTIONFACTOR:
+            eax_defer<Eax2SourceAirAbsorptionFactorValidator>(call, props.flAirAbsorptionFactor);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_FLAGS:
+            eax_defer<Eax2SourceFlagsValidator>(call, props.dwFlags);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax3_set(const EaxCall& call, Eax3Props& props)
+{
+    switch (call.get_property_id()) {
+        case EAXSOURCE_NONE:
+            break;
+
+        case EAXSOURCE_ALLPARAMETERS:
+            eax_defer<Eax3SourceAllValidator>(call, props);
+            break;
+
+        case EAXSOURCE_OBSTRUCTIONPARAMETERS:
+            eax_defer_sub<Eax4ObstructionValidator, EAXOBSTRUCTIONPROPERTIES>(call, props.lObstruction);
+            break;
+
+        case EAXSOURCE_OCCLUSIONPARAMETERS:
+            eax_defer_sub<Eax4OcclusionValidator, EAXOCCLUSIONPROPERTIES>(call, props.lOcclusion);
+            break;
+
+        case EAXSOURCE_EXCLUSIONPARAMETERS:
+            eax_defer_sub<Eax4ExclusionValidator, EAXEXCLUSIONPROPERTIES>(call, props.lExclusion);
+            break;
+
+        case EAXSOURCE_DIRECT:
+            eax_defer<Eax2SourceDirectValidator>(call, props.lDirect);
+            break;
+
+        case EAXSOURCE_DIRECTHF:
+            eax_defer<Eax2SourceDirectHfValidator>(call, props.lDirectHF);
+            break;
+
+        case EAXSOURCE_ROOM:
+            eax_defer<Eax2SourceRoomValidator>(call, props.lRoom);
+            break;
+
+        case EAXSOURCE_ROOMHF:
+            eax_defer<Eax2SourceRoomHfValidator>(call, props.lRoomHF);
+            break;
+
+        case EAXSOURCE_OBSTRUCTION:
+            eax_defer<Eax2SourceObstructionValidator>(call, props.lObstruction);
+            break;
+
+        case EAXSOURCE_OBSTRUCTIONLFRATIO:
+            eax_defer<Eax2SourceObstructionLfRatioValidator>(call, props.flObstructionLFRatio);
+            break;
+
+        case EAXSOURCE_OCCLUSION:
+            eax_defer<Eax2SourceOcclusionValidator>(call, props.lOcclusion);
+            break;
+
+        case EAXSOURCE_OCCLUSIONLFRATIO:
+            eax_defer<Eax2SourceOcclusionLfRatioValidator>(call, props.flOcclusionLFRatio);
+            break;
+
+        case EAXSOURCE_OCCLUSIONROOMRATIO:
+            eax_defer<Eax2SourceOcclusionRoomRatioValidator>(call, props.flOcclusionRoomRatio);
+            break;
+
+        case EAXSOURCE_OCCLUSIONDIRECTRATIO:
+            eax_defer<Eax3SourceOcclusionDirectRatioValidator>(call, props.flOcclusionDirectRatio);
+            break;
+
+        case EAXSOURCE_EXCLUSION:
+            eax_defer<Eax3SourceExclusionValidator>(call, props.lExclusion);
+            break;
+
+        case EAXSOURCE_EXCLUSIONLFRATIO:
+            eax_defer<Eax3SourceExclusionLfRatioValidator>(call, props.flExclusionLFRatio);
+            break;
+
+        case EAXSOURCE_OUTSIDEVOLUMEHF:
+            eax_defer<Eax2SourceOutsideVolumeHfValidator>(call, props.lOutsideVolumeHF);
+            break;
+
+        case EAXSOURCE_DOPPLERFACTOR:
+            eax_defer<Eax3SourceDopplerFactorValidator>(call, props.flDopplerFactor);
+            break;
+
+        case EAXSOURCE_ROLLOFFFACTOR:
+            eax_defer<Eax3SourceRolloffFactorValidator>(call, props.flRolloffFactor);
+            break;
+
+        case EAXSOURCE_ROOMROLLOFFFACTOR:
+            eax_defer<Eax2SourceRoomRolloffFactorValidator>(call, props.flRoomRolloffFactor);
+            break;
+
+        case EAXSOURCE_AIRABSORPTIONFACTOR:
+            eax_defer<Eax2SourceAirAbsorptionFactorValidator>(call, props.flAirAbsorptionFactor);
+            break;
+
+        case EAXSOURCE_FLAGS:
+            eax_defer<Eax2SourceFlagsValidator>(call, props.ulFlags);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax4_set(const EaxCall& call, Eax4Props& props)
+{
+    switch (call.get_property_id()) {
+        case EAXSOURCE_NONE:
+        case EAXSOURCE_ALLPARAMETERS:
+        case EAXSOURCE_OBSTRUCTIONPARAMETERS:
+        case EAXSOURCE_OCCLUSIONPARAMETERS:
+        case EAXSOURCE_EXCLUSIONPARAMETERS:
+        case EAXSOURCE_DIRECT:
+        case EAXSOURCE_DIRECTHF:
+        case EAXSOURCE_ROOM:
+        case EAXSOURCE_ROOMHF:
+        case EAXSOURCE_OBSTRUCTION:
+        case EAXSOURCE_OBSTRUCTIONLFRATIO:
+        case EAXSOURCE_OCCLUSION:
+        case EAXSOURCE_OCCLUSIONLFRATIO:
+        case EAXSOURCE_OCCLUSIONROOMRATIO:
+        case EAXSOURCE_OCCLUSIONDIRECTRATIO:
+        case EAXSOURCE_EXCLUSION:
+        case EAXSOURCE_EXCLUSIONLFRATIO:
+        case EAXSOURCE_OUTSIDEVOLUMEHF:
+        case EAXSOURCE_DOPPLERFACTOR:
+        case EAXSOURCE_ROLLOFFFACTOR:
+        case EAXSOURCE_ROOMROLLOFFFACTOR:
+        case EAXSOURCE_AIRABSORPTIONFACTOR:
+        case EAXSOURCE_FLAGS:
+            eax3_set(call, props.source);
+            break;
+
+        case EAXSOURCE_SENDPARAMETERS:
+            eax4_defer_sends<Eax4SendValidator, EAXSOURCESENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ALLSENDPARAMETERS:
+            eax4_defer_sends<Eax4AllSendValidator, EAXSOURCEALLSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_OCCLUSIONSENDPARAMETERS:
+            eax4_defer_sends<Eax4OcclusionSendValidator, EAXSOURCEOCCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_EXCLUSIONSENDPARAMETERS:
+            eax4_defer_sends<Eax4ExclusionSendValidator, EAXSOURCEEXCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ACTIVEFXSLOTID:
+            eax4_defer_active_fx_slot_id(call, props.active_fx_slots.guidActiveFXSlots);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax5_defer_all_2d(const EaxCall& call, EAX50SOURCEPROPERTIES& props)
+{
+    const auto& src_props = call.get_value<Exception, const EAXSOURCE2DPROPERTIES>();
+    Eax5SourceAll2dValidator{}(src_props);
+    props.lDirect = src_props.lDirect;
+    props.lDirectHF = src_props.lDirectHF;
+    props.lRoom = src_props.lRoom;
+    props.lRoomHF = src_props.lRoomHF;
+    props.ulFlags = src_props.ulFlags;
+}
+
+void ALsource::eax5_defer_speaker_levels(const EaxCall& call, EaxSpeakerLevels& props)
+{
+    const auto values = call.get_values<const EAXSPEAKERLEVELPROPERTIES>(eax_max_speakers);
+    std::for_each(values.cbegin(), values.cend(), Eax5SpeakerAllValidator{});
+
+    for (const auto& value : values) {
+        const auto index = static_cast<size_t>(value.lSpeakerID - EAXSPEAKER_FRONT_LEFT);
+        props[index].lLevel = value.lLevel;
+    }
+}
+
+void ALsource::eax5_set(const EaxCall& call, Eax5Props& props)
+{
+    switch (call.get_property_id()) {
+        case EAXSOURCE_NONE:
+            break;
+
+        case EAXSOURCE_ALLPARAMETERS:
+            eax_defer<Eax5SourceAllValidator>(call, props.source);
+            break;
+
+        case EAXSOURCE_OBSTRUCTIONPARAMETERS:
+        case EAXSOURCE_OCCLUSIONPARAMETERS:
+        case EAXSOURCE_EXCLUSIONPARAMETERS:
+        case EAXSOURCE_DIRECT:
+        case EAXSOURCE_DIRECTHF:
+        case EAXSOURCE_ROOM:
+        case EAXSOURCE_ROOMHF:
+        case EAXSOURCE_OBSTRUCTION:
+        case EAXSOURCE_OBSTRUCTIONLFRATIO:
+        case EAXSOURCE_OCCLUSION:
+        case EAXSOURCE_OCCLUSIONLFRATIO:
+        case EAXSOURCE_OCCLUSIONROOMRATIO:
+        case EAXSOURCE_OCCLUSIONDIRECTRATIO:
+        case EAXSOURCE_EXCLUSION:
+        case EAXSOURCE_EXCLUSIONLFRATIO:
+        case EAXSOURCE_OUTSIDEVOLUMEHF:
+        case EAXSOURCE_DOPPLERFACTOR:
+        case EAXSOURCE_ROLLOFFFACTOR:
+        case EAXSOURCE_ROOMROLLOFFFACTOR:
+        case EAXSOURCE_AIRABSORPTIONFACTOR:
+        case EAXSOURCE_FLAGS:
+            eax3_set(call, props.source);
+            break;
+
+        case EAXSOURCE_SENDPARAMETERS:
+            eax5_defer_sends<Eax5SendValidator, EAXSOURCESENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ALLSENDPARAMETERS:
+            eax5_defer_sends<Eax5AllSendValidator, EAXSOURCEALLSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_OCCLUSIONSENDPARAMETERS:
+            eax5_defer_sends<Eax5OcclusionSendValidator, EAXSOURCEOCCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_EXCLUSIONSENDPARAMETERS:
+            eax5_defer_sends<Eax5ExclusionSendValidator, EAXSOURCEEXCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ACTIVEFXSLOTID:
+            eax5_defer_active_fx_slot_id(call, props.active_fx_slots.guidActiveFXSlots);
+            break;
+
+        case EAXSOURCE_MACROFXFACTOR:
+            eax_defer<Eax5SourceMacroFXFactorValidator>(call, props.source.flMacroFXFactor);
+            break;
+
+        case EAXSOURCE_SPEAKERLEVELS:
+            eax5_defer_speaker_levels(call, props.speaker_levels);
+            break;
+
+        case EAXSOURCE_ALL2DPARAMETERS:
+            eax5_defer_all_2d(call, props.source);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax_set(const EaxCall& call)
+{
+    const auto eax_version = call.get_version();
+    switch(eax_version)
+    {
+    case 1: eax1_set(call, mEax1.d); break;
+    case 2: eax2_set(call, mEax2.d); break;
+    case 3: eax3_set(call, mEax3.d); break;
+    case 4: eax4_set(call, mEax4.d); break;
+    case 5: eax5_set(call, mEax5.d); break;
+    default: eax_fail_unknown_property_id();
+    }
+    mEaxChanged = true;
+    mEaxVersion = eax_version;
+}
+
+void ALsource::eax_get_active_fx_slot_id(const EaxCall& call, const GUID* ids, size_t max_count)
+{
+    assert(ids != nullptr);
+    assert(max_count == EAX40_MAX_ACTIVE_FXSLOTS || max_count == EAX50_MAX_ACTIVE_FXSLOTS);
+    const auto dst_ids = call.get_values<GUID>(max_count);
+    const auto count = dst_ids.size();
+    std::uninitialized_copy_n(ids, count, dst_ids.begin());
+}
+
+void ALsource::eax1_get(const EaxCall& call, const Eax1Props& props)
+{
+    switch (call.get_property_id()) {
+        case DSPROPERTY_EAXBUFFER_ALL:
+        case DSPROPERTY_EAXBUFFER_REVERBMIX:
+            call.set_value<Exception>(props.fMix);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax2_get(const EaxCall& call, const Eax2Props& props)
+{
+    switch (call.get_property_id()) {
+        case DSPROPERTY_EAX20BUFFER_NONE:
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ALLPARAMETERS:
+            call.set_value<Exception>(props);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_DIRECT:
+            call.set_value<Exception>(props.lDirect);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_DIRECTHF:
+            call.set_value<Exception>(props.lDirectHF);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ROOM:
+            call.set_value<Exception>(props.lRoom);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ROOMHF:
+            call.set_value<Exception>(props.lRoomHF);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_ROOMROLLOFFFACTOR:
+            call.set_value<Exception>(props.flRoomRolloffFactor);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OBSTRUCTION:
+            call.set_value<Exception>(props.lObstruction);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OBSTRUCTIONLFRATIO:
+            call.set_value<Exception>(props.flObstructionLFRatio);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSION:
+            call.set_value<Exception>(props.lOcclusion);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSIONLFRATIO:
+            call.set_value<Exception>(props.flOcclusionLFRatio);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSIONROOMRATIO:
+            call.set_value<Exception>(props.flOcclusionRoomRatio);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_OUTSIDEVOLUMEHF:
+            call.set_value<Exception>(props.lOutsideVolumeHF);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_AIRABSORPTIONFACTOR:
+            call.set_value<Exception>(props.flAirAbsorptionFactor);
+            break;
+
+        case DSPROPERTY_EAX20BUFFER_FLAGS:
+            call.set_value<Exception>(props.dwFlags);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax3_get_obstruction(const EaxCall& call, const Eax3Props& props)
+{
+    const auto& subprops = reinterpret_cast<const EAXOBSTRUCTIONPROPERTIES&>(props.lObstruction);
+    call.set_value<Exception>(subprops);
+}
+
+void ALsource::eax3_get_occlusion(const EaxCall& call, const Eax3Props& props)
+{
+    const auto& subprops = reinterpret_cast<const EAXOCCLUSIONPROPERTIES&>(props.lOcclusion);
+    call.set_value<Exception>(subprops);
+}
+
+void ALsource::eax3_get_exclusion(const EaxCall& call, const Eax3Props& props)
+{
+    const auto& subprops = reinterpret_cast<const EAXEXCLUSIONPROPERTIES&>(props.lExclusion);
+    call.set_value<Exception>(subprops);
+}
+
+void ALsource::eax3_get(const EaxCall& call, const Eax3Props& props)
+{
+    switch (call.get_property_id()) {
+        case EAXSOURCE_NONE:
+            break;
+
+        case EAXSOURCE_ALLPARAMETERS:
+            call.set_value<Exception>(props);
+            break;
+
+        case EAXSOURCE_OBSTRUCTIONPARAMETERS:
+            eax3_get_obstruction(call, props);
+            break;
+
+        case EAXSOURCE_OCCLUSIONPARAMETERS:
+            eax3_get_occlusion(call, props);
+            break;
+
+        case EAXSOURCE_EXCLUSIONPARAMETERS:
+            eax3_get_exclusion(call, props);
+            break;
+
+        case EAXSOURCE_DIRECT:
+            call.set_value<Exception>(props.lDirect);
+            break;
+
+        case EAXSOURCE_DIRECTHF:
+            call.set_value<Exception>(props.lDirectHF);
+            break;
+
+        case EAXSOURCE_ROOM:
+            call.set_value<Exception>(props.lRoom);
+            break;
+
+        case EAXSOURCE_ROOMHF:
+            call.set_value<Exception>(props.lRoomHF);
+            break;
+
+        case EAXSOURCE_OBSTRUCTION:
+            call.set_value<Exception>(props.lObstruction);
+            break;
+
+        case EAXSOURCE_OBSTRUCTIONLFRATIO:
+            call.set_value<Exception>(props.flObstructionLFRatio);
+            break;
+
+        case EAXSOURCE_OCCLUSION:
+            call.set_value<Exception>(props.lOcclusion);
+            break;
+
+        case EAXSOURCE_OCCLUSIONLFRATIO:
+            call.set_value<Exception>(props.flOcclusionLFRatio);
+            break;
+
+        case EAXSOURCE_OCCLUSIONROOMRATIO:
+            call.set_value<Exception>(props.flOcclusionRoomRatio);
+            break;
+
+        case EAXSOURCE_OCCLUSIONDIRECTRATIO:
+            call.set_value<Exception>(props.flOcclusionDirectRatio);
+            break;
+
+        case EAXSOURCE_EXCLUSION:
+            call.set_value<Exception>(props.lExclusion);
+            break;
+
+        case EAXSOURCE_EXCLUSIONLFRATIO:
+            call.set_value<Exception>(props.flExclusionLFRatio);
+            break;
+
+        case EAXSOURCE_OUTSIDEVOLUMEHF:
+            call.set_value<Exception>(props.lOutsideVolumeHF);
+            break;
+
+        case EAXSOURCE_DOPPLERFACTOR:
+            call.set_value<Exception>(props.flDopplerFactor);
+            break;
+
+        case EAXSOURCE_ROLLOFFFACTOR:
+            call.set_value<Exception>(props.flRolloffFactor);
+            break;
+
+        case EAXSOURCE_ROOMROLLOFFFACTOR:
+            call.set_value<Exception>(props.flRoomRolloffFactor);
+            break;
+
+        case EAXSOURCE_AIRABSORPTIONFACTOR:
+            call.set_value<Exception>(props.flAirAbsorptionFactor);
+            break;
+
+        case EAXSOURCE_FLAGS:
+            call.set_value<Exception>(props.ulFlags);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax4_get(const EaxCall& call, const Eax4Props& props)
+{
+    switch (call.get_property_id()) {
+        case EAXSOURCE_NONE:
+            break;
+
+        case EAXSOURCE_ALLPARAMETERS:
+        case EAXSOURCE_OBSTRUCTIONPARAMETERS:
+        case EAXSOURCE_OCCLUSIONPARAMETERS:
+        case EAXSOURCE_EXCLUSIONPARAMETERS:
+        case EAXSOURCE_DIRECT:
+        case EAXSOURCE_DIRECTHF:
+        case EAXSOURCE_ROOM:
+        case EAXSOURCE_ROOMHF:
+        case EAXSOURCE_OBSTRUCTION:
+        case EAXSOURCE_OBSTRUCTIONLFRATIO:
+        case EAXSOURCE_OCCLUSION:
+        case EAXSOURCE_OCCLUSIONLFRATIO:
+        case EAXSOURCE_OCCLUSIONROOMRATIO:
+        case EAXSOURCE_OCCLUSIONDIRECTRATIO:
+        case EAXSOURCE_EXCLUSION:
+        case EAXSOURCE_EXCLUSIONLFRATIO:
+        case EAXSOURCE_OUTSIDEVOLUMEHF:
+        case EAXSOURCE_DOPPLERFACTOR:
+        case EAXSOURCE_ROLLOFFFACTOR:
+        case EAXSOURCE_ROOMROLLOFFFACTOR:
+        case EAXSOURCE_AIRABSORPTIONFACTOR:
+        case EAXSOURCE_FLAGS:
+            eax3_get(call, props.source);
+            break;
+
+        case EAXSOURCE_SENDPARAMETERS:
+            eax_get_sends<EAXSOURCESENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ALLSENDPARAMETERS:
+            eax_get_sends<EAXSOURCEALLSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_OCCLUSIONSENDPARAMETERS:
+            eax_get_sends<EAXSOURCEOCCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_EXCLUSIONSENDPARAMETERS:
+            eax_get_sends<EAXSOURCEEXCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ACTIVEFXSLOTID:
+            eax_get_active_fx_slot_id(call, props.active_fx_slots.guidActiveFXSlots, EAX40_MAX_ACTIVE_FXSLOTS);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax5_get_all_2d(const EaxCall& call, const EAX50SOURCEPROPERTIES& props)
+{
+    auto& subprops = call.get_value<Exception, EAXSOURCE2DPROPERTIES>();
+    subprops.lDirect = props.lDirect;
+    subprops.lDirectHF = props.lDirectHF;
+    subprops.lRoom = props.lRoom;
+    subprops.lRoomHF = props.lRoomHF;
+    subprops.ulFlags = props.ulFlags;
+}
+
+void ALsource::eax5_get_speaker_levels(const EaxCall& call, const EaxSpeakerLevels& props)
+{
+    const auto subprops = call.get_values<EAXSPEAKERLEVELPROPERTIES>(eax_max_speakers);
+    std::uninitialized_copy_n(props.cbegin(), subprops.size(), subprops.begin());
+}
+
+void ALsource::eax5_get(const EaxCall& call, const Eax5Props& props)
+{
+    switch (call.get_property_id()) {
+        case EAXSOURCE_NONE:
+            break;
+
+        case EAXSOURCE_ALLPARAMETERS:
+        case EAXSOURCE_OBSTRUCTIONPARAMETERS:
+        case EAXSOURCE_OCCLUSIONPARAMETERS:
+        case EAXSOURCE_EXCLUSIONPARAMETERS:
+        case EAXSOURCE_DIRECT:
+        case EAXSOURCE_DIRECTHF:
+        case EAXSOURCE_ROOM:
+        case EAXSOURCE_ROOMHF:
+        case EAXSOURCE_OBSTRUCTION:
+        case EAXSOURCE_OBSTRUCTIONLFRATIO:
+        case EAXSOURCE_OCCLUSION:
+        case EAXSOURCE_OCCLUSIONLFRATIO:
+        case EAXSOURCE_OCCLUSIONROOMRATIO:
+        case EAXSOURCE_OCCLUSIONDIRECTRATIO:
+        case EAXSOURCE_EXCLUSION:
+        case EAXSOURCE_EXCLUSIONLFRATIO:
+        case EAXSOURCE_OUTSIDEVOLUMEHF:
+        case EAXSOURCE_DOPPLERFACTOR:
+        case EAXSOURCE_ROLLOFFFACTOR:
+        case EAXSOURCE_ROOMROLLOFFFACTOR:
+        case EAXSOURCE_AIRABSORPTIONFACTOR:
+        case EAXSOURCE_FLAGS:
+            eax3_get(call, props.source);
+            break;
+
+        case EAXSOURCE_SENDPARAMETERS:
+            eax_get_sends<EAXSOURCESENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ALLSENDPARAMETERS:
+            eax_get_sends<EAXSOURCEALLSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_OCCLUSIONSENDPARAMETERS:
+            eax_get_sends<EAXSOURCEOCCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_EXCLUSIONSENDPARAMETERS:
+            eax_get_sends<EAXSOURCEEXCLUSIONSENDPROPERTIES>(call, props.sends);
+            break;
+
+        case EAXSOURCE_ACTIVEFXSLOTID:
+            eax_get_active_fx_slot_id(call, props.active_fx_slots.guidActiveFXSlots, EAX50_MAX_ACTIVE_FXSLOTS);
+            break;
+
+        case EAXSOURCE_MACROFXFACTOR:
+            call.set_value<Exception>(props.source.flMacroFXFactor);
+            break;
+
+        case EAXSOURCE_SPEAKERLEVELS:
+            call.set_value<Exception>(props.speaker_levels);
+            break;
+
+        case EAXSOURCE_ALL2DPARAMETERS:
+            eax5_get_all_2d(call, props.source);
+            break;
+
+        default:
+            eax_fail_unknown_property_id();
+    }
+}
+
+void ALsource::eax_get(const EaxCall& call)
+{
+    switch (call.get_version()) {
+        case 1: eax1_get(call, mEax1.i); break;
+        case 2: eax2_get(call, mEax2.i); break;
+        case 3: eax3_get(call, mEax3.i); break;
+        case 4: eax4_get(call, mEax4.i); break;
+        case 5: eax5_get(call, mEax5.i); break;
+        default: eax_fail_unknown_version();
+    }
+}
+
+void ALsource::eax_set_al_source_send(ALeffectslot *slot, size_t sendidx, const EaxAlLowPassParam &filter)
+{
+    if(sendidx >= EAX_MAX_FXSLOTS)
+        return;
+
+    auto &send = Send[sendidx];
+    send.Gain = filter.gain;
+    send.GainHF = filter.gain_hf;
+    send.HFReference = LOWPASSFREQREF;
+    send.GainLF = 1.0f;
+    send.LFReference = HIGHPASSFREQREF;
+
+    if(slot != nullptr)
+        IncrementRef(slot->ref);
+    if(auto *oldslot = send.Slot)
+        DecrementRef(oldslot->ref);
+
+    send.Slot = slot;
+    mPropsDirty = true;
+}
+
+void ALsource::eax_commit_active_fx_slots()
+{
+    // Clear all slots to an inactive state.
+    mEaxActiveFxSlots.fill(false);
+
+    // Mark the set slots as active.
+    for(const auto& slot_id : mEax.active_fx_slots.guidActiveFXSlots)
+    {
+        if(slot_id == EAX_NULL_GUID)
+        {
+        }
+        else if(slot_id == EAX_PrimaryFXSlotID)
+        {
+            // Mark primary FX slot as active.
+            if(mEaxPrimaryFxSlotId.has_value())
+                mEaxActiveFxSlots[*mEaxPrimaryFxSlotId] = true;
+        }
+        else if(slot_id == EAXPROPERTYID_EAX50_FXSlot0)
+            mEaxActiveFxSlots[0] = true;
+        else if(slot_id == EAXPROPERTYID_EAX50_FXSlot1)
+            mEaxActiveFxSlots[1] = true;
+        else if(slot_id == EAXPROPERTYID_EAX50_FXSlot2)
+            mEaxActiveFxSlots[2] = true;
+        else if(slot_id == EAXPROPERTYID_EAX50_FXSlot3)
+            mEaxActiveFxSlots[3] = true;
+    }
+
+    // Deactivate EFX auxiliary effect slots for inactive slots. Active slots
+    // will be updated with the room filters.
+    for(auto i = size_t{}; i < EAX_MAX_FXSLOTS; ++i)
+    {
+        if(!mEaxActiveFxSlots[i])
+            eax_set_al_source_send(nullptr, i, EaxAlLowPassParam{1.0f, 1.0f});
+    }
+}
+
+void ALsource::eax_commit_filters()
+{
+    eax_update_direct_filter();
+    eax_update_room_filters();
+}
+
+void ALsource::eaxCommit()
+{
+    const auto primary_fx_slot_id = mEaxAlContext->eaxGetPrimaryFxSlotIndex();
+    const auto is_primary_fx_slot_id_changed = (mEaxPrimaryFxSlotId != primary_fx_slot_id);
+
+    if(!mEaxChanged && !is_primary_fx_slot_id_changed)
+        return;
+
+    mEaxPrimaryFxSlotId = primary_fx_slot_id;
+    mEaxChanged = false;
+
+    switch(mEaxVersion)
+    {
+    case 1:
+        mEax1.i = mEax1.d;
+        eax1_translate(mEax1.i, mEax);
+        break;
+    case 2:
+        mEax2.i = mEax2.d;
+        eax2_translate(mEax2.i, mEax);
+        break;
+    case 3:
+        mEax3.i = mEax3.d;
+        eax3_translate(mEax3.i, mEax);
+        break;
+    case 4:
+        mEax4.i = mEax4.d;
+        eax4_translate(mEax4.i, mEax);
+        break;
+    case 5:
+        mEax5.i = mEax5.d;
+        mEax = mEax5.d;
+        break;
+    }
+
+    eax_set_efx_outer_gain_hf();
+    eax_set_efx_doppler_factor();
+    eax_set_efx_rolloff_factor();
+    eax_set_efx_room_rolloff_factor();
+    eax_set_efx_air_absorption_factor();
+    eax_set_efx_dry_gain_hf_auto();
+    eax_set_efx_wet_gain_auto();
+    eax_set_efx_wet_gain_hf_auto();
+
+    eax_commit_active_fx_slots();
+    eax_commit_filters();
+}
+
+#endif // ALSOFT_EAX
diff --git a/al/source.h b/al/source.h
new file mode 100644 (file)
index 0000000..ac97c8a
--- /dev/null
@@ -0,0 +1,1044 @@
+#ifndef AL_SOURCE_H
+#define AL_SOURCE_H
+
+#include <array>
+#include <atomic>
+#include <cstddef>
+#include <iterator>
+#include <limits>
+#include <deque>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+
+#include "alc/alu.h"
+#include "alc/context.h"
+#include "alc/inprogext.h"
+#include "aldeque.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "atomic.h"
+#include "core/voice.h"
+#include "vector.h"
+
+#ifdef ALSOFT_EAX
+#include "eax/call.h"
+#include "eax/exception.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)
+
+extern bool sBufferSubDataCompat;
+
+struct ALbufferQueueItem : public VoiceBufferItem {
+    ALbuffer *mBuffer{nullptr};
+
+    DISABLE_ALLOC()
+};
+
+
+#ifdef ALSOFT_EAX
+class EaxSourceException : public EaxException {
+public:
+    explicit EaxSourceException(const char* message)
+        : EaxException{"EAX_SOURCE", message}
+    {}
+};
+#endif // ALSOFT_EAX
+
+struct ALsource {
+    /** Source properties. */
+    float Pitch{1.0f};
+    float Gain{1.0f};
+    float OuterGain{0.0f};
+    float MinGain{0.0f};
+    float MaxGain{1.0f};
+    float InnerAngle{360.0f};
+    float OuterAngle{360.0f};
+    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}};
+    std::array<float,3> OrientAt{{0.0f, 0.0f, -1.0f}};
+    std::array<float,3> OrientUp{{0.0f, 1.0f,  0.0f}};
+    bool HeadRelative{false};
+    bool Looping{false};
+    DistanceModel mDistanceModel{DistanceModel::Default};
+    Resampler mResampler{ResamplerDefault};
+    DirectMode DirectChannels{DirectMode::Off};
+    SpatializeMode mSpatialize{SpatializeMode::Auto};
+    SourceStereo mStereoMode{SourceStereo::Normal};
+
+    bool DryGainHFAuto{true};
+    bool WetGainAuto{true};
+    bool WetGainHFAuto{true};
+    float OuterGainHF{1.0f};
+
+    float AirAbsorptionFactor{0.0f};
+    float RoomRolloffFactor{0.0f};
+    float DopplerFactor{1.0f};
+
+    /* NOTE: Stereo pan angles are specified in radians, counter-clockwise
+     * rather than clockwise.
+     */
+    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 {
+        float Gain;
+        float GainHF;
+        float HFReference;
+        float GainLF;
+        float LFReference;
+    } Direct;
+    struct SendData {
+        ALeffectslot *Slot;
+        float Gain;
+        float GainHF;
+        float HFReference;
+        float GainLF;
+        float LFReference;
+    };
+    std::array<SendData,MAX_SENDS> Send;
+
+    /**
+     * Last user-specified offset, and the offset type (bytes, samples, or
+     * seconds).
+     */
+    double Offset{0.0};
+    ALenum OffsetType{AL_NONE};
+
+    /** Source type (static, streaming, or undetermined) */
+    ALenum SourceType{AL_UNDETERMINED};
+
+    /** Source state (initial, playing, paused, or stopped) */
+    ALenum state{AL_INITIAL};
+
+    /** Source Buffer Queue head. */
+    al::deque<ALbufferQueueItem> mQueue;
+
+    bool mPropsDirty{true};
+
+    /* Index into the context's Voices array. Lazily updated, only checked and
+     * reset when looking up the voice.
+     */
+    ALuint VoiceIdx{INVALID_VOICE_IDX};
+
+    /** Self ID */
+    ALuint id{0};
+
+
+    ALsource();
+    ~ALsource();
+
+    ALsource(const ALsource&) = delete;
+    ALsource& operator=(const ALsource&) = delete;
+
+    DISABLE_ALLOC()
+
+#ifdef ALSOFT_EAX
+public:
+    void eaxInitialize(ALCcontext *context) noexcept;
+    void eaxDispatch(const EaxCall& call);
+    void eaxCommit();
+    void eaxMarkAsChanged() noexcept { mEaxChanged = true; }
+
+    static ALsource* EaxLookupSource(ALCcontext& al_context, ALuint source_id) noexcept;
+
+private:
+    using Exception = EaxSourceException;
+
+    static constexpr auto eax_max_speakers = 9;
+
+    using EaxFxSlotIds = const GUID* [EAX_MAX_FXSLOTS];
+
+    static constexpr const EaxFxSlotIds eax4_fx_slot_ids = {
+        &EAXPROPERTYID_EAX40_FXSlot0,
+        &EAXPROPERTYID_EAX40_FXSlot1,
+        &EAXPROPERTYID_EAX40_FXSlot2,
+        &EAXPROPERTYID_EAX40_FXSlot3,
+    };
+
+    static constexpr const EaxFxSlotIds eax5_fx_slot_ids = {
+        &EAXPROPERTYID_EAX50_FXSlot0,
+        &EAXPROPERTYID_EAX50_FXSlot1,
+        &EAXPROPERTYID_EAX50_FXSlot2,
+        &EAXPROPERTYID_EAX50_FXSlot3,
+    };
+
+    using EaxActiveFxSlots = std::array<bool, EAX_MAX_FXSLOTS>;
+    using EaxSpeakerLevels = std::array<EAXSPEAKERLEVELPROPERTIES, eax_max_speakers>;
+    using EaxSends = std::array<EAXSOURCEALLSENDPROPERTIES, EAX_MAX_FXSLOTS>;
+
+    using Eax1Props = EAXBUFFER_REVERBPROPERTIES;
+    struct Eax1State {
+        Eax1Props i; // Immediate.
+        Eax1Props d; // Deferred.
+    };
+
+    using Eax2Props = EAX20BUFFERPROPERTIES;
+    struct Eax2State {
+        Eax2Props i; // Immediate.
+        Eax2Props d; // Deferred.
+    };
+
+    using Eax3Props = EAX30SOURCEPROPERTIES;
+    struct Eax3State {
+        Eax3Props i; // Immediate.
+        Eax3Props d; // Deferred.
+    };
+
+    struct Eax4Props {
+        Eax3Props source;
+        EaxSends sends;
+        EAX40ACTIVEFXSLOTS active_fx_slots;
+
+        bool operator==(const Eax4Props& rhs) noexcept
+        {
+            return std::memcmp(this, &rhs, sizeof(Eax4Props)) == 0;
+        }
+    };
+
+    struct Eax4State {
+        Eax4Props i; // Immediate.
+        Eax4Props d; // Deferred.
+    };
+
+    struct Eax5Props {
+        EAX50SOURCEPROPERTIES source;
+        EaxSends sends;
+        EAX50ACTIVEFXSLOTS active_fx_slots;
+        EaxSpeakerLevels speaker_levels;
+
+        bool operator==(const Eax5Props& rhs) noexcept
+        {
+            return std::memcmp(this, &rhs, sizeof(Eax5Props)) == 0;
+        }
+    };
+
+    struct Eax5State {
+        Eax5Props i; // Immediate.
+        Eax5Props d; // Deferred.
+    };
+
+    ALCcontext* mEaxAlContext{};
+    EaxFxSlotIndex mEaxPrimaryFxSlotId{};
+    EaxActiveFxSlots mEaxActiveFxSlots{};
+    int mEaxVersion{};
+    bool mEaxChanged{};
+    Eax1State mEax1{};
+    Eax2State mEax2{};
+    Eax3State mEax3{};
+    Eax4State mEax4{};
+    Eax5State mEax5{};
+    Eax5Props mEax{};
+
+    // ----------------------------------------------------------------------
+    // Source validators
+
+    struct Eax1SourceReverbMixValidator {
+        void operator()(float reverb_mix) const
+        {
+            if (reverb_mix == EAX_REVERBMIX_USEDISTANCE)
+                return;
+
+            eax_validate_range<Exception>(
+                "Reverb Mix",
+                reverb_mix,
+                EAX_BUFFER_MINREVERBMIX,
+                EAX_BUFFER_MAXREVERBMIX);
+        }
+    };
+
+    struct Eax2SourceDirectValidator {
+        void operator()(long lDirect) const
+        {
+            eax_validate_range<Exception>(
+                "Direct",
+                lDirect,
+                EAXSOURCE_MINDIRECT,
+                EAXSOURCE_MAXDIRECT);
+        }
+    };
+
+    struct Eax2SourceDirectHfValidator {
+        void operator()(long lDirectHF) const
+        {
+            eax_validate_range<Exception>(
+                "Direct HF",
+                lDirectHF,
+                EAXSOURCE_MINDIRECTHF,
+                EAXSOURCE_MAXDIRECTHF);
+        }
+    };
+
+    struct Eax2SourceRoomValidator {
+        void operator()(long lRoom) const
+        {
+            eax_validate_range<Exception>(
+                "Room",
+                lRoom,
+                EAXSOURCE_MINROOM,
+                EAXSOURCE_MAXROOM);
+        }
+    };
+
+    struct Eax2SourceRoomHfValidator {
+        void operator()(long lRoomHF) const
+        {
+            eax_validate_range<Exception>(
+                "Room HF",
+                lRoomHF,
+                EAXSOURCE_MINROOMHF,
+                EAXSOURCE_MAXROOMHF);
+        }
+    };
+
+    struct Eax2SourceRoomRolloffFactorValidator {
+        void operator()(float flRoomRolloffFactor) const
+        {
+            eax_validate_range<Exception>(
+                "Room Rolloff Factor",
+                flRoomRolloffFactor,
+                EAXSOURCE_MINROOMROLLOFFFACTOR,
+                EAXSOURCE_MAXROOMROLLOFFFACTOR);
+        }
+    };
+
+    struct Eax2SourceObstructionValidator {
+        void operator()(long lObstruction) const
+        {
+            eax_validate_range<Exception>(
+                "Obstruction",
+                lObstruction,
+                EAXSOURCE_MINOBSTRUCTION,
+                EAXSOURCE_MAXOBSTRUCTION);
+        }
+    };
+
+    struct Eax2SourceObstructionLfRatioValidator {
+        void operator()(float flObstructionLFRatio) const
+        {
+            eax_validate_range<Exception>(
+                "Obstruction LF Ratio",
+                flObstructionLFRatio,
+                EAXSOURCE_MINOBSTRUCTIONLFRATIO,
+                EAXSOURCE_MAXOBSTRUCTIONLFRATIO);
+        }
+    };
+
+    struct Eax2SourceOcclusionValidator {
+        void operator()(long lOcclusion) const
+        {
+            eax_validate_range<Exception>(
+                "Occlusion",
+                lOcclusion,
+                EAXSOURCE_MINOCCLUSION,
+                EAXSOURCE_MAXOCCLUSION);
+        }
+    };
+
+    struct Eax2SourceOcclusionLfRatioValidator {
+        void operator()(float flOcclusionLFRatio) const
+        {
+            eax_validate_range<Exception>(
+                "Occlusion LF Ratio",
+                flOcclusionLFRatio,
+                EAXSOURCE_MINOCCLUSIONLFRATIO,
+                EAXSOURCE_MAXOCCLUSIONLFRATIO);
+        }
+    };
+
+    struct Eax2SourceOcclusionRoomRatioValidator {
+        void operator()(float flOcclusionRoomRatio) const
+        {
+            eax_validate_range<Exception>(
+                "Occlusion Room Ratio",
+                flOcclusionRoomRatio,
+                EAXSOURCE_MINOCCLUSIONROOMRATIO,
+                EAXSOURCE_MAXOCCLUSIONROOMRATIO);
+        }
+    };
+
+    struct Eax2SourceOutsideVolumeHfValidator {
+        void operator()(long lOutsideVolumeHF) const
+        {
+            eax_validate_range<Exception>(
+                "Outside Volume HF",
+                lOutsideVolumeHF,
+                EAXSOURCE_MINOUTSIDEVOLUMEHF,
+                EAXSOURCE_MAXOUTSIDEVOLUMEHF);
+        }
+    };
+
+    struct Eax2SourceAirAbsorptionFactorValidator {
+        void operator()(float flAirAbsorptionFactor) const
+        {
+            eax_validate_range<Exception>(
+                "Air Absorption Factor",
+                flAirAbsorptionFactor,
+                EAXSOURCE_MINAIRABSORPTIONFACTOR,
+                EAXSOURCE_MAXAIRABSORPTIONFACTOR);
+        }
+    };
+
+    struct Eax2SourceFlagsValidator {
+        void operator()(unsigned long dwFlags) const
+        {
+            eax_validate_range<Exception>(
+                "Flags",
+                dwFlags,
+                0UL,
+                ~EAX20SOURCEFLAGS_RESERVED);
+        }
+    };
+
+    struct Eax3SourceOcclusionDirectRatioValidator {
+        void operator()(float flOcclusionDirectRatio) const
+        {
+            eax_validate_range<Exception>(
+                "Occlusion Direct Ratio",
+                flOcclusionDirectRatio,
+                EAXSOURCE_MINOCCLUSIONDIRECTRATIO,
+                EAXSOURCE_MAXOCCLUSIONDIRECTRATIO);
+        }
+    };
+
+    struct Eax3SourceExclusionValidator {
+        void operator()(long lExclusion) const
+        {
+            eax_validate_range<Exception>(
+                "Exclusion",
+                lExclusion,
+                EAXSOURCE_MINEXCLUSION,
+                EAXSOURCE_MAXEXCLUSION);
+        }
+    };
+
+    struct Eax3SourceExclusionLfRatioValidator {
+        void operator()(float flExclusionLFRatio) const
+        {
+            eax_validate_range<Exception>(
+                "Exclusion LF Ratio",
+                flExclusionLFRatio,
+                EAXSOURCE_MINEXCLUSIONLFRATIO,
+                EAXSOURCE_MAXEXCLUSIONLFRATIO);
+        }
+    };
+
+    struct Eax3SourceDopplerFactorValidator {
+        void operator()(float flDopplerFactor) const
+        {
+            eax_validate_range<Exception>(
+                "Doppler Factor",
+                flDopplerFactor,
+                EAXSOURCE_MINDOPPLERFACTOR,
+                EAXSOURCE_MAXDOPPLERFACTOR);
+        }
+    };
+
+    struct Eax3SourceRolloffFactorValidator {
+        void operator()(float flRolloffFactor) const
+        {
+            eax_validate_range<Exception>(
+                "Rolloff Factor",
+                flRolloffFactor,
+                EAXSOURCE_MINROLLOFFFACTOR,
+                EAXSOURCE_MAXROLLOFFFACTOR);
+        }
+    };
+
+    struct Eax5SourceMacroFXFactorValidator {
+        void operator()(float flMacroFXFactor) const
+        {
+            eax_validate_range<Exception>(
+                "Macro FX Factor",
+                flMacroFXFactor,
+                EAXSOURCE_MINMACROFXFACTOR,
+                EAXSOURCE_MAXMACROFXFACTOR);
+        }
+    };
+
+    struct Eax5SourceFlagsValidator {
+        void operator()(unsigned long dwFlags) const
+        {
+            eax_validate_range<Exception>(
+                "Flags",
+                dwFlags,
+                0UL,
+                ~EAX50SOURCEFLAGS_RESERVED);
+        }
+    };
+
+    struct Eax1SourceAllValidator {
+        void operator()(const Eax1Props& props) const
+        {
+            Eax1SourceReverbMixValidator{}(props.fMix);
+        }
+    };
+
+    struct Eax2SourceAllValidator {
+        void operator()(const Eax2Props& props) const
+        {
+            Eax2SourceDirectValidator{}(props.lDirect);
+            Eax2SourceDirectHfValidator{}(props.lDirectHF);
+            Eax2SourceRoomValidator{}(props.lRoom);
+            Eax2SourceRoomHfValidator{}(props.lRoomHF);
+            Eax2SourceRoomRolloffFactorValidator{}(props.flRoomRolloffFactor);
+            Eax2SourceObstructionValidator{}(props.lObstruction);
+            Eax2SourceObstructionLfRatioValidator{}(props.flObstructionLFRatio);
+            Eax2SourceOcclusionValidator{}(props.lOcclusion);
+            Eax2SourceOcclusionLfRatioValidator{}(props.flOcclusionLFRatio);
+            Eax2SourceOcclusionRoomRatioValidator{}(props.flOcclusionRoomRatio);
+            Eax2SourceOutsideVolumeHfValidator{}(props.lOutsideVolumeHF);
+            Eax2SourceAirAbsorptionFactorValidator{}(props.flAirAbsorptionFactor);
+            Eax2SourceFlagsValidator{}(props.dwFlags);
+        }
+    };
+
+    struct Eax3SourceAllValidator {
+        void operator()(const Eax3Props& props) const
+        {
+            Eax2SourceDirectValidator{}(props.lDirect);
+            Eax2SourceDirectHfValidator{}(props.lDirectHF);
+            Eax2SourceRoomValidator{}(props.lRoom);
+            Eax2SourceRoomHfValidator{}(props.lRoomHF);
+            Eax2SourceObstructionValidator{}(props.lObstruction);
+            Eax2SourceObstructionLfRatioValidator{}(props.flObstructionLFRatio);
+            Eax2SourceOcclusionValidator{}(props.lOcclusion);
+            Eax2SourceOcclusionLfRatioValidator{}(props.flOcclusionLFRatio);
+            Eax2SourceOcclusionRoomRatioValidator{}(props.flOcclusionRoomRatio);
+            Eax3SourceOcclusionDirectRatioValidator{}(props.flOcclusionDirectRatio);
+            Eax3SourceExclusionValidator{}(props.lExclusion);
+            Eax3SourceExclusionLfRatioValidator{}(props.flExclusionLFRatio);
+            Eax2SourceOutsideVolumeHfValidator{}(props.lOutsideVolumeHF);
+            Eax3SourceDopplerFactorValidator{}(props.flDopplerFactor);
+            Eax3SourceRolloffFactorValidator{}(props.flRolloffFactor);
+            Eax2SourceRoomRolloffFactorValidator{}(props.flRoomRolloffFactor);
+            Eax2SourceAirAbsorptionFactorValidator{}(props.flAirAbsorptionFactor);
+            Eax2SourceFlagsValidator{}(props.ulFlags);
+        }
+    };
+
+    struct Eax5SourceAllValidator {
+        void operator()(const EAX50SOURCEPROPERTIES& props) const
+        {
+            Eax3SourceAllValidator{}(static_cast<const Eax3Props&>(props));
+            Eax5SourceMacroFXFactorValidator{}(props.flMacroFXFactor);
+        }
+    };
+
+    struct Eax5SourceAll2dValidator {
+        void operator()(const EAXSOURCE2DPROPERTIES& props) const
+        {
+            Eax2SourceDirectValidator{}(props.lDirect);
+            Eax2SourceDirectHfValidator{}(props.lDirectHF);
+            Eax2SourceRoomValidator{}(props.lRoom);
+            Eax2SourceRoomHfValidator{}(props.lRoomHF);
+            Eax5SourceFlagsValidator{}(props.ulFlags);
+        }
+    };
+
+    struct Eax4ObstructionValidator {
+        void operator()(const EAXOBSTRUCTIONPROPERTIES& props) const
+        {
+            Eax2SourceObstructionValidator{}(props.lObstruction);
+            Eax2SourceObstructionLfRatioValidator{}(props.flObstructionLFRatio);
+        }
+    };
+
+    struct Eax4OcclusionValidator {
+        void operator()(const EAXOCCLUSIONPROPERTIES& props) const
+        {
+            Eax2SourceOcclusionValidator{}(props.lOcclusion);
+            Eax2SourceOcclusionLfRatioValidator{}(props.flOcclusionLFRatio);
+            Eax2SourceOcclusionRoomRatioValidator{}(props.flOcclusionRoomRatio);
+            Eax3SourceOcclusionDirectRatioValidator{}(props.flOcclusionDirectRatio);
+        }
+    };
+
+    struct Eax4ExclusionValidator {
+        void operator()(const EAXEXCLUSIONPROPERTIES& props) const
+        {
+            Eax3SourceExclusionValidator{}(props.lExclusion);
+            Eax3SourceExclusionLfRatioValidator{}(props.flExclusionLFRatio);
+        }
+    };
+
+    // Source validators
+    // ----------------------------------------------------------------------
+    // Send validators
+
+    struct Eax4SendReceivingFxSlotIdValidator {
+        void operator()(const GUID& guidReceivingFXSlotID) const
+        {
+            if (guidReceivingFXSlotID != EAXPROPERTYID_EAX40_FXSlot0 &&
+                guidReceivingFXSlotID != EAXPROPERTYID_EAX40_FXSlot1 &&
+                guidReceivingFXSlotID != EAXPROPERTYID_EAX40_FXSlot2 &&
+                guidReceivingFXSlotID != EAXPROPERTYID_EAX40_FXSlot3)
+            {
+                eax_fail_unknown_receiving_fx_slot_id();
+            }
+        }
+    };
+
+    struct Eax5SendReceivingFxSlotIdValidator {
+        void operator()(const GUID& guidReceivingFXSlotID) const
+        {
+            if (guidReceivingFXSlotID != EAXPROPERTYID_EAX50_FXSlot0 &&
+                guidReceivingFXSlotID != EAXPROPERTYID_EAX50_FXSlot1 &&
+                guidReceivingFXSlotID != EAXPROPERTYID_EAX50_FXSlot2 &&
+                guidReceivingFXSlotID != EAXPROPERTYID_EAX50_FXSlot3)
+            {
+                eax_fail_unknown_receiving_fx_slot_id();
+            }
+        }
+    };
+
+    struct Eax4SendSendValidator {
+        void operator()(long lSend) const
+        {
+            eax_validate_range<Exception>(
+                "Send",
+                lSend,
+                EAXSOURCE_MINSEND,
+                EAXSOURCE_MAXSEND);
+        }
+    };
+
+    struct Eax4SendSendHfValidator {
+        void operator()(long lSendHF) const
+        {
+            eax_validate_range<Exception>(
+                "Send HF",
+                lSendHF,
+                EAXSOURCE_MINSENDHF,
+                EAXSOURCE_MAXSENDHF);
+        }
+    };
+
+    template<typename TIdValidator>
+    struct EaxSendValidator {
+        void operator()(const EAXSOURCESENDPROPERTIES& props) const
+        {
+            TIdValidator{}(props.guidReceivingFXSlotID);
+            Eax4SendSendValidator{}(props.lSend);
+            Eax4SendSendHfValidator{}(props.lSendHF);
+        }
+    };
+
+    struct Eax4SendValidator : EaxSendValidator<Eax4SendReceivingFxSlotIdValidator> {};
+    struct Eax5SendValidator : EaxSendValidator<Eax5SendReceivingFxSlotIdValidator> {};
+
+    template<typename TIdValidator>
+    struct EaxOcclusionSendValidator {
+        void operator()(const EAXSOURCEOCCLUSIONSENDPROPERTIES& props) const
+        {
+            TIdValidator{}(props.guidReceivingFXSlotID);
+            Eax2SourceOcclusionValidator{}(props.lOcclusion);
+            Eax2SourceOcclusionLfRatioValidator{}(props.flOcclusionLFRatio);
+            Eax2SourceOcclusionRoomRatioValidator{}(props.flOcclusionRoomRatio);
+            Eax3SourceOcclusionDirectRatioValidator{}(props.flOcclusionDirectRatio);
+        }
+    };
+
+    struct Eax4OcclusionSendValidator : EaxOcclusionSendValidator<Eax4SendReceivingFxSlotIdValidator> {};
+    struct Eax5OcclusionSendValidator : EaxOcclusionSendValidator<Eax5SendReceivingFxSlotIdValidator> {};
+
+    template<typename TIdValidator>
+    struct EaxExclusionSendValidator {
+        void operator()(const EAXSOURCEEXCLUSIONSENDPROPERTIES& props) const
+        {
+            TIdValidator{}(props.guidReceivingFXSlotID);
+            Eax3SourceExclusionValidator{}(props.lExclusion);
+            Eax3SourceExclusionLfRatioValidator{}(props.flExclusionLFRatio);
+        }
+    };
+
+    struct Eax4ExclusionSendValidator : EaxExclusionSendValidator<Eax4SendReceivingFxSlotIdValidator> {};
+    struct Eax5ExclusionSendValidator : EaxExclusionSendValidator<Eax5SendReceivingFxSlotIdValidator> {};
+
+    template<typename TIdValidator>
+    struct EaxAllSendValidator {
+        void operator()(const EAXSOURCEALLSENDPROPERTIES& props) const
+        {
+            TIdValidator{}(props.guidReceivingFXSlotID);
+            Eax4SendSendValidator{}(props.lSend);
+            Eax4SendSendHfValidator{}(props.lSendHF);
+            Eax2SourceOcclusionValidator{}(props.lOcclusion);
+            Eax2SourceOcclusionLfRatioValidator{}(props.flOcclusionLFRatio);
+            Eax2SourceOcclusionRoomRatioValidator{}(props.flOcclusionRoomRatio);
+            Eax3SourceOcclusionDirectRatioValidator{}(props.flOcclusionDirectRatio);
+            Eax3SourceExclusionValidator{}(props.lExclusion);
+            Eax3SourceExclusionLfRatioValidator{}(props.flExclusionLFRatio);
+        }
+    };
+
+    struct Eax4AllSendValidator : EaxAllSendValidator<Eax4SendReceivingFxSlotIdValidator> {};
+    struct Eax5AllSendValidator : EaxAllSendValidator<Eax5SendReceivingFxSlotIdValidator> {};
+
+    // Send validators
+    // ----------------------------------------------------------------------
+    // Active FX slot ID validators
+
+    struct Eax4ActiveFxSlotIdValidator {
+        void operator()(const GUID &guid) const
+        {
+            if(guid != EAX_NULL_GUID && guid != EAX_PrimaryFXSlotID
+                && guid != EAXPROPERTYID_EAX40_FXSlot0 && guid != EAXPROPERTYID_EAX40_FXSlot1
+                && guid != EAXPROPERTYID_EAX40_FXSlot2 && guid != EAXPROPERTYID_EAX40_FXSlot3)
+            {
+                eax_fail_unknown_active_fx_slot_id();
+            }
+        }
+    };
+
+    struct Eax5ActiveFxSlotIdValidator {
+        void operator()(const GUID &guid) const
+        {
+            if(guid != EAX_NULL_GUID && guid != EAX_PrimaryFXSlotID
+                && guid != EAXPROPERTYID_EAX50_FXSlot0 && guid != EAXPROPERTYID_EAX50_FXSlot1
+                && guid != EAXPROPERTYID_EAX50_FXSlot2 && guid != EAXPROPERTYID_EAX50_FXSlot3)
+            {
+                eax_fail_unknown_active_fx_slot_id();
+            }
+        }
+    };
+
+    // Active FX slot ID validators
+    // ----------------------------------------------------------------------
+    // Speaker level validators.
+
+    struct Eax5SpeakerIdValidator {
+        void operator()(long lSpeakerID) const
+        {
+            switch (lSpeakerID) {
+                case EAXSPEAKER_FRONT_LEFT:
+                case EAXSPEAKER_FRONT_CENTER:
+                case EAXSPEAKER_FRONT_RIGHT:
+                case EAXSPEAKER_SIDE_RIGHT:
+                case EAXSPEAKER_REAR_RIGHT:
+                case EAXSPEAKER_REAR_CENTER:
+                case EAXSPEAKER_REAR_LEFT:
+                case EAXSPEAKER_SIDE_LEFT:
+                case EAXSPEAKER_LOW_FREQUENCY:
+                    break;
+
+                default:
+                    eax_fail("Unknown speaker ID.");
+            }
+        }
+    };
+
+    struct Eax5SpeakerLevelValidator {
+        void operator()(long lLevel) const
+        {
+            // TODO Use a range when the feature will be implemented.
+            if (lLevel != EAXSOURCE_DEFAULTSPEAKERLEVEL)
+                eax_fail("Speaker level out of range.");
+        }
+    };
+
+    struct Eax5SpeakerAllValidator {
+        void operator()(const EAXSPEAKERLEVELPROPERTIES& all) const
+        {
+            Eax5SpeakerIdValidator{}(all.lSpeakerID);
+            Eax5SpeakerLevelValidator{}(all.lLevel);
+        }
+    };
+
+    // Speaker level validators.
+    // ----------------------------------------------------------------------
+
+    struct Eax4SendIndexGetter {
+        EaxFxSlotIndexValue operator()(const GUID &guid) const
+        {
+            if(guid == EAXPROPERTYID_EAX40_FXSlot0)
+                return 0;
+            if(guid == EAXPROPERTYID_EAX40_FXSlot1)
+                return 1;
+            if(guid == EAXPROPERTYID_EAX40_FXSlot2)
+                return 2;
+            if(guid == EAXPROPERTYID_EAX40_FXSlot3)
+                return 3;
+            eax_fail_unknown_receiving_fx_slot_id();
+        }
+    };
+
+    struct Eax5SendIndexGetter {
+        EaxFxSlotIndexValue operator()(const GUID &guid) const
+        {
+            if(guid == EAXPROPERTYID_EAX50_FXSlot0)
+                return 0;
+            if(guid == EAXPROPERTYID_EAX50_FXSlot1)
+                return 1;
+            if(guid == EAXPROPERTYID_EAX50_FXSlot2)
+                return 2;
+            if(guid == EAXPROPERTYID_EAX50_FXSlot3)
+                return 3;
+            eax_fail_unknown_receiving_fx_slot_id();
+        }
+    };
+
+    [[noreturn]] static void eax_fail(const char* message);
+    [[noreturn]] static void eax_fail_unknown_property_id();
+    [[noreturn]] static void eax_fail_unknown_version();
+    [[noreturn]] static void eax_fail_unknown_active_fx_slot_id();
+    [[noreturn]] static void eax_fail_unknown_receiving_fx_slot_id();
+
+    void eax_set_sends_defaults(EaxSends& sends, const EaxFxSlotIds& ids) noexcept;
+    void eax1_set_defaults(Eax1Props& props) noexcept;
+    void eax1_set_defaults() noexcept;
+    void eax2_set_defaults(Eax2Props& props) noexcept;
+    void eax2_set_defaults() noexcept;
+    void eax3_set_defaults(Eax3Props& props) noexcept;
+    void eax3_set_defaults() noexcept;
+    void eax4_set_sends_defaults(EaxSends& sends) noexcept;
+    void eax4_set_active_fx_slots_defaults(EAX40ACTIVEFXSLOTS& slots) noexcept;
+    void eax4_set_defaults() noexcept;
+    void eax5_set_source_defaults(EAX50SOURCEPROPERTIES& props) noexcept;
+    void eax5_set_sends_defaults(EaxSends& sends) noexcept;
+    void eax5_set_active_fx_slots_defaults(EAX50ACTIVEFXSLOTS& slots) noexcept;
+    void eax5_set_speaker_levels_defaults(EaxSpeakerLevels& speaker_levels) noexcept;
+    void eax5_set_defaults(Eax5Props& props) noexcept;
+    void eax5_set_defaults() noexcept;
+    void eax_set_defaults() noexcept;
+
+    void eax1_translate(const Eax1Props& src, Eax5Props& dst) noexcept;
+    void eax2_translate(const Eax2Props& src, Eax5Props& dst) noexcept;
+    void eax3_translate(const Eax3Props& src, Eax5Props& dst) noexcept;
+    void eax4_translate(const Eax4Props& src, Eax5Props& dst) 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_update_direct_filter();
+    void eax_update_room_filters();
+    void eax_commit_filters();
+
+    static void eax_copy_send_for_get(
+        const EAXSOURCEALLSENDPROPERTIES& src,
+        EAXSOURCESENDPROPERTIES& dst) noexcept
+    {
+        dst = reinterpret_cast<const EAXSOURCESENDPROPERTIES&>(src);
+    }
+
+    static void eax_copy_send_for_get(
+        const EAXSOURCEALLSENDPROPERTIES& src,
+        EAXSOURCEALLSENDPROPERTIES& dst) noexcept
+    {
+        dst = src;
+    }
+
+    static void eax_copy_send_for_get(
+        const EAXSOURCEALLSENDPROPERTIES& src,
+        EAXSOURCEOCCLUSIONSENDPROPERTIES& dst) noexcept
+    {
+        dst.guidReceivingFXSlotID = src.guidReceivingFXSlotID;
+        dst.lOcclusion = src.lOcclusion;
+        dst.flOcclusionLFRatio = src.flOcclusionLFRatio;
+        dst.flOcclusionRoomRatio = src.flOcclusionRoomRatio;
+        dst.flOcclusionDirectRatio = src.flOcclusionDirectRatio;
+    }
+
+    static void eax_copy_send_for_get(
+        const EAXSOURCEALLSENDPROPERTIES& src,
+        EAXSOURCEEXCLUSIONSENDPROPERTIES& dst) noexcept
+    {
+        dst.guidReceivingFXSlotID = src.guidReceivingFXSlotID;
+        dst.lExclusion = src.lExclusion;
+        dst.flExclusionLFRatio = src.flExclusionLFRatio;
+    }
+
+    template<typename TDstSend>
+    void eax_get_sends(const EaxCall& call, const EaxSends& src_sends)
+    {
+        const auto dst_sends = call.get_values<TDstSend>(EAX_MAX_FXSLOTS);
+        const auto count = dst_sends.size();
+
+        for (auto i = decltype(count){}; i < count; ++i) {
+            const auto& src_send = src_sends[i];
+            auto& dst_send = dst_sends[i];
+            eax_copy_send_for_get(src_send, dst_send);
+        }
+    }
+
+    void eax_get_active_fx_slot_id(const EaxCall& call, const GUID* ids, size_t max_count);
+    void eax1_get(const EaxCall& call, const Eax1Props& props);
+    void eax2_get(const EaxCall& call, const Eax2Props& props);
+    void eax3_get_obstruction(const EaxCall& call, const Eax3Props& props);
+    void eax3_get_occlusion(const EaxCall& call, const Eax3Props& props);
+    void eax3_get_exclusion(const EaxCall& call, const Eax3Props& props);
+    void eax3_get(const EaxCall& call, const Eax3Props& props);
+    void eax4_get(const EaxCall& call, const Eax4Props& props);
+    void eax5_get_all_2d(const EaxCall& call, const EAX50SOURCEPROPERTIES& props);
+    void eax5_get_speaker_levels(const EaxCall& call, const EaxSpeakerLevels& props);
+    void eax5_get(const EaxCall& call, const Eax5Props& props);
+    void eax_get(const EaxCall& call);
+
+    static void eax_copy_send_for_set(
+        const EAXSOURCESENDPROPERTIES& src,
+        EAXSOURCEALLSENDPROPERTIES& dst) noexcept
+    {
+        dst.lSend = src.lSend;
+        dst.lSendHF = src.lSendHF;
+    }
+
+    static void eax_copy_send_for_set(
+        const EAXSOURCEALLSENDPROPERTIES& src,
+        EAXSOURCEALLSENDPROPERTIES& dst) noexcept
+    {
+        dst.lSend = src.lSend;
+        dst.lSendHF = src.lSendHF;
+        dst.lOcclusion = src.lOcclusion;
+        dst.flOcclusionLFRatio = src.flOcclusionLFRatio;
+        dst.flOcclusionRoomRatio = src.flOcclusionRoomRatio;
+        dst.flOcclusionDirectRatio = src.flOcclusionDirectRatio;
+        dst.lExclusion = src.lExclusion;
+        dst.flExclusionLFRatio = src.flExclusionLFRatio;
+    }
+
+    static void eax_copy_send_for_set(
+        const EAXSOURCEOCCLUSIONSENDPROPERTIES& src,
+        EAXSOURCEALLSENDPROPERTIES& dst) noexcept
+    {
+        dst.lOcclusion = src.lOcclusion;
+        dst.flOcclusionLFRatio = src.flOcclusionLFRatio;
+        dst.flOcclusionRoomRatio = src.flOcclusionRoomRatio;
+        dst.flOcclusionDirectRatio = src.flOcclusionDirectRatio;
+    }
+
+    static void eax_copy_send_for_set(
+        const EAXSOURCEEXCLUSIONSENDPROPERTIES& src,
+        EAXSOURCEALLSENDPROPERTIES& dst) noexcept
+    {
+        dst.lExclusion = src.lExclusion;
+        dst.flExclusionLFRatio = src.flExclusionLFRatio;
+    }
+
+    template<typename TValidator, typename TIndexGetter, typename TSrcSend>
+    void eax_defer_sends(const EaxCall& call, EaxSends& dst_sends)
+    {
+        const auto src_sends = call.get_values<const TSrcSend>(EAX_MAX_FXSLOTS);
+        std::for_each(src_sends.cbegin(), src_sends.cend(), TValidator{});
+        const auto count = src_sends.size();
+        const auto index_getter = TIndexGetter{};
+
+        for (auto i = decltype(count){}; i < count; ++i) {
+            const auto& src_send = src_sends[i];
+            const auto dst_index = index_getter(src_send.guidReceivingFXSlotID);
+            auto& dst_send = dst_sends[dst_index];
+            eax_copy_send_for_set(src_send, dst_send);
+        }
+    }
+
+    template<typename TValidator, typename TSrcSend>
+    void eax4_defer_sends(const EaxCall& call, EaxSends& dst_sends)
+    {
+        eax_defer_sends<TValidator, Eax4SendIndexGetter, TSrcSend>(call, dst_sends);
+    }
+
+    template<typename TValidator, typename TSrcSend>
+    void eax5_defer_sends(const EaxCall& call, EaxSends& dst_sends)
+    {
+        eax_defer_sends<TValidator, Eax5SendIndexGetter, TSrcSend>(call, dst_sends);
+    }
+
+    template<typename TValidator, size_t TIdCount>
+    void eax_defer_active_fx_slot_id(const EaxCall& call, GUID (&dst_ids)[TIdCount])
+    {
+        const auto src_ids = call.get_values<const GUID>(TIdCount);
+        std::for_each(src_ids.cbegin(), src_ids.cend(), TValidator{});
+        std::uninitialized_copy(src_ids.cbegin(), src_ids.cend(), dst_ids);
+    }
+
+    template<size_t TIdCount>
+    void eax4_defer_active_fx_slot_id(const EaxCall& call, GUID (&dst_ids)[TIdCount])
+    {
+        eax_defer_active_fx_slot_id<Eax4ActiveFxSlotIdValidator>(call, dst_ids);
+    }
+
+    template<size_t TIdCount>
+    void eax5_defer_active_fx_slot_id(const EaxCall& call, GUID (&dst_ids)[TIdCount])
+    {
+        eax_defer_active_fx_slot_id<Eax5ActiveFxSlotIdValidator>(call, dst_ids);
+    }
+
+    template<typename TValidator, typename TProperty>
+    static void eax_defer(const EaxCall& call, TProperty& property)
+    {
+        const auto& value = call.get_value<Exception, const TProperty>();
+        TValidator{}(value);
+        property = value;
+    }
+
+    // Defers source's sub-properties (obstruction, occlusion, exclusion).
+    template<typename TValidator, typename TSubproperty, typename TProperty>
+    void eax_defer_sub(const EaxCall& call, TProperty& property)
+    {
+        const auto& src_props = call.get_value<Exception, const TSubproperty>();
+        TValidator{}(src_props);
+        auto& dst_props = reinterpret_cast<TSubproperty&>(property);
+        dst_props = src_props;
+    }
+
+    void eax_set_efx_outer_gain_hf();
+    void eax_set_efx_doppler_factor();
+    void eax_set_efx_rolloff_factor();
+    void eax_set_efx_room_rolloff_factor();
+    void eax_set_efx_air_absorption_factor();
+    void eax_set_efx_dry_gain_hf_auto();
+    void eax_set_efx_wet_gain_auto();
+    void eax_set_efx_wet_gain_hf_auto();
+
+    void eax1_set(const EaxCall& call, Eax1Props& props);
+    void eax2_set(const EaxCall& call, Eax2Props& props);
+    void eax3_set(const EaxCall& call, Eax3Props& props);
+    void eax4_set(const EaxCall& call, Eax4Props& props);
+    void eax5_defer_all_2d(const EaxCall& call, EAX50SOURCEPROPERTIES& props);
+    void eax5_defer_speaker_levels(const EaxCall& call, EaxSpeakerLevels& props);
+    void eax5_set(const EaxCall& call, Eax5Props& props);
+    void eax_set(const EaxCall& call);
+
+    // `alSource3i(source, AL_AUXILIARY_SEND_FILTER, ...)`
+    void eax_set_al_source_send(ALeffectslot *slot, size_t sendidx,
+        const EaxAlLowPassParam &filter);
+
+    void eax_commit_active_fx_slots();
+#endif // ALSOFT_EAX
+};
+
+void UpdateAllSourceProps(ALCcontext *context);
+
+#endif
diff --git a/al/state.cpp b/al/state.cpp
new file mode 100644 (file)
index 0000000..86d81b1
--- /dev/null
@@ -0,0 +1,963 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 1999-2000 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 "version.h"
+
+#include <atomic>
+#include <cmath>
+#include <mutex>
+#include <stdexcept>
+#include <string>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "alc/alu.h"
+#include "alc/context.h"
+#include "alc/inprogext.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "atomic.h"
+#include "core/context.h"
+#include "core/except.h"
+#include "core/mixer/defs.h"
+#include "core/voice.h"
+#include "intrusive_ptr.h"
+#include "opthelpers.h"
+#include "strutils.h"
+
+#ifdef ALSOFT_EAX
+#include "alc/device.h"
+
+#include "eax/globals.h"
+#include "eax/x_ram.h"
+#endif // ALSOFT_EAX
+
+
+namespace {
+
+constexpr ALchar alVendor[] = "OpenAL Community";
+constexpr ALchar alVersion[] = "1.1 ALSOFT " ALSOFT_VERSION;
+constexpr ALchar alRenderer[] = "OpenAL Soft";
+
+// Error Messages
+constexpr ALchar alNoError[] = "No Error";
+constexpr ALchar alErrInvalidName[] = "Invalid Name";
+constexpr ALchar alErrInvalidEnum[] = "Invalid Enum";
+constexpr ALchar alErrInvalidValue[] = "Invalid Value";
+constexpr ALchar alErrInvalidOp[] = "Invalid Operation";
+constexpr ALchar alErrOutOfMemory[] = "Out of Memory";
+
+/* Resampler strings */
+template<Resampler rtype> struct ResamplerName { };
+template<> struct ResamplerName<Resampler::Point>
+{ static constexpr const ALchar *Get() noexcept { return "Nearest"; } };
+template<> struct ResamplerName<Resampler::Linear>
+{ static constexpr const ALchar *Get() noexcept { return "Linear"; } };
+template<> struct ResamplerName<Resampler::Cubic>
+{ static constexpr const ALchar *Get() noexcept { return "Cubic"; } };
+template<> struct ResamplerName<Resampler::FastBSinc12>
+{ static constexpr const ALchar *Get() noexcept { return "11th order Sinc (fast)"; } };
+template<> struct ResamplerName<Resampler::BSinc12>
+{ static constexpr const ALchar *Get() noexcept { return "11th order Sinc"; } };
+template<> struct ResamplerName<Resampler::FastBSinc24>
+{ static constexpr const ALchar *Get() noexcept { return "23rd order Sinc (fast)"; } };
+template<> struct ResamplerName<Resampler::BSinc24>
+{ static constexpr const ALchar *Get() noexcept { return "23rd order Sinc"; } };
+
+const ALchar *GetResamplerName(const Resampler rtype)
+{
+#define HANDLE_RESAMPLER(r) case r: return ResamplerName<r>::Get()
+    switch(rtype)
+    {
+    HANDLE_RESAMPLER(Resampler::Point);
+    HANDLE_RESAMPLER(Resampler::Linear);
+    HANDLE_RESAMPLER(Resampler::Cubic);
+    HANDLE_RESAMPLER(Resampler::FastBSinc12);
+    HANDLE_RESAMPLER(Resampler::BSinc12);
+    HANDLE_RESAMPLER(Resampler::FastBSinc24);
+    HANDLE_RESAMPLER(Resampler::BSinc24);
+    }
+#undef HANDLE_RESAMPLER
+    /* Should never get here. */
+    throw std::runtime_error{"Unexpected resampler index"};
+}
+
+al::optional<DistanceModel> DistanceModelFromALenum(ALenum model)
+{
+    switch(model)
+    {
+    case AL_NONE: return DistanceModel::Disable;
+    case AL_INVERSE_DISTANCE: return DistanceModel::Inverse;
+    case AL_INVERSE_DISTANCE_CLAMPED: return DistanceModel::InverseClamped;
+    case AL_LINEAR_DISTANCE: return DistanceModel::Linear;
+    case AL_LINEAR_DISTANCE_CLAMPED: return DistanceModel::LinearClamped;
+    case AL_EXPONENT_DISTANCE: return DistanceModel::Exponent;
+    case AL_EXPONENT_DISTANCE_CLAMPED: return DistanceModel::ExponentClamped;
+    }
+    return al::nullopt;
+}
+ALenum ALenumFromDistanceModel(DistanceModel model)
+{
+    switch(model)
+    {
+    case DistanceModel::Disable: return AL_NONE;
+    case DistanceModel::Inverse: return AL_INVERSE_DISTANCE;
+    case DistanceModel::InverseClamped: return AL_INVERSE_DISTANCE_CLAMPED;
+    case DistanceModel::Linear: return AL_LINEAR_DISTANCE;
+    case DistanceModel::LinearClamped: return AL_LINEAR_DISTANCE_CLAMPED;
+    case DistanceModel::Exponent: return AL_EXPONENT_DISTANCE;
+    case DistanceModel::ExponentClamped: return AL_EXPONENT_DISTANCE_CLAMPED;
+    }
+    throw std::runtime_error{"Unexpected distance model "+std::to_string(static_cast<int>(model))};
+}
+
+} // namespace
+
+/* WARNING: Non-standard export! Not part of any extension, or exposed in the
+ * alcFunctions list.
+ */
+AL_API const ALchar* AL_APIENTRY alsoft_get_version(void)
+START_API_FUNC
+{
+    static const auto spoof = al::getenv("ALSOFT_SPOOF_VERSION");
+    if(spoof) return spoof->c_str();
+    return ALSOFT_VERSION;
+}
+END_API_FUNC
+
+#define DO_UPDATEPROPS() do {                                                 \
+    if(!context->mDeferUpdates)                                               \
+        UpdateContextProps(context.get());                                    \
+    else                                                                      \
+        context->mPropsDirty = true;                                          \
+} while(0)
+
+
+AL_API void AL_APIENTRY alEnable(ALenum capability)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    switch(capability)
+    {
+    case AL_SOURCE_DISTANCE_MODEL:
+        {
+            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:
+        context->setError(AL_INVALID_VALUE, "Invalid enable property 0x%04x", capability);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDisable(ALenum capability)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    switch(capability)
+    {
+    case AL_SOURCE_DISTANCE_MODEL:
+        {
+            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:
+        context->setError(AL_INVALID_VALUE, "Invalid disable property 0x%04x", capability);
+    }
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alIsEnabled(ALenum capability)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return AL_FALSE;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    ALboolean value{AL_FALSE};
+    switch(capability)
+    {
+    case AL_SOURCE_DISTANCE_MODEL:
+        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);
+    }
+
+    return value;
+}
+END_API_FUNC
+
+AL_API ALboolean AL_APIENTRY alGetBoolean(ALenum pname)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return AL_FALSE;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    ALboolean value{AL_FALSE};
+    switch(pname)
+    {
+    case AL_DOPPLER_FACTOR:
+        if(context->mDopplerFactor != 0.0f)
+            value = AL_TRUE;
+        break;
+
+    case AL_DOPPLER_VELOCITY:
+        if(context->mDopplerVelocity != 0.0f)
+            value = AL_TRUE;
+        break;
+
+    case AL_DISTANCE_MODEL:
+        if(context->mDistanceModel == DistanceModel::Default)
+            value = AL_TRUE;
+        break;
+
+    case AL_SPEED_OF_SOUND:
+        if(context->mSpeedOfSound != 0.0f)
+            value = AL_TRUE;
+        break;
+
+    case AL_DEFERRED_UPDATES_SOFT:
+        if(context->mDeferUpdates)
+            value = AL_TRUE;
+        break;
+
+    case AL_GAIN_LIMIT_SOFT:
+        if(GainMixMax/context->mGainBoost != 0.0f)
+            value = AL_TRUE;
+        break;
+
+    case AL_NUM_RESAMPLERS_SOFT:
+        /* Always non-0. */
+        value = AL_TRUE;
+        break;
+
+    case AL_DEFAULT_RESAMPLER_SOFT:
+        value = static_cast<int>(ResamplerDefault) ? AL_TRUE : AL_FALSE;
+        break;
+
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid boolean property 0x%04x", pname);
+    }
+
+    return value;
+}
+END_API_FUNC
+
+AL_API ALdouble AL_APIENTRY alGetDouble(ALenum pname)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return 0.0;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    ALdouble value{0.0};
+    switch(pname)
+    {
+    case AL_DOPPLER_FACTOR:
+        value = context->mDopplerFactor;
+        break;
+
+    case AL_DOPPLER_VELOCITY:
+        value = context->mDopplerVelocity;
+        break;
+
+    case AL_DISTANCE_MODEL:
+        value = static_cast<ALdouble>(ALenumFromDistanceModel(context->mDistanceModel));
+        break;
+
+    case AL_SPEED_OF_SOUND:
+        value = context->mSpeedOfSound;
+        break;
+
+    case AL_DEFERRED_UPDATES_SOFT:
+        if(context->mDeferUpdates)
+            value = static_cast<ALdouble>(AL_TRUE);
+        break;
+
+    case AL_GAIN_LIMIT_SOFT:
+        value = ALdouble{GainMixMax}/context->mGainBoost;
+        break;
+
+    case AL_NUM_RESAMPLERS_SOFT:
+        value = static_cast<ALdouble>(Resampler::Max) + 1.0;
+        break;
+
+    case AL_DEFAULT_RESAMPLER_SOFT:
+        value = static_cast<ALdouble>(ResamplerDefault);
+        break;
+
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid double property 0x%04x", pname);
+    }
+
+    return value;
+}
+END_API_FUNC
+
+AL_API ALfloat AL_APIENTRY alGetFloat(ALenum pname)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return 0.0f;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    ALfloat value{0.0f};
+    switch(pname)
+    {
+    case AL_DOPPLER_FACTOR:
+        value = context->mDopplerFactor;
+        break;
+
+    case AL_DOPPLER_VELOCITY:
+        value = context->mDopplerVelocity;
+        break;
+
+    case AL_DISTANCE_MODEL:
+        value = static_cast<ALfloat>(ALenumFromDistanceModel(context->mDistanceModel));
+        break;
+
+    case AL_SPEED_OF_SOUND:
+        value = context->mSpeedOfSound;
+        break;
+
+    case AL_DEFERRED_UPDATES_SOFT:
+        if(context->mDeferUpdates)
+            value = static_cast<ALfloat>(AL_TRUE);
+        break;
+
+    case AL_GAIN_LIMIT_SOFT:
+        value = GainMixMax/context->mGainBoost;
+        break;
+
+    case AL_NUM_RESAMPLERS_SOFT:
+        value = static_cast<ALfloat>(Resampler::Max) + 1.0f;
+        break;
+
+    case AL_DEFAULT_RESAMPLER_SOFT:
+        value = static_cast<ALfloat>(ResamplerDefault);
+        break;
+
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid float property 0x%04x", pname);
+    }
+
+    return value;
+}
+END_API_FUNC
+
+AL_API ALint AL_APIENTRY alGetInteger(ALenum pname)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return 0;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    ALint value{0};
+    switch(pname)
+    {
+    case AL_DOPPLER_FACTOR:
+        value = static_cast<ALint>(context->mDopplerFactor);
+        break;
+
+    case AL_DOPPLER_VELOCITY:
+        value = static_cast<ALint>(context->mDopplerVelocity);
+        break;
+
+    case AL_DISTANCE_MODEL:
+        value = ALenumFromDistanceModel(context->mDistanceModel);
+        break;
+
+    case AL_SPEED_OF_SOUND:
+        value = static_cast<ALint>(context->mSpeedOfSound);
+        break;
+
+    case AL_DEFERRED_UPDATES_SOFT:
+        if(context->mDeferUpdates)
+            value = AL_TRUE;
+        break;
+
+    case AL_GAIN_LIMIT_SOFT:
+        value = static_cast<ALint>(GainMixMax/context->mGainBoost);
+        break;
+
+    case AL_NUM_RESAMPLERS_SOFT:
+        value = static_cast<int>(Resampler::Max) + 1;
+        break;
+
+    case AL_DEFAULT_RESAMPLER_SOFT:
+        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);
+    }
+
+    return value;
+}
+END_API_FUNC
+
+AL_API ALint64SOFT AL_APIENTRY alGetInteger64SOFT(ALenum pname)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return 0_i64;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    ALint64SOFT value{0};
+    switch(pname)
+    {
+    case AL_DOPPLER_FACTOR:
+        value = static_cast<ALint64SOFT>(context->mDopplerFactor);
+        break;
+
+    case AL_DOPPLER_VELOCITY:
+        value = static_cast<ALint64SOFT>(context->mDopplerVelocity);
+        break;
+
+    case AL_DISTANCE_MODEL:
+        value = ALenumFromDistanceModel(context->mDistanceModel);
+        break;
+
+    case AL_SPEED_OF_SOUND:
+        value = static_cast<ALint64SOFT>(context->mSpeedOfSound);
+        break;
+
+    case AL_DEFERRED_UPDATES_SOFT:
+        if(context->mDeferUpdates)
+            value = AL_TRUE;
+        break;
+
+    case AL_GAIN_LIMIT_SOFT:
+        value = static_cast<ALint64SOFT>(GainMixMax/context->mGainBoost);
+        break;
+
+    case AL_NUM_RESAMPLERS_SOFT:
+        value = static_cast<ALint64SOFT>(Resampler::Max) + 1;
+        break;
+
+    case AL_DEFAULT_RESAMPLER_SOFT:
+        value = static_cast<ALint64SOFT>(ResamplerDefault);
+        break;
+
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid integer64 property 0x%04x", pname);
+    }
+
+    return value;
+}
+END_API_FUNC
+
+AL_API ALvoid* AL_APIENTRY alGetPointerSOFT(ALenum pname)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return nullptr;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    void *value{nullptr};
+    switch(pname)
+    {
+    case AL_EVENT_CALLBACK_FUNCTION_SOFT:
+        value = reinterpret_cast<void*>(context->mEventCb);
+        break;
+
+    case AL_EVENT_CALLBACK_USER_PARAM_SOFT:
+        value = context->mEventParam;
+        break;
+
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid pointer property 0x%04x", pname);
+    }
+
+    return value;
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetBooleanv(ALenum pname, ALboolean *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(pname)
+        {
+            case AL_DOPPLER_FACTOR:
+            case AL_DOPPLER_VELOCITY:
+            case AL_DISTANCE_MODEL:
+            case AL_SPEED_OF_SOUND:
+            case AL_DEFERRED_UPDATES_SOFT:
+            case AL_GAIN_LIMIT_SOFT:
+            case AL_NUM_RESAMPLERS_SOFT:
+            case AL_DEFAULT_RESAMPLER_SOFT:
+                values[0] = alGetBoolean(pname);
+                return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(pname)
+    {
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid boolean-vector property 0x%04x", pname);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetDoublev(ALenum pname, ALdouble *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(pname)
+        {
+            case AL_DOPPLER_FACTOR:
+            case AL_DOPPLER_VELOCITY:
+            case AL_DISTANCE_MODEL:
+            case AL_SPEED_OF_SOUND:
+            case AL_DEFERRED_UPDATES_SOFT:
+            case AL_GAIN_LIMIT_SOFT:
+            case AL_NUM_RESAMPLERS_SOFT:
+            case AL_DEFAULT_RESAMPLER_SOFT:
+                values[0] = alGetDouble(pname);
+                return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(pname)
+    {
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid double-vector property 0x%04x", pname);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetFloatv(ALenum pname, ALfloat *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(pname)
+        {
+            case AL_DOPPLER_FACTOR:
+            case AL_DOPPLER_VELOCITY:
+            case AL_DISTANCE_MODEL:
+            case AL_SPEED_OF_SOUND:
+            case AL_DEFERRED_UPDATES_SOFT:
+            case AL_GAIN_LIMIT_SOFT:
+            case AL_NUM_RESAMPLERS_SOFT:
+            case AL_DEFAULT_RESAMPLER_SOFT:
+                values[0] = alGetFloat(pname);
+                return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(pname)
+    {
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid float-vector property 0x%04x", pname);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetIntegerv(ALenum pname, ALint *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(pname)
+        {
+            case AL_DOPPLER_FACTOR:
+            case AL_DOPPLER_VELOCITY:
+            case AL_DISTANCE_MODEL:
+            case AL_SPEED_OF_SOUND:
+            case AL_DEFERRED_UPDATES_SOFT:
+            case AL_GAIN_LIMIT_SOFT:
+            case AL_NUM_RESAMPLERS_SOFT:
+            case AL_DEFAULT_RESAMPLER_SOFT:
+                values[0] = alGetInteger(pname);
+                return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(pname)
+    {
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid integer-vector property 0x%04x", pname);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetInteger64vSOFT(ALenum pname, ALint64SOFT *values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(pname)
+        {
+            case AL_DOPPLER_FACTOR:
+            case AL_DOPPLER_VELOCITY:
+            case AL_DISTANCE_MODEL:
+            case AL_SPEED_OF_SOUND:
+            case AL_DEFERRED_UPDATES_SOFT:
+            case AL_GAIN_LIMIT_SOFT:
+            case AL_NUM_RESAMPLERS_SOFT:
+            case AL_DEFAULT_RESAMPLER_SOFT:
+                values[0] = alGetInteger64SOFT(pname);
+                return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(pname)
+    {
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid integer64-vector property 0x%04x", pname);
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alGetPointervSOFT(ALenum pname, ALvoid **values)
+START_API_FUNC
+{
+    if(values)
+    {
+        switch(pname)
+        {
+            case AL_EVENT_CALLBACK_FUNCTION_SOFT:
+            case AL_EVENT_CALLBACK_USER_PARAM_SOFT:
+                values[0] = alGetPointerSOFT(pname);
+                return;
+        }
+    }
+
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!values)
+        context->setError(AL_INVALID_VALUE, "NULL pointer");
+    else switch(pname)
+    {
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid pointer-vector property 0x%04x", pname);
+    }
+}
+END_API_FUNC
+
+AL_API const ALchar* AL_APIENTRY alGetString(ALenum pname)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return nullptr;
+
+    const ALchar *value{nullptr};
+    switch(pname)
+    {
+    case AL_VENDOR:
+        value = alVendor;
+        break;
+
+    case AL_VERSION:
+        value = alVersion;
+        break;
+
+    case AL_RENDERER:
+        value = alRenderer;
+        break;
+
+    case AL_EXTENSIONS:
+        value = context->mExtensionList;
+        break;
+
+    case AL_NO_ERROR:
+        value = alNoError;
+        break;
+
+    case AL_INVALID_NAME:
+        value = alErrInvalidName;
+        break;
+
+    case AL_INVALID_ENUM:
+        value = alErrInvalidEnum;
+        break;
+
+    case AL_INVALID_VALUE:
+        value = alErrInvalidValue;
+        break;
+
+    case AL_INVALID_OPERATION:
+        value = alErrInvalidOp;
+        break;
+
+    case AL_OUT_OF_MEMORY:
+        value = alErrOutOfMemory;
+        break;
+
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid string property 0x%04x", pname);
+    }
+    return value;
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDopplerFactor(ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!(value >= 0.0f && std::isfinite(value)))
+        context->setError(AL_INVALID_VALUE, "Doppler factor %f out of range", value);
+    else
+    {
+        std::lock_guard<std::mutex> _{context->mPropLock};
+        context->mDopplerFactor = value;
+        DO_UPDATEPROPS();
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDopplerVelocity(ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!(value >= 0.0f && std::isfinite(value)))
+        context->setError(AL_INVALID_VALUE, "Doppler velocity %f out of range", value);
+    else
+    {
+        std::lock_guard<std::mutex> _{context->mPropLock};
+        context->mDopplerVelocity = value;
+        DO_UPDATEPROPS();
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alSpeedOfSound(ALfloat value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(!(value > 0.0f && std::isfinite(value)))
+        context->setError(AL_INVALID_VALUE, "Speed of sound %f out of range", value);
+    else
+    {
+        std::lock_guard<std::mutex> _{context->mPropLock};
+        context->mSpeedOfSound = value;
+        DO_UPDATEPROPS();
+    }
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alDistanceModel(ALenum value)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    if(auto model = DistanceModelFromALenum(value))
+    {
+        std::lock_guard<std::mutex> _{context->mPropLock};
+        context->mDistanceModel = *model;
+        if(!context->mSourceDistanceModel)
+            DO_UPDATEPROPS();
+    }
+    else
+        context->setError(AL_INVALID_VALUE, "Distance model 0x%04x out of range", value);
+}
+END_API_FUNC
+
+
+AL_API void AL_APIENTRY alDeferUpdatesSOFT(void)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    context->deferUpdates();
+}
+END_API_FUNC
+
+AL_API void AL_APIENTRY alProcessUpdatesSOFT(void)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return;
+
+    std::lock_guard<std::mutex> _{context->mPropLock};
+    context->processUpdates();
+}
+END_API_FUNC
+
+
+AL_API const ALchar* AL_APIENTRY alGetStringiSOFT(ALenum pname, ALsizei index)
+START_API_FUNC
+{
+    ContextRef context{GetContextRef()};
+    if(!context) UNLIKELY return nullptr;
+
+    const ALchar *value{nullptr};
+    switch(pname)
+    {
+    case AL_RESAMPLER_NAME_SOFT:
+        if(index < 0 || index > static_cast<ALint>(Resampler::Max))
+            context->setError(AL_INVALID_VALUE, "Resampler name index %d out of range", index);
+        else
+            value = GetResamplerName(static_cast<Resampler>(index));
+        break;
+
+    default:
+        context->setError(AL_INVALID_VALUE, "Invalid string indexed property");
+    }
+    return value;
+}
+END_API_FUNC
+
+
+void UpdateContextProps(ALCcontext *context)
+{
+    /* Get an unused proprty container, or allocate a new one as needed. */
+    ContextProps *props{context->mFreeContextProps.load(std::memory_order_acquire)};
+    if(!props)
+        props = new ContextProps{};
+    else
+    {
+        ContextProps *next;
+        do {
+            next = props->next.load(std::memory_order_relaxed);
+        } while(context->mFreeContextProps.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;
+
+    props->AirAbsorptionGainHF = context->mAirAbsorptionGainHF;
+    props->DopplerFactor = context->mDopplerFactor;
+    props->DopplerVelocity = context->mDopplerVelocity;
+    props->SpeedOfSound = context->mSpeedOfSound;
+
+    props->SourceDistanceModel = context->mSourceDistanceModel;
+    props->mDistanceModel = context->mDistanceModel;
+
+    /* Set the new container for updating internal parameters. */
+    props = context->mParams.ContextUpdate.exchange(props, std::memory_order_acq_rel);
+    if(props)
+    {
+        /* If there was an unused update container, put it back in the
+         * freelist.
+         */
+        AtomicReplaceHead(context->mFreeContextProps, props);
+    }
+}
diff --git a/alc/alc.cpp b/alc/alc.cpp
new file mode 100644 (file)
index 0000000..af8ff55
--- /dev/null
@@ -0,0 +1,4125 @@
+/**
+ * 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 "version.h"
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <bitset>
+#include <cassert>
+#include <cctype>
+#include <chrono>
+#include <cinttypes>
+#include <climits>
+#include <cmath>
+#include <csignal>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <functional>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <mutex>
+#include <new>
+#include <stddef.h>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <utility>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+#include "AL/efx.h"
+
+#include "al/auxeffectslot.h"
+#include "al/buffer.h"
+#include "al/effect.h"
+#include "al/filter.h"
+#include "al/listener.h"
+#include "al/source.h"
+#include "albit.h"
+#include "albyte.h"
+#include "alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "alu.h"
+#include "atomic.h"
+#include "context.h"
+#include "core/ambidefs.h"
+#include "core/bformatdec.h"
+#include "core/bs2b.h"
+#include "core/context.h"
+#include "core/cpu_caps.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/except.h"
+#include "core/helpers.h"
+#include "core/mastering.h"
+#include "core/mixer/hrtfdefs.h"
+#include "core/fpu_ctrl.h"
+#include "core/front_stablizer.h"
+#include "core/logging.h"
+#include "core/uhjfilter.h"
+#include "core/voice.h"
+#include "core/voice_change.h"
+#include "device.h"
+#include "effects/base.h"
+#include "inprogext.h"
+#include "intrusive_ptr.h"
+#include "opthelpers.h"
+#include "strutils.h"
+#include "threads.h"
+#include "vector.h"
+
+#include "backends/base.h"
+#include "backends/null.h"
+#include "backends/loopback.h"
+#ifdef HAVE_PIPEWIRE
+#include "backends/pipewire.h"
+#endif
+#ifdef HAVE_JACK
+#include "backends/jack.h"
+#endif
+#ifdef HAVE_PULSEAUDIO
+#include "backends/pulseaudio.h"
+#endif
+#ifdef HAVE_ALSA
+#include "backends/alsa.h"
+#endif
+#ifdef HAVE_WASAPI
+#include "backends/wasapi.h"
+#endif
+#ifdef HAVE_COREAUDIO
+#include "backends/coreaudio.h"
+#endif
+#ifdef HAVE_OPENSL
+#include "backends/opensl.h"
+#endif
+#ifdef HAVE_OBOE
+#include "backends/oboe.h"
+#endif
+#ifdef HAVE_SOLARIS
+#include "backends/solaris.h"
+#endif
+#ifdef HAVE_SNDIO
+#include "backends/sndio.h"
+#endif
+#ifdef HAVE_OSS
+#include "backends/oss.h"
+#endif
+#ifdef HAVE_DSOUND
+#include "backends/dsound.h"
+#endif
+#ifdef HAVE_WINMM
+#include "backends/winmm.h"
+#endif
+#ifdef HAVE_PORTAUDIO
+#include "backends/portaudio.h"
+#endif
+#ifdef HAVE_SDL2
+#include "backends/sdl2.h"
+#endif
+#ifdef HAVE_WAVE
+#include "backends/wave.h"
+#endif
+
+#ifdef ALSOFT_EAX
+#include "al/eax/globals.h"
+#include "al/eax/x_ram.h"
+#endif // ALSOFT_EAX
+
+
+FILE *gLogFile{stderr};
+#ifdef _DEBUG
+LogLevel gLogLevel{LogLevel::Warning};
+#else
+LogLevel gLogLevel{LogLevel::Error};
+#endif
+
+/************************************************
+ * Library initialization
+ ************************************************/
+#if defined(_WIN32) && !defined(AL_LIBTYPE_STATIC)
+BOOL APIENTRY DllMain(HINSTANCE module, DWORD reason, LPVOID /*reserved*/)
+{
+    switch(reason)
+    {
+    case DLL_PROCESS_ATTACH:
+        /* Pin the DLL so we won't get unloaded until the process terminates */
+        GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
+            reinterpret_cast<WCHAR*>(module), &module);
+        break;
+    }
+    return TRUE;
+}
+#endif
+
+namespace {
+
+using namespace std::placeholders;
+using std::chrono::seconds;
+using std::chrono::nanoseconds;
+
+using voidp = void*;
+using float2 = std::array<float,2>;
+
+
+/************************************************
+ * Backends
+ ************************************************/
+struct BackendInfo {
+    const char *name;
+    BackendFactory& (*getFactory)(void);
+};
+
+BackendInfo BackendList[] = {
+#ifdef HAVE_PIPEWIRE
+    { "pipewire", PipeWireBackendFactory::getFactory },
+#endif
+#ifdef HAVE_PULSEAUDIO
+    { "pulse", PulseBackendFactory::getFactory },
+#endif
+#ifdef HAVE_WASAPI
+    { "wasapi", WasapiBackendFactory::getFactory },
+#endif
+#ifdef HAVE_COREAUDIO
+    { "core", CoreAudioBackendFactory::getFactory },
+#endif
+#ifdef HAVE_OBOE
+    { "oboe", OboeBackendFactory::getFactory },
+#endif
+#ifdef HAVE_OPENSL
+    { "opensl", OSLBackendFactory::getFactory },
+#endif
+#ifdef HAVE_ALSA
+    { "alsa", AlsaBackendFactory::getFactory },
+#endif
+#ifdef HAVE_SOLARIS
+    { "solaris", SolarisBackendFactory::getFactory },
+#endif
+#ifdef HAVE_SNDIO
+    { "sndio", SndIOBackendFactory::getFactory },
+#endif
+#ifdef HAVE_OSS
+    { "oss", OSSBackendFactory::getFactory },
+#endif
+#ifdef HAVE_JACK
+    { "jack", JackBackendFactory::getFactory },
+#endif
+#ifdef HAVE_DSOUND
+    { "dsound", DSoundBackendFactory::getFactory },
+#endif
+#ifdef HAVE_WINMM
+    { "winmm", WinMMBackendFactory::getFactory },
+#endif
+#ifdef HAVE_PORTAUDIO
+    { "port", PortBackendFactory::getFactory },
+#endif
+#ifdef HAVE_SDL2
+    { "sdl2", SDL2BackendFactory::getFactory },
+#endif
+
+    { "null", NullBackendFactory::getFactory },
+#ifdef HAVE_WAVE
+    { "wave", WaveBackendFactory::getFactory },
+#endif
+};
+
+BackendFactory *PlaybackFactory{};
+BackendFactory *CaptureFactory{};
+
+
+/************************************************
+ * Functions, enums, and errors
+ ************************************************/
+#define DECL(x) { #x, reinterpret_cast<void*>(x) }
+const struct {
+    const char *funcName;
+    void *address;
+} alcFunctions[] = {
+    DECL(alcCreateContext),
+    DECL(alcMakeContextCurrent),
+    DECL(alcProcessContext),
+    DECL(alcSuspendContext),
+    DECL(alcDestroyContext),
+    DECL(alcGetCurrentContext),
+    DECL(alcGetContextsDevice),
+    DECL(alcOpenDevice),
+    DECL(alcCloseDevice),
+    DECL(alcGetError),
+    DECL(alcIsExtensionPresent),
+    DECL(alcGetProcAddress),
+    DECL(alcGetEnumValue),
+    DECL(alcGetString),
+    DECL(alcGetIntegerv),
+    DECL(alcCaptureOpenDevice),
+    DECL(alcCaptureCloseDevice),
+    DECL(alcCaptureStart),
+    DECL(alcCaptureStop),
+    DECL(alcCaptureSamples),
+
+    DECL(alcSetThreadContext),
+    DECL(alcGetThreadContext),
+
+    DECL(alcLoopbackOpenDeviceSOFT),
+    DECL(alcIsRenderFormatSupportedSOFT),
+    DECL(alcRenderSamplesSOFT),
+
+    DECL(alcDevicePauseSOFT),
+    DECL(alcDeviceResumeSOFT),
+
+    DECL(alcGetStringiSOFT),
+    DECL(alcResetDeviceSOFT),
+
+    DECL(alcGetInteger64vSOFT),
+
+    DECL(alcReopenDeviceSOFT),
+
+    DECL(alEnable),
+    DECL(alDisable),
+    DECL(alIsEnabled),
+    DECL(alGetString),
+    DECL(alGetBooleanv),
+    DECL(alGetIntegerv),
+    DECL(alGetFloatv),
+    DECL(alGetDoublev),
+    DECL(alGetBoolean),
+    DECL(alGetInteger),
+    DECL(alGetFloat),
+    DECL(alGetDouble),
+    DECL(alGetError),
+    DECL(alIsExtensionPresent),
+    DECL(alGetProcAddress),
+    DECL(alGetEnumValue),
+    DECL(alListenerf),
+    DECL(alListener3f),
+    DECL(alListenerfv),
+    DECL(alListeneri),
+    DECL(alListener3i),
+    DECL(alListeneriv),
+    DECL(alGetListenerf),
+    DECL(alGetListener3f),
+    DECL(alGetListenerfv),
+    DECL(alGetListeneri),
+    DECL(alGetListener3i),
+    DECL(alGetListeneriv),
+    DECL(alGenSources),
+    DECL(alDeleteSources),
+    DECL(alIsSource),
+    DECL(alSourcef),
+    DECL(alSource3f),
+    DECL(alSourcefv),
+    DECL(alSourcei),
+    DECL(alSource3i),
+    DECL(alSourceiv),
+    DECL(alGetSourcef),
+    DECL(alGetSource3f),
+    DECL(alGetSourcefv),
+    DECL(alGetSourcei),
+    DECL(alGetSource3i),
+    DECL(alGetSourceiv),
+    DECL(alSourcePlayv),
+    DECL(alSourceStopv),
+    DECL(alSourceRewindv),
+    DECL(alSourcePausev),
+    DECL(alSourcePlay),
+    DECL(alSourceStop),
+    DECL(alSourceRewind),
+    DECL(alSourcePause),
+    DECL(alSourceQueueBuffers),
+    DECL(alSourceUnqueueBuffers),
+    DECL(alGenBuffers),
+    DECL(alDeleteBuffers),
+    DECL(alIsBuffer),
+    DECL(alBufferData),
+    DECL(alBufferf),
+    DECL(alBuffer3f),
+    DECL(alBufferfv),
+    DECL(alBufferi),
+    DECL(alBuffer3i),
+    DECL(alBufferiv),
+    DECL(alGetBufferf),
+    DECL(alGetBuffer3f),
+    DECL(alGetBufferfv),
+    DECL(alGetBufferi),
+    DECL(alGetBuffer3i),
+    DECL(alGetBufferiv),
+    DECL(alDopplerFactor),
+    DECL(alDopplerVelocity),
+    DECL(alSpeedOfSound),
+    DECL(alDistanceModel),
+
+    DECL(alGenFilters),
+    DECL(alDeleteFilters),
+    DECL(alIsFilter),
+    DECL(alFilteri),
+    DECL(alFilteriv),
+    DECL(alFilterf),
+    DECL(alFilterfv),
+    DECL(alGetFilteri),
+    DECL(alGetFilteriv),
+    DECL(alGetFilterf),
+    DECL(alGetFilterfv),
+    DECL(alGenEffects),
+    DECL(alDeleteEffects),
+    DECL(alIsEffect),
+    DECL(alEffecti),
+    DECL(alEffectiv),
+    DECL(alEffectf),
+    DECL(alEffectfv),
+    DECL(alGetEffecti),
+    DECL(alGetEffectiv),
+    DECL(alGetEffectf),
+    DECL(alGetEffectfv),
+    DECL(alGenAuxiliaryEffectSlots),
+    DECL(alDeleteAuxiliaryEffectSlots),
+    DECL(alIsAuxiliaryEffectSlot),
+    DECL(alAuxiliaryEffectSloti),
+    DECL(alAuxiliaryEffectSlotiv),
+    DECL(alAuxiliaryEffectSlotf),
+    DECL(alAuxiliaryEffectSlotfv),
+    DECL(alGetAuxiliaryEffectSloti),
+    DECL(alGetAuxiliaryEffectSlotiv),
+    DECL(alGetAuxiliaryEffectSlotf),
+    DECL(alGetAuxiliaryEffectSlotfv),
+
+    DECL(alDeferUpdatesSOFT),
+    DECL(alProcessUpdatesSOFT),
+
+    DECL(alSourcedSOFT),
+    DECL(alSource3dSOFT),
+    DECL(alSourcedvSOFT),
+    DECL(alGetSourcedSOFT),
+    DECL(alGetSource3dSOFT),
+    DECL(alGetSourcedvSOFT),
+    DECL(alSourcei64SOFT),
+    DECL(alSource3i64SOFT),
+    DECL(alSourcei64vSOFT),
+    DECL(alGetSourcei64SOFT),
+    DECL(alGetSource3i64SOFT),
+    DECL(alGetSourcei64vSOFT),
+
+    DECL(alGetStringiSOFT),
+
+    DECL(alBufferStorageSOFT),
+    DECL(alMapBufferSOFT),
+    DECL(alUnmapBufferSOFT),
+    DECL(alFlushMappedBufferSOFT),
+
+    DECL(alEventControlSOFT),
+    DECL(alEventCallbackSOFT),
+    DECL(alGetPointerSOFT),
+    DECL(alGetPointervSOFT),
+
+    DECL(alBufferCallbackSOFT),
+    DECL(alGetBufferPtrSOFT),
+    DECL(alGetBuffer3PtrSOFT),
+    DECL(alGetBufferPtrvSOFT),
+
+    DECL(alAuxiliaryEffectSlotPlaySOFT),
+    DECL(alAuxiliaryEffectSlotPlayvSOFT),
+    DECL(alAuxiliaryEffectSlotStopSOFT),
+    DECL(alAuxiliaryEffectSlotStopvSOFT),
+
+    DECL(alSourcePlayAtTimeSOFT),
+    DECL(alSourcePlayAtTimevSOFT),
+
+    DECL(alBufferSubDataSOFT),
+
+    DECL(alBufferDataStatic),
+#ifdef ALSOFT_EAX
+}, eaxFunctions[] = {
+    DECL(EAXGet),
+    DECL(EAXSet),
+    DECL(EAXGetBufferMode),
+    DECL(EAXSetBufferMode),
+#endif
+};
+#undef DECL
+
+#define DECL(x) { #x, (x) }
+constexpr struct {
+    const ALCchar *enumName;
+    ALCenum value;
+} alcEnumerations[] = {
+    DECL(ALC_INVALID),
+    DECL(ALC_FALSE),
+    DECL(ALC_TRUE),
+
+    DECL(ALC_MAJOR_VERSION),
+    DECL(ALC_MINOR_VERSION),
+    DECL(ALC_ATTRIBUTES_SIZE),
+    DECL(ALC_ALL_ATTRIBUTES),
+    DECL(ALC_DEFAULT_DEVICE_SPECIFIER),
+    DECL(ALC_DEVICE_SPECIFIER),
+    DECL(ALC_ALL_DEVICES_SPECIFIER),
+    DECL(ALC_DEFAULT_ALL_DEVICES_SPECIFIER),
+    DECL(ALC_EXTENSIONS),
+    DECL(ALC_FREQUENCY),
+    DECL(ALC_REFRESH),
+    DECL(ALC_SYNC),
+    DECL(ALC_MONO_SOURCES),
+    DECL(ALC_STEREO_SOURCES),
+    DECL(ALC_CAPTURE_DEVICE_SPECIFIER),
+    DECL(ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER),
+    DECL(ALC_CAPTURE_SAMPLES),
+    DECL(ALC_CONNECTED),
+
+    DECL(ALC_EFX_MAJOR_VERSION),
+    DECL(ALC_EFX_MINOR_VERSION),
+    DECL(ALC_MAX_AUXILIARY_SENDS),
+
+    DECL(ALC_FORMAT_CHANNELS_SOFT),
+    DECL(ALC_FORMAT_TYPE_SOFT),
+
+    DECL(ALC_MONO_SOFT),
+    DECL(ALC_STEREO_SOFT),
+    DECL(ALC_QUAD_SOFT),
+    DECL(ALC_5POINT1_SOFT),
+    DECL(ALC_6POINT1_SOFT),
+    DECL(ALC_7POINT1_SOFT),
+    DECL(ALC_BFORMAT3D_SOFT),
+
+    DECL(ALC_BYTE_SOFT),
+    DECL(ALC_UNSIGNED_BYTE_SOFT),
+    DECL(ALC_SHORT_SOFT),
+    DECL(ALC_UNSIGNED_SHORT_SOFT),
+    DECL(ALC_INT_SOFT),
+    DECL(ALC_UNSIGNED_INT_SOFT),
+    DECL(ALC_FLOAT_SOFT),
+
+    DECL(ALC_HRTF_SOFT),
+    DECL(ALC_DONT_CARE_SOFT),
+    DECL(ALC_HRTF_STATUS_SOFT),
+    DECL(ALC_HRTF_DISABLED_SOFT),
+    DECL(ALC_HRTF_ENABLED_SOFT),
+    DECL(ALC_HRTF_DENIED_SOFT),
+    DECL(ALC_HRTF_REQUIRED_SOFT),
+    DECL(ALC_HRTF_HEADPHONES_DETECTED_SOFT),
+    DECL(ALC_HRTF_UNSUPPORTED_FORMAT_SOFT),
+    DECL(ALC_NUM_HRTF_SPECIFIERS_SOFT),
+    DECL(ALC_HRTF_SPECIFIER_SOFT),
+    DECL(ALC_HRTF_ID_SOFT),
+
+    DECL(ALC_AMBISONIC_LAYOUT_SOFT),
+    DECL(ALC_AMBISONIC_SCALING_SOFT),
+    DECL(ALC_AMBISONIC_ORDER_SOFT),
+    DECL(ALC_ACN_SOFT),
+    DECL(ALC_FUMA_SOFT),
+    DECL(ALC_N3D_SOFT),
+    DECL(ALC_SN3D_SOFT),
+
+    DECL(ALC_OUTPUT_LIMITER_SOFT),
+
+    DECL(ALC_DEVICE_CLOCK_SOFT),
+    DECL(ALC_DEVICE_LATENCY_SOFT),
+    DECL(ALC_DEVICE_CLOCK_LATENCY_SOFT),
+    DECL(AL_SAMPLE_OFFSET_CLOCK_SOFT),
+    DECL(AL_SEC_OFFSET_CLOCK_SOFT),
+
+    DECL(ALC_OUTPUT_MODE_SOFT),
+    DECL(ALC_ANY_SOFT),
+    DECL(ALC_STEREO_BASIC_SOFT),
+    DECL(ALC_STEREO_UHJ_SOFT),
+    DECL(ALC_STEREO_HRTF_SOFT),
+    DECL(ALC_SURROUND_5_1_SOFT),
+    DECL(ALC_SURROUND_6_1_SOFT),
+    DECL(ALC_SURROUND_7_1_SOFT),
+
+    DECL(ALC_NO_ERROR),
+    DECL(ALC_INVALID_DEVICE),
+    DECL(ALC_INVALID_CONTEXT),
+    DECL(ALC_INVALID_ENUM),
+    DECL(ALC_INVALID_VALUE),
+    DECL(ALC_OUT_OF_MEMORY),
+
+
+    DECL(AL_INVALID),
+    DECL(AL_NONE),
+    DECL(AL_FALSE),
+    DECL(AL_TRUE),
+
+    DECL(AL_SOURCE_RELATIVE),
+    DECL(AL_CONE_INNER_ANGLE),
+    DECL(AL_CONE_OUTER_ANGLE),
+    DECL(AL_PITCH),
+    DECL(AL_POSITION),
+    DECL(AL_DIRECTION),
+    DECL(AL_VELOCITY),
+    DECL(AL_LOOPING),
+    DECL(AL_BUFFER),
+    DECL(AL_GAIN),
+    DECL(AL_MIN_GAIN),
+    DECL(AL_MAX_GAIN),
+    DECL(AL_ORIENTATION),
+    DECL(AL_REFERENCE_DISTANCE),
+    DECL(AL_ROLLOFF_FACTOR),
+    DECL(AL_CONE_OUTER_GAIN),
+    DECL(AL_MAX_DISTANCE),
+    DECL(AL_SEC_OFFSET),
+    DECL(AL_SAMPLE_OFFSET),
+    DECL(AL_BYTE_OFFSET),
+    DECL(AL_SOURCE_TYPE),
+    DECL(AL_STATIC),
+    DECL(AL_STREAMING),
+    DECL(AL_UNDETERMINED),
+    DECL(AL_METERS_PER_UNIT),
+    DECL(AL_LOOP_POINTS_SOFT),
+    DECL(AL_DIRECT_CHANNELS_SOFT),
+
+    DECL(AL_DIRECT_FILTER),
+    DECL(AL_AUXILIARY_SEND_FILTER),
+    DECL(AL_AIR_ABSORPTION_FACTOR),
+    DECL(AL_ROOM_ROLLOFF_FACTOR),
+    DECL(AL_CONE_OUTER_GAINHF),
+    DECL(AL_DIRECT_FILTER_GAINHF_AUTO),
+    DECL(AL_AUXILIARY_SEND_FILTER_GAIN_AUTO),
+    DECL(AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO),
+
+    DECL(AL_SOURCE_STATE),
+    DECL(AL_INITIAL),
+    DECL(AL_PLAYING),
+    DECL(AL_PAUSED),
+    DECL(AL_STOPPED),
+
+    DECL(AL_BUFFERS_QUEUED),
+    DECL(AL_BUFFERS_PROCESSED),
+
+    DECL(AL_FORMAT_MONO8),
+    DECL(AL_FORMAT_MONO16),
+    DECL(AL_FORMAT_MONO_FLOAT32),
+    DECL(AL_FORMAT_MONO_DOUBLE_EXT),
+    DECL(AL_FORMAT_STEREO8),
+    DECL(AL_FORMAT_STEREO16),
+    DECL(AL_FORMAT_STEREO_FLOAT32),
+    DECL(AL_FORMAT_STEREO_DOUBLE_EXT),
+    DECL(AL_FORMAT_MONO_IMA4),
+    DECL(AL_FORMAT_STEREO_IMA4),
+    DECL(AL_FORMAT_MONO_MSADPCM_SOFT),
+    DECL(AL_FORMAT_STEREO_MSADPCM_SOFT),
+    DECL(AL_FORMAT_QUAD8_LOKI),
+    DECL(AL_FORMAT_QUAD16_LOKI),
+    DECL(AL_FORMAT_QUAD8),
+    DECL(AL_FORMAT_QUAD16),
+    DECL(AL_FORMAT_QUAD32),
+    DECL(AL_FORMAT_51CHN8),
+    DECL(AL_FORMAT_51CHN16),
+    DECL(AL_FORMAT_51CHN32),
+    DECL(AL_FORMAT_61CHN8),
+    DECL(AL_FORMAT_61CHN16),
+    DECL(AL_FORMAT_61CHN32),
+    DECL(AL_FORMAT_71CHN8),
+    DECL(AL_FORMAT_71CHN16),
+    DECL(AL_FORMAT_71CHN32),
+    DECL(AL_FORMAT_REAR8),
+    DECL(AL_FORMAT_REAR16),
+    DECL(AL_FORMAT_REAR32),
+    DECL(AL_FORMAT_MONO_MULAW),
+    DECL(AL_FORMAT_MONO_MULAW_EXT),
+    DECL(AL_FORMAT_STEREO_MULAW),
+    DECL(AL_FORMAT_STEREO_MULAW_EXT),
+    DECL(AL_FORMAT_QUAD_MULAW),
+    DECL(AL_FORMAT_51CHN_MULAW),
+    DECL(AL_FORMAT_61CHN_MULAW),
+    DECL(AL_FORMAT_71CHN_MULAW),
+    DECL(AL_FORMAT_REAR_MULAW),
+    DECL(AL_FORMAT_MONO_ALAW_EXT),
+    DECL(AL_FORMAT_STEREO_ALAW_EXT),
+
+    DECL(AL_FORMAT_BFORMAT2D_8),
+    DECL(AL_FORMAT_BFORMAT2D_16),
+    DECL(AL_FORMAT_BFORMAT2D_FLOAT32),
+    DECL(AL_FORMAT_BFORMAT2D_MULAW),
+    DECL(AL_FORMAT_BFORMAT3D_8),
+    DECL(AL_FORMAT_BFORMAT3D_16),
+    DECL(AL_FORMAT_BFORMAT3D_FLOAT32),
+    DECL(AL_FORMAT_BFORMAT3D_MULAW),
+
+    DECL(AL_FREQUENCY),
+    DECL(AL_BITS),
+    DECL(AL_CHANNELS),
+    DECL(AL_SIZE),
+    DECL(AL_UNPACK_BLOCK_ALIGNMENT_SOFT),
+    DECL(AL_PACK_BLOCK_ALIGNMENT_SOFT),
+
+    DECL(AL_SOURCE_RADIUS),
+
+    DECL(AL_SAMPLE_OFFSET_LATENCY_SOFT),
+    DECL(AL_SEC_OFFSET_LATENCY_SOFT),
+
+    DECL(AL_STEREO_ANGLES),
+
+    DECL(AL_UNUSED),
+    DECL(AL_PENDING),
+    DECL(AL_PROCESSED),
+
+    DECL(AL_NO_ERROR),
+    DECL(AL_INVALID_NAME),
+    DECL(AL_INVALID_ENUM),
+    DECL(AL_INVALID_VALUE),
+    DECL(AL_INVALID_OPERATION),
+    DECL(AL_OUT_OF_MEMORY),
+
+    DECL(AL_VENDOR),
+    DECL(AL_VERSION),
+    DECL(AL_RENDERER),
+    DECL(AL_EXTENSIONS),
+
+    DECL(AL_DOPPLER_FACTOR),
+    DECL(AL_DOPPLER_VELOCITY),
+    DECL(AL_DISTANCE_MODEL),
+    DECL(AL_SPEED_OF_SOUND),
+    DECL(AL_SOURCE_DISTANCE_MODEL),
+    DECL(AL_DEFERRED_UPDATES_SOFT),
+    DECL(AL_GAIN_LIMIT_SOFT),
+
+    DECL(AL_INVERSE_DISTANCE),
+    DECL(AL_INVERSE_DISTANCE_CLAMPED),
+    DECL(AL_LINEAR_DISTANCE),
+    DECL(AL_LINEAR_DISTANCE_CLAMPED),
+    DECL(AL_EXPONENT_DISTANCE),
+    DECL(AL_EXPONENT_DISTANCE_CLAMPED),
+
+    DECL(AL_FILTER_TYPE),
+    DECL(AL_FILTER_NULL),
+    DECL(AL_FILTER_LOWPASS),
+    DECL(AL_FILTER_HIGHPASS),
+    DECL(AL_FILTER_BANDPASS),
+
+    DECL(AL_LOWPASS_GAIN),
+    DECL(AL_LOWPASS_GAINHF),
+
+    DECL(AL_HIGHPASS_GAIN),
+    DECL(AL_HIGHPASS_GAINLF),
+
+    DECL(AL_BANDPASS_GAIN),
+    DECL(AL_BANDPASS_GAINHF),
+    DECL(AL_BANDPASS_GAINLF),
+
+    DECL(AL_EFFECT_TYPE),
+    DECL(AL_EFFECT_NULL),
+    DECL(AL_EFFECT_REVERB),
+    DECL(AL_EFFECT_EAXREVERB),
+    DECL(AL_EFFECT_CHORUS),
+    DECL(AL_EFFECT_DISTORTION),
+    DECL(AL_EFFECT_ECHO),
+    DECL(AL_EFFECT_FLANGER),
+    DECL(AL_EFFECT_PITCH_SHIFTER),
+    DECL(AL_EFFECT_FREQUENCY_SHIFTER),
+    DECL(AL_EFFECT_VOCAL_MORPHER),
+    DECL(AL_EFFECT_RING_MODULATOR),
+    DECL(AL_EFFECT_AUTOWAH),
+    DECL(AL_EFFECT_COMPRESSOR),
+    DECL(AL_EFFECT_EQUALIZER),
+    DECL(AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT),
+    DECL(AL_EFFECT_DEDICATED_DIALOGUE),
+
+    DECL(AL_EFFECTSLOT_EFFECT),
+    DECL(AL_EFFECTSLOT_GAIN),
+    DECL(AL_EFFECTSLOT_AUXILIARY_SEND_AUTO),
+    DECL(AL_EFFECTSLOT_NULL),
+
+    DECL(AL_EAXREVERB_DENSITY),
+    DECL(AL_EAXREVERB_DIFFUSION),
+    DECL(AL_EAXREVERB_GAIN),
+    DECL(AL_EAXREVERB_GAINHF),
+    DECL(AL_EAXREVERB_GAINLF),
+    DECL(AL_EAXREVERB_DECAY_TIME),
+    DECL(AL_EAXREVERB_DECAY_HFRATIO),
+    DECL(AL_EAXREVERB_DECAY_LFRATIO),
+    DECL(AL_EAXREVERB_REFLECTIONS_GAIN),
+    DECL(AL_EAXREVERB_REFLECTIONS_DELAY),
+    DECL(AL_EAXREVERB_REFLECTIONS_PAN),
+    DECL(AL_EAXREVERB_LATE_REVERB_GAIN),
+    DECL(AL_EAXREVERB_LATE_REVERB_DELAY),
+    DECL(AL_EAXREVERB_LATE_REVERB_PAN),
+    DECL(AL_EAXREVERB_ECHO_TIME),
+    DECL(AL_EAXREVERB_ECHO_DEPTH),
+    DECL(AL_EAXREVERB_MODULATION_TIME),
+    DECL(AL_EAXREVERB_MODULATION_DEPTH),
+    DECL(AL_EAXREVERB_AIR_ABSORPTION_GAINHF),
+    DECL(AL_EAXREVERB_HFREFERENCE),
+    DECL(AL_EAXREVERB_LFREFERENCE),
+    DECL(AL_EAXREVERB_ROOM_ROLLOFF_FACTOR),
+    DECL(AL_EAXREVERB_DECAY_HFLIMIT),
+
+    DECL(AL_REVERB_DENSITY),
+    DECL(AL_REVERB_DIFFUSION),
+    DECL(AL_REVERB_GAIN),
+    DECL(AL_REVERB_GAINHF),
+    DECL(AL_REVERB_DECAY_TIME),
+    DECL(AL_REVERB_DECAY_HFRATIO),
+    DECL(AL_REVERB_REFLECTIONS_GAIN),
+    DECL(AL_REVERB_REFLECTIONS_DELAY),
+    DECL(AL_REVERB_LATE_REVERB_GAIN),
+    DECL(AL_REVERB_LATE_REVERB_DELAY),
+    DECL(AL_REVERB_AIR_ABSORPTION_GAINHF),
+    DECL(AL_REVERB_ROOM_ROLLOFF_FACTOR),
+    DECL(AL_REVERB_DECAY_HFLIMIT),
+
+    DECL(AL_CHORUS_WAVEFORM),
+    DECL(AL_CHORUS_PHASE),
+    DECL(AL_CHORUS_RATE),
+    DECL(AL_CHORUS_DEPTH),
+    DECL(AL_CHORUS_FEEDBACK),
+    DECL(AL_CHORUS_DELAY),
+
+    DECL(AL_DISTORTION_EDGE),
+    DECL(AL_DISTORTION_GAIN),
+    DECL(AL_DISTORTION_LOWPASS_CUTOFF),
+    DECL(AL_DISTORTION_EQCENTER),
+    DECL(AL_DISTORTION_EQBANDWIDTH),
+
+    DECL(AL_ECHO_DELAY),
+    DECL(AL_ECHO_LRDELAY),
+    DECL(AL_ECHO_DAMPING),
+    DECL(AL_ECHO_FEEDBACK),
+    DECL(AL_ECHO_SPREAD),
+
+    DECL(AL_FLANGER_WAVEFORM),
+    DECL(AL_FLANGER_PHASE),
+    DECL(AL_FLANGER_RATE),
+    DECL(AL_FLANGER_DEPTH),
+    DECL(AL_FLANGER_FEEDBACK),
+    DECL(AL_FLANGER_DELAY),
+
+    DECL(AL_FREQUENCY_SHIFTER_FREQUENCY),
+    DECL(AL_FREQUENCY_SHIFTER_LEFT_DIRECTION),
+    DECL(AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION),
+
+    DECL(AL_RING_MODULATOR_FREQUENCY),
+    DECL(AL_RING_MODULATOR_HIGHPASS_CUTOFF),
+    DECL(AL_RING_MODULATOR_WAVEFORM),
+
+    DECL(AL_PITCH_SHIFTER_COARSE_TUNE),
+    DECL(AL_PITCH_SHIFTER_FINE_TUNE),
+
+    DECL(AL_COMPRESSOR_ONOFF),
+
+    DECL(AL_EQUALIZER_LOW_GAIN),
+    DECL(AL_EQUALIZER_LOW_CUTOFF),
+    DECL(AL_EQUALIZER_MID1_GAIN),
+    DECL(AL_EQUALIZER_MID1_CENTER),
+    DECL(AL_EQUALIZER_MID1_WIDTH),
+    DECL(AL_EQUALIZER_MID2_GAIN),
+    DECL(AL_EQUALIZER_MID2_CENTER),
+    DECL(AL_EQUALIZER_MID2_WIDTH),
+    DECL(AL_EQUALIZER_HIGH_GAIN),
+    DECL(AL_EQUALIZER_HIGH_CUTOFF),
+
+    DECL(AL_DEDICATED_GAIN),
+
+    DECL(AL_AUTOWAH_ATTACK_TIME),
+    DECL(AL_AUTOWAH_RELEASE_TIME),
+    DECL(AL_AUTOWAH_RESONANCE),
+    DECL(AL_AUTOWAH_PEAK_GAIN),
+
+    DECL(AL_VOCAL_MORPHER_PHONEMEA),
+    DECL(AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING),
+    DECL(AL_VOCAL_MORPHER_PHONEMEB),
+    DECL(AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING),
+    DECL(AL_VOCAL_MORPHER_WAVEFORM),
+    DECL(AL_VOCAL_MORPHER_RATE),
+
+    DECL(AL_EFFECTSLOT_TARGET_SOFT),
+
+    DECL(AL_NUM_RESAMPLERS_SOFT),
+    DECL(AL_DEFAULT_RESAMPLER_SOFT),
+    DECL(AL_SOURCE_RESAMPLER_SOFT),
+    DECL(AL_RESAMPLER_NAME_SOFT),
+
+    DECL(AL_SOURCE_SPATIALIZE_SOFT),
+    DECL(AL_AUTO_SOFT),
+
+    DECL(AL_MAP_READ_BIT_SOFT),
+    DECL(AL_MAP_WRITE_BIT_SOFT),
+    DECL(AL_MAP_PERSISTENT_BIT_SOFT),
+    DECL(AL_PRESERVE_DATA_BIT_SOFT),
+
+    DECL(AL_EVENT_CALLBACK_FUNCTION_SOFT),
+    DECL(AL_EVENT_CALLBACK_USER_PARAM_SOFT),
+    DECL(AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT),
+    DECL(AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT),
+    DECL(AL_EVENT_TYPE_DISCONNECTED_SOFT),
+
+    DECL(AL_DROP_UNMATCHED_SOFT),
+    DECL(AL_REMIX_UNMATCHED_SOFT),
+
+    DECL(AL_AMBISONIC_LAYOUT_SOFT),
+    DECL(AL_AMBISONIC_SCALING_SOFT),
+    DECL(AL_FUMA_SOFT),
+    DECL(AL_ACN_SOFT),
+    DECL(AL_SN3D_SOFT),
+    DECL(AL_N3D_SOFT),
+
+    DECL(AL_BUFFER_CALLBACK_FUNCTION_SOFT),
+    DECL(AL_BUFFER_CALLBACK_USER_PARAM_SOFT),
+
+    DECL(AL_UNPACK_AMBISONIC_ORDER_SOFT),
+
+    DECL(AL_EFFECT_CONVOLUTION_REVERB_SOFT),
+    DECL(AL_EFFECTSLOT_STATE_SOFT),
+
+    DECL(AL_FORMAT_UHJ2CHN8_SOFT),
+    DECL(AL_FORMAT_UHJ2CHN16_SOFT),
+    DECL(AL_FORMAT_UHJ2CHN_FLOAT32_SOFT),
+    DECL(AL_FORMAT_UHJ3CHN8_SOFT),
+    DECL(AL_FORMAT_UHJ3CHN16_SOFT),
+    DECL(AL_FORMAT_UHJ3CHN_FLOAT32_SOFT),
+    DECL(AL_FORMAT_UHJ4CHN8_SOFT),
+    DECL(AL_FORMAT_UHJ4CHN16_SOFT),
+    DECL(AL_FORMAT_UHJ4CHN_FLOAT32_SOFT),
+    DECL(AL_STEREO_MODE_SOFT),
+    DECL(AL_NORMAL_SOFT),
+    DECL(AL_SUPER_STEREO_SOFT),
+    DECL(AL_SUPER_STEREO_WIDTH_SOFT),
+
+    DECL(AL_FORMAT_UHJ2CHN_MULAW_SOFT),
+    DECL(AL_FORMAT_UHJ2CHN_ALAW_SOFT),
+    DECL(AL_FORMAT_UHJ2CHN_IMA4_SOFT),
+    DECL(AL_FORMAT_UHJ2CHN_MSADPCM_SOFT),
+    DECL(AL_FORMAT_UHJ3CHN_MULAW_SOFT),
+    DECL(AL_FORMAT_UHJ3CHN_ALAW_SOFT),
+    DECL(AL_FORMAT_UHJ4CHN_MULAW_SOFT),
+    DECL(AL_FORMAT_UHJ4CHN_ALAW_SOFT),
+
+    DECL(AL_STOP_SOURCES_ON_DISCONNECT_SOFT),
+
+#ifdef ALSOFT_EAX
+}, eaxEnumerations[] = {
+    DECL(AL_EAX_RAM_SIZE),
+    DECL(AL_EAX_RAM_FREE),
+    DECL(AL_STORAGE_AUTOMATIC),
+    DECL(AL_STORAGE_HARDWARE),
+    DECL(AL_STORAGE_ACCESSIBLE),
+#endif // ALSOFT_EAX
+};
+#undef DECL
+
+constexpr ALCchar alcNoError[] = "No Error";
+constexpr ALCchar alcErrInvalidDevice[] = "Invalid Device";
+constexpr ALCchar alcErrInvalidContext[] = "Invalid Context";
+constexpr ALCchar alcErrInvalidEnum[] = "Invalid Enum";
+constexpr ALCchar alcErrInvalidValue[] = "Invalid Value";
+constexpr ALCchar alcErrOutOfMemory[] = "Out of Memory";
+
+
+/************************************************
+ * Global variables
+ ************************************************/
+
+/* Enumerated device names */
+constexpr ALCchar alcDefaultName[] = "OpenAL Soft\0";
+
+std::string alcAllDevicesList;
+std::string alcCaptureDeviceList;
+
+/* Default is always the first in the list */
+std::string alcDefaultAllDevicesSpecifier;
+std::string alcCaptureDefaultDeviceSpecifier;
+
+std::atomic<ALCenum> LastNullDeviceError{ALC_NO_ERROR};
+
+/* Flag to trap ALC device errors */
+bool TrapALCError{false};
+
+/* One-time configuration init control */
+std::once_flag alc_config_once{};
+
+/* Flag to specify if alcSuspendContext/alcProcessContext should defer/process
+ * updates.
+ */
+bool SuspendDefers{true};
+
+/* Initial seed for dithering. */
+constexpr uint DitherRNGSeed{22222u};
+
+
+/************************************************
+ * ALC information
+ ************************************************/
+constexpr ALCchar alcNoDeviceExtList[] =
+    "ALC_ENUMERATE_ALL_EXT "
+    "ALC_ENUMERATION_EXT "
+    "ALC_EXT_CAPTURE "
+    "ALC_EXT_EFX "
+    "ALC_EXT_thread_local_context "
+    "ALC_SOFT_loopback "
+    "ALC_SOFT_loopback_bformat "
+    "ALC_SOFT_reopen_device";
+constexpr ALCchar alcExtensionList[] =
+    "ALC_ENUMERATE_ALL_EXT "
+    "ALC_ENUMERATION_EXT "
+    "ALC_EXT_CAPTURE "
+    "ALC_EXT_DEDICATED "
+    "ALC_EXT_disconnect "
+    "ALC_EXT_EFX "
+    "ALC_EXT_thread_local_context "
+    "ALC_SOFT_device_clock "
+    "ALC_SOFT_HRTF "
+    "ALC_SOFT_loopback "
+    "ALC_SOFT_loopback_bformat "
+    "ALC_SOFT_output_limiter "
+    "ALC_SOFT_output_mode "
+    "ALC_SOFT_pause_device "
+    "ALC_SOFT_reopen_device";
+constexpr int alcMajorVersion{1};
+constexpr int alcMinorVersion{1};
+
+constexpr int alcEFXMajorVersion{1};
+constexpr int alcEFXMinorVersion{0};
+
+
+using DeviceRef = al::intrusive_ptr<ALCdevice>;
+
+
+/************************************************
+ * Device lists
+ ************************************************/
+al::vector<ALCdevice*> DeviceList;
+al::vector<ALCcontext*> ContextList;
+
+std::recursive_mutex ListLock;
+
+
+void alc_initconfig(void)
+{
+    if(auto loglevel = al::getenv("ALSOFT_LOGLEVEL"))
+    {
+        long lvl = strtol(loglevel->c_str(), nullptr, 0);
+        if(lvl >= static_cast<long>(LogLevel::Trace))
+            gLogLevel = LogLevel::Trace;
+        else if(lvl <= static_cast<long>(LogLevel::Disable))
+            gLogLevel = LogLevel::Disable;
+        else
+            gLogLevel = static_cast<LogLevel>(lvl);
+    }
+
+#ifdef _WIN32
+    if(const auto logfile = al::getenv(L"ALSOFT_LOGFILE"))
+    {
+        FILE *logf{_wfopen(logfile->c_str(), L"wt")};
+        if(logf) gLogFile = logf;
+        else
+        {
+            auto u8name = wstr_to_utf8(logfile->c_str());
+            ERR("Failed to open log file '%s'\n", u8name.c_str());
+        }
+    }
+#else
+    if(const auto logfile = al::getenv("ALSOFT_LOGFILE"))
+    {
+        FILE *logf{fopen(logfile->c_str(), "wt")};
+        if(logf) gLogFile = logf;
+        else ERR("Failed to open log file '%s'\n", logfile->c_str());
+    }
+#endif
+
+    TRACE("Initializing library v%s-%s %s\n", ALSOFT_VERSION, ALSOFT_GIT_COMMIT_HASH,
+        ALSOFT_GIT_BRANCH);
+    {
+        std::string names;
+        if(al::size(BackendList) < 1)
+            names = "(none)";
+        else
+        {
+            const al::span<const BackendInfo> infos{BackendList};
+            names = infos[0].name;
+            for(const auto &backend : infos.subspan<1>())
+            {
+                names += ", ";
+                names += backend.name;
+            }
+        }
+        TRACE("Supported backends: %s\n", names.c_str());
+    }
+    ReadALConfig();
+
+    if(auto suspendmode = al::getenv("__ALSOFT_SUSPEND_CONTEXT"))
+    {
+        if(al::strcasecmp(suspendmode->c_str(), "ignore") == 0)
+        {
+            SuspendDefers = false;
+            TRACE("Selected context suspend behavior, \"ignore\"\n");
+        }
+        else
+            ERR("Unhandled context suspend behavior setting: \"%s\"\n", suspendmode->c_str());
+    }
+
+    int capfilter{0};
+#if defined(HAVE_SSE4_1)
+    capfilter |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3 | CPU_CAP_SSE4_1;
+#elif defined(HAVE_SSE3)
+    capfilter |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3;
+#elif defined(HAVE_SSE2)
+    capfilter |= CPU_CAP_SSE | CPU_CAP_SSE2;
+#elif defined(HAVE_SSE)
+    capfilter |= CPU_CAP_SSE;
+#endif
+#ifdef HAVE_NEON
+    capfilter |= CPU_CAP_NEON;
+#endif
+    if(auto cpuopt = ConfigValueStr(nullptr, nullptr, "disable-cpu-exts"))
+    {
+        const char *str{cpuopt->c_str()};
+        if(al::strcasecmp(str, "all") == 0)
+            capfilter = 0;
+        else
+        {
+            const char *next = str;
+            do {
+                str = next;
+                while(isspace(str[0]))
+                    str++;
+                next = strchr(str, ',');
+
+                if(!str[0] || str[0] == ',')
+                    continue;
+
+                size_t len{next ? static_cast<size_t>(next-str) : strlen(str)};
+                while(len > 0 && isspace(str[len-1]))
+                    len--;
+                if(len == 3 && al::strncasecmp(str, "sse", len) == 0)
+                    capfilter &= ~CPU_CAP_SSE;
+                else if(len == 4 && al::strncasecmp(str, "sse2", len) == 0)
+                    capfilter &= ~CPU_CAP_SSE2;
+                else if(len == 4 && al::strncasecmp(str, "sse3", len) == 0)
+                    capfilter &= ~CPU_CAP_SSE3;
+                else if(len == 6 && al::strncasecmp(str, "sse4.1", len) == 0)
+                    capfilter &= ~CPU_CAP_SSE4_1;
+                else if(len == 4 && al::strncasecmp(str, "neon", len) == 0)
+                    capfilter &= ~CPU_CAP_NEON;
+                else
+                    WARN("Invalid CPU extension \"%s\"\n", str);
+            } while(next++);
+        }
+    }
+    if(auto cpuopt = GetCPUInfo())
+    {
+        if(!cpuopt->mVendor.empty() || !cpuopt->mName.empty())
+        {
+            TRACE("Vendor ID: \"%s\"\n", cpuopt->mVendor.c_str());
+            TRACE("Name: \"%s\"\n", cpuopt->mName.c_str());
+        }
+        const int caps{cpuopt->mCaps};
+        TRACE("Extensions:%s%s%s%s%s%s\n",
+            ((capfilter&CPU_CAP_SSE)    ? ((caps&CPU_CAP_SSE)    ? " +SSE"    : " -SSE")    : ""),
+            ((capfilter&CPU_CAP_SSE2)   ? ((caps&CPU_CAP_SSE2)   ? " +SSE2"   : " -SSE2")   : ""),
+            ((capfilter&CPU_CAP_SSE3)   ? ((caps&CPU_CAP_SSE3)   ? " +SSE3"   : " -SSE3")   : ""),
+            ((capfilter&CPU_CAP_SSE4_1) ? ((caps&CPU_CAP_SSE4_1) ? " +SSE4.1" : " -SSE4.1") : ""),
+            ((capfilter&CPU_CAP_NEON)   ? ((caps&CPU_CAP_NEON)   ? " +NEON"   : " -NEON")   : ""),
+            ((!capfilter) ? " -none-" : ""));
+        CPUCapFlags = caps & capfilter;
+    }
+
+    if(auto priopt = ConfigValueInt(nullptr, nullptr, "rt-prio"))
+        RTPrioLevel = *priopt;
+    if(auto limopt = ConfigValueBool(nullptr, nullptr, "rt-time-limit"))
+        AllowRTTimeLimit = *limopt;
+
+    {
+        CompatFlagBitset compatflags{};
+        auto checkflag = [](const char *envname, const char *optname) -> bool
+        {
+            if(auto optval = al::getenv(envname))
+            {
+                if(al::strcasecmp(optval->c_str(), "true") == 0
+                    || strtol(optval->c_str(), nullptr, 0) == 1)
+                    return true;
+                return false;
+            }
+            return GetConfigValueBool(nullptr, "game_compat", optname, false);
+        };
+        sBufferSubDataCompat = checkflag("__ALSOFT_ENABLE_SUB_DATA_EXT", "enable-sub-data-ext");
+        compatflags.set(CompatFlags::ReverseX, checkflag("__ALSOFT_REVERSE_X", "reverse-x"));
+        compatflags.set(CompatFlags::ReverseY, checkflag("__ALSOFT_REVERSE_Y", "reverse-y"));
+        compatflags.set(CompatFlags::ReverseZ, checkflag("__ALSOFT_REVERSE_Z", "reverse-z"));
+
+        aluInit(compatflags, ConfigValueFloat(nullptr, "game_compat", "nfc-scale").value_or(1.0f));
+    }
+    Voice::InitMixer(ConfigValueStr(nullptr, nullptr, "resampler"));
+
+    auto uhjfiltopt = ConfigValueStr(nullptr, "uhj", "decode-filter");
+    if(!uhjfiltopt)
+    {
+        if((uhjfiltopt = ConfigValueStr(nullptr, "uhj", "filter")))
+            WARN("uhj/filter is deprecated, please use uhj/decode-filter\n");
+    }
+    if(uhjfiltopt)
+    {
+        if(al::strcasecmp(uhjfiltopt->c_str(), "fir256") == 0)
+            UhjDecodeQuality = UhjQualityType::FIR256;
+        else if(al::strcasecmp(uhjfiltopt->c_str(), "fir512") == 0)
+            UhjDecodeQuality = UhjQualityType::FIR512;
+        else if(al::strcasecmp(uhjfiltopt->c_str(), "iir") == 0)
+            UhjDecodeQuality = UhjQualityType::IIR;
+        else
+            WARN("Unsupported uhj/decode-filter: %s\n", uhjfiltopt->c_str());
+    }
+    if((uhjfiltopt = ConfigValueStr(nullptr, "uhj", "encode-filter")))
+    {
+        if(al::strcasecmp(uhjfiltopt->c_str(), "fir256") == 0)
+            UhjEncodeQuality = UhjQualityType::FIR256;
+        else if(al::strcasecmp(uhjfiltopt->c_str(), "fir512") == 0)
+            UhjEncodeQuality = UhjQualityType::FIR512;
+        else if(al::strcasecmp(uhjfiltopt->c_str(), "iir") == 0)
+            UhjEncodeQuality = UhjQualityType::IIR;
+        else
+            WARN("Unsupported uhj/encode-filter: %s\n", uhjfiltopt->c_str());
+    }
+
+    auto traperr = al::getenv("ALSOFT_TRAP_ERROR");
+    if(traperr && (al::strcasecmp(traperr->c_str(), "true") == 0
+            || std::strtol(traperr->c_str(), nullptr, 0) == 1))
+    {
+        TrapALError  = true;
+        TrapALCError = true;
+    }
+    else
+    {
+        traperr = al::getenv("ALSOFT_TRAP_AL_ERROR");
+        if(traperr)
+            TrapALError = al::strcasecmp(traperr->c_str(), "true") == 0
+                || strtol(traperr->c_str(), nullptr, 0) == 1;
+        else
+            TrapALError = !!GetConfigValueBool(nullptr, nullptr, "trap-al-error", false);
+
+        traperr = al::getenv("ALSOFT_TRAP_ALC_ERROR");
+        if(traperr)
+            TrapALCError = al::strcasecmp(traperr->c_str(), "true") == 0
+                || strtol(traperr->c_str(), nullptr, 0) == 1;
+        else
+            TrapALCError = !!GetConfigValueBool(nullptr, nullptr, "trap-alc-error", false);
+    }
+
+    if(auto boostopt = ConfigValueFloat(nullptr, "reverb", "boost"))
+    {
+        const float valf{std::isfinite(*boostopt) ? clampf(*boostopt, -24.0f, 24.0f) : 0.0f};
+        ReverbBoost *= std::pow(10.0f, valf / 20.0f);
+    }
+
+    auto BackendListEnd = std::end(BackendList);
+    auto devopt = al::getenv("ALSOFT_DRIVERS");
+    if(devopt || (devopt=ConfigValueStr(nullptr, nullptr, "drivers")))
+    {
+        auto backendlist_cur = std::begin(BackendList);
+
+        bool endlist{true};
+        const char *next{devopt->c_str()};
+        do {
+            const char *devs{next};
+            while(isspace(devs[0]))
+                devs++;
+            next = strchr(devs, ',');
+
+            const bool delitem{devs[0] == '-'};
+            if(devs[0] == '-') devs++;
+
+            if(!devs[0] || devs[0] == ',')
+            {
+                endlist = false;
+                continue;
+            }
+            endlist = true;
+
+            size_t len{next ? (static_cast<size_t>(next-devs)) : strlen(devs)};
+            while(len > 0 && isspace(devs[len-1])) --len;
+#ifdef HAVE_WASAPI
+            /* HACK: For backwards compatibility, convert backend references of
+             * mmdevapi to wasapi. This should eventually be removed.
+             */
+            if(len == 8 && strncmp(devs, "mmdevapi", len) == 0)
+            {
+                devs = "wasapi";
+                len = 6;
+            }
+#endif
+
+            auto find_backend = [devs,len](const BackendInfo &backend) -> bool
+            { return len == strlen(backend.name) && strncmp(backend.name, devs, len) == 0; };
+            auto this_backend = std::find_if(std::begin(BackendList), BackendListEnd,
+                find_backend);
+
+            if(this_backend == BackendListEnd)
+                continue;
+
+            if(delitem)
+                BackendListEnd = std::move(this_backend+1, BackendListEnd, this_backend);
+            else
+                backendlist_cur = std::rotate(backendlist_cur, this_backend, this_backend+1);
+        } while(next++);
+
+        if(endlist)
+            BackendListEnd = backendlist_cur;
+    }
+
+    auto init_backend = [](BackendInfo &backend) -> void
+    {
+        if(PlaybackFactory && CaptureFactory)
+            return;
+
+        BackendFactory &factory = backend.getFactory();
+        if(!factory.init())
+        {
+            WARN("Failed to initialize backend \"%s\"\n", backend.name);
+            return;
+        }
+
+        TRACE("Initialized backend \"%s\"\n", backend.name);
+        if(!PlaybackFactory && factory.querySupport(BackendType::Playback))
+        {
+            PlaybackFactory = &factory;
+            TRACE("Added \"%s\" for playback\n", backend.name);
+        }
+        if(!CaptureFactory && factory.querySupport(BackendType::Capture))
+        {
+            CaptureFactory = &factory;
+            TRACE("Added \"%s\" for capture\n", backend.name);
+        }
+    };
+    std::for_each(std::begin(BackendList), BackendListEnd, init_backend);
+
+    LoopbackBackendFactory::getFactory().init();
+
+    if(!PlaybackFactory)
+        WARN("No playback backend available!\n");
+    if(!CaptureFactory)
+        WARN("No capture backend available!\n");
+
+    if(auto exclopt = ConfigValueStr(nullptr, nullptr, "excludefx"))
+    {
+        const char *next{exclopt->c_str()};
+        do {
+            const char *str{next};
+            next = strchr(str, ',');
+
+            if(!str[0] || next == str)
+                continue;
+
+            size_t len{next ? static_cast<size_t>(next-str) : strlen(str)};
+            for(const EffectList &effectitem : gEffectList)
+            {
+                if(len == strlen(effectitem.name) &&
+                   strncmp(effectitem.name, str, len) == 0)
+                    DisabledEffects[effectitem.type] = true;
+            }
+        } while(next++);
+    }
+
+    InitEffect(&ALCcontext::sDefaultEffect);
+    auto defrevopt = al::getenv("ALSOFT_DEFAULT_REVERB");
+    if(defrevopt || (defrevopt=ConfigValueStr(nullptr, nullptr, "default-reverb")))
+        LoadReverbPreset(defrevopt->c_str(), &ALCcontext::sDefaultEffect);
+
+#ifdef ALSOFT_EAX
+    {
+        static constexpr char eax_block_name[] = "eax";
+
+        if(const auto eax_enable_opt = ConfigValueBool(nullptr, eax_block_name, "enable"))
+        {
+            eax_g_is_enabled = *eax_enable_opt;
+            if(!eax_g_is_enabled)
+                TRACE("%s\n", "EAX disabled by a configuration.");
+        }
+        else
+            eax_g_is_enabled = true;
+
+        if((DisabledEffects[EAXREVERB_EFFECT] || DisabledEffects[CHORUS_EFFECT])
+            && eax_g_is_enabled)
+        {
+            eax_g_is_enabled = false;
+            TRACE("EAX disabled because %s disabled.\n",
+                (DisabledEffects[EAXREVERB_EFFECT] && DisabledEffects[CHORUS_EFFECT])
+                    ? "EAXReverb and Chorus are" :
+                DisabledEffects[EAXREVERB_EFFECT] ? "EAXReverb is" :
+                DisabledEffects[CHORUS_EFFECT] ? "Chorus is" : "");
+        }
+    }
+#endif // ALSOFT_EAX
+}
+inline void InitConfig()
+{ std::call_once(alc_config_once, [](){alc_initconfig();}); }
+
+
+/************************************************
+ * Device enumeration
+ ************************************************/
+void ProbeAllDevicesList()
+{
+    InitConfig();
+
+    std::lock_guard<std::recursive_mutex> _{ListLock};
+    if(!PlaybackFactory)
+        decltype(alcAllDevicesList){}.swap(alcAllDevicesList);
+    else
+    {
+        std::string names{PlaybackFactory->probe(BackendType::Playback)};
+        if(names.empty()) names += '\0';
+        names.swap(alcAllDevicesList);
+    }
+}
+void ProbeCaptureDeviceList()
+{
+    InitConfig();
+
+    std::lock_guard<std::recursive_mutex> _{ListLock};
+    if(!CaptureFactory)
+        decltype(alcCaptureDeviceList){}.swap(alcCaptureDeviceList);
+    else
+    {
+        std::string names{CaptureFactory->probe(BackendType::Capture)};
+        if(names.empty()) names += '\0';
+        names.swap(alcCaptureDeviceList);
+    }
+}
+
+
+struct DevFmtPair { DevFmtChannels chans; DevFmtType type; };
+al::optional<DevFmtPair> DecomposeDevFormat(ALenum format)
+{
+    static const struct {
+        ALenum format;
+        DevFmtChannels channels;
+        DevFmtType type;
+    } list[] = {
+        { AL_FORMAT_MONO8,        DevFmtMono, DevFmtUByte },
+        { AL_FORMAT_MONO16,       DevFmtMono, DevFmtShort },
+        { AL_FORMAT_MONO_FLOAT32, DevFmtMono, DevFmtFloat },
+
+        { AL_FORMAT_STEREO8,        DevFmtStereo, DevFmtUByte },
+        { AL_FORMAT_STEREO16,       DevFmtStereo, DevFmtShort },
+        { AL_FORMAT_STEREO_FLOAT32, DevFmtStereo, DevFmtFloat },
+
+        { AL_FORMAT_QUAD8,  DevFmtQuad, DevFmtUByte },
+        { AL_FORMAT_QUAD16, DevFmtQuad, DevFmtShort },
+        { AL_FORMAT_QUAD32, DevFmtQuad, DevFmtFloat },
+
+        { AL_FORMAT_51CHN8,  DevFmtX51, DevFmtUByte },
+        { AL_FORMAT_51CHN16, DevFmtX51, DevFmtShort },
+        { AL_FORMAT_51CHN32, DevFmtX51, DevFmtFloat },
+
+        { AL_FORMAT_61CHN8,  DevFmtX61, DevFmtUByte },
+        { AL_FORMAT_61CHN16, DevFmtX61, DevFmtShort },
+        { AL_FORMAT_61CHN32, DevFmtX61, DevFmtFloat },
+
+        { AL_FORMAT_71CHN8,  DevFmtX71, DevFmtUByte },
+        { AL_FORMAT_71CHN16, DevFmtX71, DevFmtShort },
+        { AL_FORMAT_71CHN32, DevFmtX71, DevFmtFloat },
+    };
+
+    for(const auto &item : list)
+    {
+        if(item.format == format)
+            return al::make_optional<DevFmtPair>({item.channels, item.type});
+    }
+
+    return al::nullopt;
+}
+
+al::optional<DevFmtType> DevFmtTypeFromEnum(ALCenum type)
+{
+    switch(type)
+    {
+    case ALC_BYTE_SOFT: return DevFmtByte;
+    case ALC_UNSIGNED_BYTE_SOFT: return DevFmtUByte;
+    case ALC_SHORT_SOFT: return DevFmtShort;
+    case ALC_UNSIGNED_SHORT_SOFT: return DevFmtUShort;
+    case ALC_INT_SOFT: return DevFmtInt;
+    case ALC_UNSIGNED_INT_SOFT: return DevFmtUInt;
+    case ALC_FLOAT_SOFT: return DevFmtFloat;
+    }
+    WARN("Unsupported format type: 0x%04x\n", type);
+    return al::nullopt;
+}
+ALCenum EnumFromDevFmt(DevFmtType type)
+{
+    switch(type)
+    {
+    case DevFmtByte: return ALC_BYTE_SOFT;
+    case DevFmtUByte: return ALC_UNSIGNED_BYTE_SOFT;
+    case DevFmtShort: return ALC_SHORT_SOFT;
+    case DevFmtUShort: return ALC_UNSIGNED_SHORT_SOFT;
+    case DevFmtInt: return ALC_INT_SOFT;
+    case DevFmtUInt: return ALC_UNSIGNED_INT_SOFT;
+    case DevFmtFloat: return ALC_FLOAT_SOFT;
+    }
+    throw std::runtime_error{"Invalid DevFmtType: "+std::to_string(int(type))};
+}
+
+al::optional<DevFmtChannels> DevFmtChannelsFromEnum(ALCenum channels)
+{
+    switch(channels)
+    {
+    case ALC_MONO_SOFT: return DevFmtMono;
+    case ALC_STEREO_SOFT: return DevFmtStereo;
+    case ALC_QUAD_SOFT: return DevFmtQuad;
+    case ALC_5POINT1_SOFT: return DevFmtX51;
+    case ALC_6POINT1_SOFT: return DevFmtX61;
+    case ALC_7POINT1_SOFT: return DevFmtX71;
+    case ALC_BFORMAT3D_SOFT: return DevFmtAmbi3D;
+    }
+    WARN("Unsupported format channels: 0x%04x\n", channels);
+    return al::nullopt;
+}
+ALCenum EnumFromDevFmt(DevFmtChannels channels)
+{
+    switch(channels)
+    {
+    case DevFmtMono: return ALC_MONO_SOFT;
+    case DevFmtStereo: return ALC_STEREO_SOFT;
+    case DevFmtQuad: return ALC_QUAD_SOFT;
+    case DevFmtX51: return ALC_5POINT1_SOFT;
+    case DevFmtX61: return ALC_6POINT1_SOFT;
+    case DevFmtX71: return ALC_7POINT1_SOFT;
+    case DevFmtAmbi3D: return ALC_BFORMAT3D_SOFT;
+    /* FIXME: Shouldn't happen. */
+    case DevFmtX714:
+    case DevFmtX3D71: break;
+    }
+    throw std::runtime_error{"Invalid DevFmtChannels: "+std::to_string(int(channels))};
+}
+
+al::optional<DevAmbiLayout> DevAmbiLayoutFromEnum(ALCenum layout)
+{
+    switch(layout)
+    {
+    case ALC_FUMA_SOFT: return DevAmbiLayout::FuMa;
+    case ALC_ACN_SOFT: return DevAmbiLayout::ACN;
+    }
+    WARN("Unsupported ambisonic layout: 0x%04x\n", layout);
+    return al::nullopt;
+}
+ALCenum EnumFromDevAmbi(DevAmbiLayout layout)
+{
+    switch(layout)
+    {
+    case DevAmbiLayout::FuMa: return ALC_FUMA_SOFT;
+    case DevAmbiLayout::ACN: return ALC_ACN_SOFT;
+    }
+    throw std::runtime_error{"Invalid DevAmbiLayout: "+std::to_string(int(layout))};
+}
+
+al::optional<DevAmbiScaling> DevAmbiScalingFromEnum(ALCenum scaling)
+{
+    switch(scaling)
+    {
+    case ALC_FUMA_SOFT: return DevAmbiScaling::FuMa;
+    case ALC_SN3D_SOFT: return DevAmbiScaling::SN3D;
+    case ALC_N3D_SOFT: return DevAmbiScaling::N3D;
+    }
+    WARN("Unsupported ambisonic scaling: 0x%04x\n", scaling);
+    return al::nullopt;
+}
+ALCenum EnumFromDevAmbi(DevAmbiScaling scaling)
+{
+    switch(scaling)
+    {
+    case DevAmbiScaling::FuMa: return ALC_FUMA_SOFT;
+    case DevAmbiScaling::SN3D: return ALC_SN3D_SOFT;
+    case DevAmbiScaling::N3D: return ALC_N3D_SOFT;
+    }
+    throw std::runtime_error{"Invalid DevAmbiScaling: "+std::to_string(int(scaling))};
+}
+
+
+/* Downmixing channel arrays, to map the given format's missing channels to
+ * existing ones. Based on Wine's DSound downmix values, which are based on
+ * PulseAudio's.
+ */
+constexpr std::array<InputRemixMap::TargetMix,2> FrontStereoSplit{{
+    {FrontLeft, 0.5f}, {FrontRight, 0.5f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,1> FrontLeft9{{
+    {FrontLeft, 1.0f/9.0f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,1> FrontRight9{{
+    {FrontRight, 1.0f/9.0f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,2> BackMonoToFrontSplit{{
+    {FrontLeft, 0.5f/9.0f}, {FrontRight, 0.5f/9.0f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,2> LeftStereoSplit{{
+    {FrontLeft, 0.5f}, {BackLeft, 0.5f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,2> RightStereoSplit{{
+    {FrontRight, 0.5f}, {BackRight, 0.5f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,2> BackStereoSplit{{
+    {BackLeft, 0.5f}, {BackRight, 0.5f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,2> SideStereoSplit{{
+    {SideLeft, 0.5f}, {SideRight, 0.5f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,1> ToSideLeft{{
+    {SideLeft, 1.0f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,1> ToSideRight{{
+    {SideRight, 1.0f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,2> BackLeftSplit{{
+    {SideLeft, 0.5f}, {BackCenter, 0.5f}
+}};
+constexpr std::array<InputRemixMap::TargetMix,2> BackRightSplit{{
+    {SideRight, 0.5f}, {BackCenter, 0.5f}
+}};
+
+const std::array<InputRemixMap,6> StereoDownmix{{
+    { FrontCenter, FrontStereoSplit },
+    { SideLeft,    FrontLeft9 },
+    { SideRight,   FrontRight9 },
+    { BackLeft,    FrontLeft9 },
+    { BackRight,   FrontRight9 },
+    { BackCenter,  BackMonoToFrontSplit },
+}};
+const std::array<InputRemixMap,4> QuadDownmix{{
+    { FrontCenter, FrontStereoSplit },
+    { SideLeft,    LeftStereoSplit },
+    { SideRight,   RightStereoSplit },
+    { BackCenter,  BackStereoSplit },
+}};
+const std::array<InputRemixMap,3> X51Downmix{{
+    { BackLeft,   ToSideLeft },
+    { BackRight,  ToSideRight },
+    { BackCenter, SideStereoSplit },
+}};
+const std::array<InputRemixMap,2> X61Downmix{{
+    { BackLeft,  BackLeftSplit },
+    { BackRight, BackRightSplit },
+}};
+const std::array<InputRemixMap,1> X71Downmix{{
+    { BackCenter, BackStereoSplit },
+}};
+
+
+/** Stores the latest ALC device error. */
+void alcSetError(ALCdevice *device, ALCenum errorCode)
+{
+    WARN("Error generated on device %p, code 0x%04x\n", voidp{device}, errorCode);
+    if(TrapALCError)
+    {
+#ifdef _WIN32
+        /* DebugBreak() will cause an exception if there is no debugger */
+        if(IsDebuggerPresent())
+            DebugBreak();
+#elif defined(SIGTRAP)
+        raise(SIGTRAP);
+#endif
+    }
+
+    if(device)
+        device->LastError.store(errorCode);
+    else
+        LastNullDeviceError.store(errorCode);
+}
+
+
+std::unique_ptr<Compressor> CreateDeviceLimiter(const ALCdevice *device, const float threshold)
+{
+    static constexpr bool AutoKnee{true};
+    static constexpr bool AutoAttack{true};
+    static constexpr bool AutoRelease{true};
+    static constexpr bool AutoPostGain{true};
+    static constexpr bool AutoDeclip{true};
+    static constexpr float LookAheadTime{0.001f};
+    static constexpr float HoldTime{0.002f};
+    static constexpr float PreGainDb{0.0f};
+    static constexpr float PostGainDb{0.0f};
+    static constexpr float Ratio{std::numeric_limits<float>::infinity()};
+    static constexpr float KneeDb{0.0f};
+    static constexpr float AttackTime{0.02f};
+    static constexpr float ReleaseTime{0.2f};
+
+    return Compressor::Create(device->RealOut.Buffer.size(), static_cast<float>(device->Frequency),
+        AutoKnee, AutoAttack, AutoRelease, AutoPostGain, AutoDeclip, LookAheadTime, HoldTime,
+        PreGainDb, PostGainDb, threshold, Ratio, KneeDb, AttackTime, ReleaseTime);
+}
+
+/**
+ * Updates the device's base clock time with however many samples have been
+ * done. This is used so frequency changes on the device don't cause the time
+ * to jump forward or back. Must not be called while the device is running/
+ * mixing.
+ */
+inline void UpdateClockBase(ALCdevice *device)
+{
+    IncrementRef(device->MixCount);
+    device->ClockBase += nanoseconds{seconds{device->SamplesDone}} / device->Frequency;
+    device->SamplesDone = 0;
+    IncrementRef(device->MixCount);
+}
+
+/**
+ * Updates device parameters according to the attribute list (caller is
+ * responsible for holding the list lock).
+ */
+ALCenum UpdateDeviceParams(ALCdevice *device, const int *attrList)
+{
+    if((!attrList || !attrList[0]) && device->Type == DeviceType::Loopback)
+    {
+        WARN("Missing attributes for loopback device\n");
+        return ALC_INVALID_VALUE;
+    }
+
+    uint numMono{device->NumMonoSources};
+    uint numStereo{device->NumStereoSources};
+    uint numSends{device->NumAuxSends};
+    al::optional<StereoEncoding> stereomode;
+    al::optional<bool> optlimit;
+    al::optional<uint> optsrate;
+    al::optional<DevFmtChannels> optchans;
+    al::optional<DevFmtType> opttype;
+    al::optional<DevAmbiLayout> optlayout;
+    al::optional<DevAmbiScaling> optscale;
+    uint period_size{DEFAULT_UPDATE_SIZE};
+    uint buffer_size{DEFAULT_UPDATE_SIZE * DEFAULT_NUM_UPDATES};
+    int hrtf_id{-1};
+    uint aorder{0u};
+
+    if(device->Type != DeviceType::Loopback)
+    {
+        /* Get default settings from the user configuration */
+
+        if(auto freqopt = device->configValue<uint>(nullptr, "frequency"))
+        {
+            optsrate = clampu(*freqopt, MIN_OUTPUT_RATE, MAX_OUTPUT_RATE);
+
+            const double scale{static_cast<double>(*optsrate) / DEFAULT_OUTPUT_RATE};
+            period_size = static_cast<uint>(period_size*scale + 0.5);
+        }
+
+        if(auto persizeopt = device->configValue<uint>(nullptr, "period_size"))
+            period_size = clampu(*persizeopt, 64, 8192);
+        if(auto numperopt = device->configValue<uint>(nullptr, "periods"))
+            buffer_size = clampu(*numperopt, 2, 16) * period_size;
+        else
+            buffer_size = period_size * DEFAULT_NUM_UPDATES;
+
+        if(auto typeopt = device->configValue<std::string>(nullptr, "sample-type"))
+        {
+            static constexpr struct TypeMap {
+                const char name[8];
+                DevFmtType type;
+            } typelist[] = {
+                { "int8",    DevFmtByte   },
+                { "uint8",   DevFmtUByte  },
+                { "int16",   DevFmtShort  },
+                { "uint16",  DevFmtUShort },
+                { "int32",   DevFmtInt    },
+                { "uint32",  DevFmtUInt   },
+                { "float32", DevFmtFloat  },
+            };
+
+            const ALCchar *fmt{typeopt->c_str()};
+            auto iter = std::find_if(std::begin(typelist), std::end(typelist),
+                [fmt](const TypeMap &entry) -> bool
+                { return al::strcasecmp(entry.name, fmt) == 0; });
+            if(iter == std::end(typelist))
+                ERR("Unsupported sample-type: %s\n", fmt);
+            else
+                opttype = iter->type;
+        }
+        if(auto chanopt = device->configValue<std::string>(nullptr, "channels"))
+        {
+            static constexpr struct ChannelMap {
+                const char name[16];
+                DevFmtChannels chans;
+                uint8_t order;
+            } chanlist[] = {
+                { "mono",       DevFmtMono,   0 },
+                { "stereo",     DevFmtStereo, 0 },
+                { "quad",       DevFmtQuad,   0 },
+                { "surround51", DevFmtX51,    0 },
+                { "surround61", DevFmtX61,    0 },
+                { "surround71", DevFmtX71,    0 },
+                { "surround714", DevFmtX714,  0 },
+                { "surround3d71", DevFmtX3D71, 0 },
+                { "surround51rear", DevFmtX51, 0 },
+                { "ambi1", DevFmtAmbi3D, 1 },
+                { "ambi2", DevFmtAmbi3D, 2 },
+                { "ambi3", DevFmtAmbi3D, 3 },
+            };
+
+            const ALCchar *fmt{chanopt->c_str()};
+            auto iter = std::find_if(std::begin(chanlist), std::end(chanlist),
+                [fmt](const ChannelMap &entry) -> bool
+                { return al::strcasecmp(entry.name, fmt) == 0; });
+            if(iter == std::end(chanlist))
+                ERR("Unsupported channels: %s\n", fmt);
+            else
+            {
+                optchans = iter->chans;
+                aorder = iter->order;
+            }
+        }
+        if(auto ambiopt = device->configValue<std::string>(nullptr, "ambi-format"))
+        {
+            const ALCchar *fmt{ambiopt->c_str()};
+            if(al::strcasecmp(fmt, "fuma") == 0)
+            {
+                optlayout = DevAmbiLayout::FuMa;
+                optscale = DevAmbiScaling::FuMa;
+            }
+            else if(al::strcasecmp(fmt, "acn+fuma") == 0)
+            {
+                optlayout = DevAmbiLayout::ACN;
+                optscale = DevAmbiScaling::FuMa;
+            }
+            else if(al::strcasecmp(fmt, "ambix") == 0 || al::strcasecmp(fmt, "acn+sn3d") == 0)
+            {
+                optlayout = DevAmbiLayout::ACN;
+                optscale = DevAmbiScaling::SN3D;
+            }
+            else if(al::strcasecmp(fmt, "acn+n3d") == 0)
+            {
+                optlayout = DevAmbiLayout::ACN;
+                optscale = DevAmbiScaling::N3D;
+            }
+            else
+                ERR("Unsupported ambi-format: %s\n", fmt);
+        }
+
+        if(auto hrtfopt = device->configValue<std::string>(nullptr, "hrtf"))
+        {
+            WARN("general/hrtf is deprecated, please use stereo-encoding instead\n");
+
+            const char *hrtf{hrtfopt->c_str()};
+            if(al::strcasecmp(hrtf, "true") == 0)
+                stereomode = StereoEncoding::Hrtf;
+            else if(al::strcasecmp(hrtf, "false") == 0)
+            {
+                if(!stereomode || *stereomode == StereoEncoding::Hrtf)
+                    stereomode = StereoEncoding::Default;
+            }
+            else if(al::strcasecmp(hrtf, "auto") != 0)
+                ERR("Unexpected hrtf value: %s\n", hrtf);
+        }
+    }
+
+    if(auto encopt = device->configValue<std::string>(nullptr, "stereo-encoding"))
+    {
+        const char *mode{encopt->c_str()};
+        if(al::strcasecmp(mode, "basic") == 0 || al::strcasecmp(mode, "panpot") == 0)
+            stereomode = StereoEncoding::Basic;
+        else if(al::strcasecmp(mode, "uhj") == 0)
+            stereomode = StereoEncoding::Uhj;
+        else if(al::strcasecmp(mode, "hrtf") == 0)
+            stereomode = StereoEncoding::Hrtf;
+        else
+            ERR("Unexpected stereo-encoding: %s\n", mode);
+    }
+
+    // Check for app-specified attributes
+    if(attrList && attrList[0])
+    {
+        ALenum outmode{ALC_ANY_SOFT};
+        al::optional<bool> opthrtf;
+        int freqAttr{};
+
+#define ATTRIBUTE(a) a: TRACE("%s = %d\n", #a, attrList[attrIdx + 1]);
+        size_t attrIdx{0};
+        while(attrList[attrIdx])
+        {
+            switch(attrList[attrIdx])
+            {
+            case ATTRIBUTE(ALC_FORMAT_CHANNELS_SOFT)
+                if(device->Type == DeviceType::Loopback)
+                    optchans = DevFmtChannelsFromEnum(attrList[attrIdx + 1]);
+                break;
+
+            case ATTRIBUTE(ALC_FORMAT_TYPE_SOFT)
+                if(device->Type == DeviceType::Loopback)
+                    opttype = DevFmtTypeFromEnum(attrList[attrIdx + 1]);
+                break;
+
+            case ATTRIBUTE(ALC_FREQUENCY)
+                freqAttr = attrList[attrIdx + 1];
+                break;
+
+            case ATTRIBUTE(ALC_AMBISONIC_LAYOUT_SOFT)
+                if(device->Type == DeviceType::Loopback)
+                    optlayout = DevAmbiLayoutFromEnum(attrList[attrIdx + 1]);
+                break;
+
+            case ATTRIBUTE(ALC_AMBISONIC_SCALING_SOFT)
+                if(device->Type == DeviceType::Loopback)
+                    optscale = DevAmbiScalingFromEnum(attrList[attrIdx + 1]);
+                break;
+
+            case ATTRIBUTE(ALC_AMBISONIC_ORDER_SOFT)
+                if(device->Type == DeviceType::Loopback)
+                    aorder = static_cast<uint>(attrList[attrIdx + 1]);
+                break;
+
+            case ATTRIBUTE(ALC_MONO_SOURCES)
+                numMono = static_cast<uint>(attrList[attrIdx + 1]);
+                if(numMono > INT_MAX) numMono = 0;
+                break;
+
+            case ATTRIBUTE(ALC_STEREO_SOURCES)
+                numStereo = static_cast<uint>(attrList[attrIdx + 1]);
+                if(numStereo > INT_MAX) numStereo = 0;
+                break;
+
+            case ATTRIBUTE(ALC_MAX_AUXILIARY_SENDS)
+                numSends = static_cast<uint>(attrList[attrIdx + 1]);
+                if(numSends > INT_MAX) numSends = 0;
+                else numSends = minu(numSends, MAX_SENDS);
+                break;
+
+            case ATTRIBUTE(ALC_HRTF_SOFT)
+                if(attrList[attrIdx + 1] == ALC_FALSE)
+                    opthrtf = false;
+                else if(attrList[attrIdx + 1] == ALC_TRUE)
+                    opthrtf = true;
+                else if(attrList[attrIdx + 1] == ALC_DONT_CARE_SOFT)
+                    opthrtf = al::nullopt;
+                break;
+
+            case ATTRIBUTE(ALC_HRTF_ID_SOFT)
+                hrtf_id = attrList[attrIdx + 1];
+                break;
+
+            case ATTRIBUTE(ALC_OUTPUT_LIMITER_SOFT)
+                if(attrList[attrIdx + 1] == ALC_FALSE)
+                    optlimit = false;
+                else if(attrList[attrIdx + 1] == ALC_TRUE)
+                    optlimit = true;
+                else if(attrList[attrIdx + 1] == ALC_DONT_CARE_SOFT)
+                    optlimit = al::nullopt;
+                break;
+
+            case ATTRIBUTE(ALC_OUTPUT_MODE_SOFT)
+                outmode = attrList[attrIdx + 1];
+                break;
+
+            default:
+                TRACE("0x%04X = %d (0x%x)\n", attrList[attrIdx],
+                    attrList[attrIdx + 1], attrList[attrIdx + 1]);
+                break;
+            }
+
+            attrIdx += 2;
+        }
+#undef ATTRIBUTE
+
+        if(device->Type == DeviceType::Loopback)
+        {
+            if(!optchans || !opttype)
+                return ALC_INVALID_VALUE;
+            if(freqAttr < MIN_OUTPUT_RATE || freqAttr > MAX_OUTPUT_RATE)
+                return ALC_INVALID_VALUE;
+            if(*optchans == DevFmtAmbi3D)
+            {
+                if(!optlayout || !optscale)
+                    return ALC_INVALID_VALUE;
+                if(aorder < 1 || aorder > MaxAmbiOrder)
+                    return ALC_INVALID_VALUE;
+                if((*optlayout == DevAmbiLayout::FuMa || *optscale == DevAmbiScaling::FuMa)
+                    && aorder > 3)
+                    return ALC_INVALID_VALUE;
+            }
+            else if(*optchans == DevFmtStereo)
+            {
+                if(opthrtf)
+                {
+                    if(*opthrtf)
+                        stereomode = StereoEncoding::Hrtf;
+                    else
+                    {
+                        if(stereomode.value_or(StereoEncoding::Hrtf) == StereoEncoding::Hrtf)
+                            stereomode = StereoEncoding::Default;
+                    }
+                }
+
+                if(outmode == ALC_STEREO_BASIC_SOFT)
+                    stereomode = StereoEncoding::Basic;
+                else if(outmode == ALC_STEREO_UHJ_SOFT)
+                    stereomode = StereoEncoding::Uhj;
+                else if(outmode == ALC_STEREO_HRTF_SOFT)
+                    stereomode = StereoEncoding::Hrtf;
+            }
+
+            optsrate = static_cast<uint>(freqAttr);
+        }
+        else
+        {
+            if(opthrtf)
+            {
+                if(*opthrtf)
+                    stereomode = StereoEncoding::Hrtf;
+                else
+                {
+                    if(stereomode.value_or(StereoEncoding::Hrtf) == StereoEncoding::Hrtf)
+                        stereomode = StereoEncoding::Default;
+                }
+            }
+
+            if(outmode != ALC_ANY_SOFT)
+            {
+                using OutputMode = ALCdevice::OutputMode;
+                switch(OutputMode(outmode))
+                {
+                case OutputMode::Any: break;
+                case OutputMode::Mono: optchans = DevFmtMono; break;
+                case OutputMode::Stereo: optchans = DevFmtStereo; break;
+                case OutputMode::StereoBasic:
+                    optchans = DevFmtStereo;
+                    stereomode = StereoEncoding::Basic;
+                    break;
+                case OutputMode::Uhj2:
+                    optchans = DevFmtStereo;
+                    stereomode = StereoEncoding::Uhj;
+                    break;
+                case OutputMode::Hrtf:
+                    optchans = DevFmtStereo;
+                    stereomode = StereoEncoding::Hrtf;
+                    break;
+                case OutputMode::Quad: optchans = DevFmtQuad; break;
+                case OutputMode::X51: optchans = DevFmtX51; break;
+                case OutputMode::X61: optchans = DevFmtX61; break;
+                case OutputMode::X71: optchans = DevFmtX71; break;
+                }
+            }
+
+            if(freqAttr)
+            {
+                uint oldrate = optsrate.value_or(DEFAULT_OUTPUT_RATE);
+                freqAttr = clampi(freqAttr, MIN_OUTPUT_RATE, MAX_OUTPUT_RATE);
+
+                const double scale{static_cast<double>(freqAttr) / oldrate};
+                period_size = static_cast<uint>(period_size*scale + 0.5);
+                buffer_size = static_cast<uint>(buffer_size*scale + 0.5);
+                optsrate = static_cast<uint>(freqAttr);
+            }
+        }
+
+        /* If a context is already running on the device, stop playback so the
+         * device attributes can be updated.
+         */
+        if(device->Flags.test(DeviceRunning))
+            device->Backend->stop();
+        device->Flags.reset(DeviceRunning);
+
+        UpdateClockBase(device);
+    }
+
+    if(device->Flags.test(DeviceRunning))
+        return ALC_NO_ERROR;
+
+    device->AvgSpeakerDist = 0.0f;
+    device->mNFCtrlFilter = NfcFilter{};
+    device->mUhjEncoder = nullptr;
+    device->AmbiDecoder = nullptr;
+    device->Bs2b = nullptr;
+    device->PostProcess = nullptr;
+
+    device->Limiter = nullptr;
+    device->ChannelDelays = nullptr;
+
+    std::fill(std::begin(device->HrtfAccumData), std::end(device->HrtfAccumData), float2{});
+
+    device->Dry.AmbiMap.fill(BFChannelConfig{});
+    device->Dry.Buffer = {};
+    std::fill(std::begin(device->NumChannelsPerOrder), std::end(device->NumChannelsPerOrder), 0u);
+    device->RealOut.RemixMap = {};
+    device->RealOut.ChannelIndex.fill(InvalidChannelIndex);
+    device->RealOut.Buffer = {};
+    device->MixBuffer.clear();
+    device->MixBuffer.shrink_to_fit();
+
+    UpdateClockBase(device);
+    device->FixedLatency = nanoseconds::zero();
+
+    device->DitherDepth = 0.0f;
+    device->DitherSeed = DitherRNGSeed;
+
+    device->mHrtfStatus = ALC_HRTF_DISABLED_SOFT;
+
+    /*************************************************************************
+     * Update device format request
+     */
+
+    if(device->Type == DeviceType::Loopback)
+    {
+        device->Frequency = *optsrate;
+        device->FmtChans = *optchans;
+        device->FmtType = *opttype;
+        if(device->FmtChans == DevFmtAmbi3D)
+        {
+            device->mAmbiOrder = aorder;
+            device->mAmbiLayout = *optlayout;
+            device->mAmbiScale = *optscale;
+        }
+        device->Flags.set(FrequencyRequest).set(ChannelsRequest).set(SampleTypeRequest);
+    }
+    else
+    {
+        device->FmtType = opttype.value_or(DevFmtTypeDefault);
+        device->FmtChans = optchans.value_or(DevFmtChannelsDefault);
+        device->mAmbiOrder = 0;
+        device->BufferSize = buffer_size;
+        device->UpdateSize = period_size;
+        device->Frequency = optsrate.value_or(DEFAULT_OUTPUT_RATE);
+        device->Flags.set(FrequencyRequest, optsrate.has_value())
+            .set(ChannelsRequest, optchans.has_value())
+            .set(SampleTypeRequest, opttype.has_value());
+
+        if(device->FmtChans == DevFmtAmbi3D)
+        {
+            device->mAmbiOrder = clampu(aorder, 1, MaxAmbiOrder);
+            device->mAmbiLayout = optlayout.value_or(DevAmbiLayout::Default);
+            device->mAmbiScale = optscale.value_or(DevAmbiScaling::Default);
+            if(device->mAmbiOrder > 3
+                && (device->mAmbiLayout == DevAmbiLayout::FuMa
+                    || device->mAmbiScale == DevAmbiScaling::FuMa))
+            {
+                ERR("FuMa is incompatible with %d%s order ambisonics (up to 3rd order only)\n",
+                    device->mAmbiOrder,
+                    (((device->mAmbiOrder%100)/10) == 1) ? "th" :
+                    ((device->mAmbiOrder%10) == 1) ? "st" :
+                    ((device->mAmbiOrder%10) == 2) ? "nd" :
+                    ((device->mAmbiOrder%10) == 3) ? "rd" : "th");
+                device->mAmbiOrder = 3;
+            }
+        }
+    }
+
+    TRACE("Pre-reset: %s%s, %s%s, %s%uhz, %u / %u buffer\n",
+        device->Flags.test(ChannelsRequest)?"*":"", DevFmtChannelsString(device->FmtChans),
+        device->Flags.test(SampleTypeRequest)?"*":"", DevFmtTypeString(device->FmtType),
+        device->Flags.test(FrequencyRequest)?"*":"", device->Frequency,
+        device->UpdateSize, device->BufferSize);
+
+    const uint oldFreq{device->Frequency};
+    const DevFmtChannels oldChans{device->FmtChans};
+    const DevFmtType oldType{device->FmtType};
+    try {
+        auto backend = device->Backend.get();
+        if(!backend->reset())
+            throw al::backend_exception{al::backend_error::DeviceError, "Device reset failure"};
+    }
+    catch(std::exception &e) {
+        ERR("Device error: %s\n", e.what());
+        device->handleDisconnect("%s", e.what());
+        return ALC_INVALID_DEVICE;
+    }
+
+    if(device->FmtChans != oldChans && device->Flags.test(ChannelsRequest))
+    {
+        ERR("Failed to set %s, got %s instead\n", DevFmtChannelsString(oldChans),
+            DevFmtChannelsString(device->FmtChans));
+        device->Flags.reset(ChannelsRequest);
+    }
+    if(device->FmtType != oldType && device->Flags.test(SampleTypeRequest))
+    {
+        ERR("Failed to set %s, got %s instead\n", DevFmtTypeString(oldType),
+            DevFmtTypeString(device->FmtType));
+        device->Flags.reset(SampleTypeRequest);
+    }
+    if(device->Frequency != oldFreq && device->Flags.test(FrequencyRequest))
+    {
+        WARN("Failed to set %uhz, got %uhz instead\n", oldFreq, device->Frequency);
+        device->Flags.reset(FrequencyRequest);
+    }
+
+    TRACE("Post-reset: %s, %s, %uhz, %u / %u buffer\n",
+        DevFmtChannelsString(device->FmtChans), DevFmtTypeString(device->FmtType),
+        device->Frequency, device->UpdateSize, device->BufferSize);
+
+    if(device->Type != DeviceType::Loopback)
+    {
+        if(auto modeopt = device->configValue<std::string>(nullptr, "stereo-mode"))
+        {
+            const char *mode{modeopt->c_str()};
+            if(al::strcasecmp(mode, "headphones") == 0)
+                device->Flags.set(DirectEar);
+            else if(al::strcasecmp(mode, "speakers") == 0)
+                device->Flags.reset(DirectEar);
+            else if(al::strcasecmp(mode, "auto") != 0)
+                ERR("Unexpected stereo-mode: %s\n", mode);
+        }
+    }
+
+    aluInitRenderer(device, hrtf_id, stereomode);
+
+    /* Calculate the max number of sources, and split them between the mono and
+     * stereo count given the requested number of stereo sources.
+     */
+    if(auto srcsopt = device->configValue<uint>(nullptr, "sources"))
+    {
+        if(*srcsopt <= 0) numMono = 256;
+        else numMono = maxu(*srcsopt, 16);
+    }
+    else
+    {
+        numMono = minu(numMono, INT_MAX-numStereo);
+        numMono = maxu(numMono+numStereo, 256);
+    }
+    numStereo = minu(numStereo, numMono);
+    numMono -= numStereo;
+    device->SourcesMax = numMono + numStereo;
+    device->NumMonoSources = numMono;
+    device->NumStereoSources = numStereo;
+
+    if(auto sendsopt = device->configValue<int>(nullptr, "sends"))
+        numSends = minu(numSends, static_cast<uint>(clampi(*sendsopt, 0, MAX_SENDS)));
+    device->NumAuxSends = numSends;
+
+    TRACE("Max sources: %d (%d + %d), effect slots: %d, sends: %d\n",
+        device->SourcesMax, device->NumMonoSources, device->NumStereoSources,
+        device->AuxiliaryEffectSlotMax, device->NumAuxSends);
+
+    switch(device->FmtChans)
+    {
+    case DevFmtMono: break;
+    case DevFmtStereo:
+        if(!device->mUhjEncoder)
+            device->RealOut.RemixMap = StereoDownmix;
+        break;
+    case DevFmtQuad: device->RealOut.RemixMap = QuadDownmix; break;
+    case DevFmtX51: device->RealOut.RemixMap = X51Downmix; break;
+    case DevFmtX61: device->RealOut.RemixMap = X61Downmix; break;
+    case DevFmtX71: device->RealOut.RemixMap = X71Downmix; break;
+    case DevFmtX714: device->RealOut.RemixMap = X71Downmix; break;
+    case DevFmtX3D71: device->RealOut.RemixMap = X51Downmix; break;
+    case DevFmtAmbi3D: break;
+    }
+
+    nanoseconds::rep sample_delay{0};
+    if(auto *encoder{device->mUhjEncoder.get()})
+        sample_delay += encoder->getDelay();
+
+    if(device->getConfigValueBool(nullptr, "dither", true))
+    {
+        int depth{device->configValue<int>(nullptr, "dither-depth").value_or(0)};
+        if(depth <= 0)
+        {
+            switch(device->FmtType)
+            {
+            case DevFmtByte:
+            case DevFmtUByte:
+                depth = 8;
+                break;
+            case DevFmtShort:
+            case DevFmtUShort:
+                depth = 16;
+                break;
+            case DevFmtInt:
+            case DevFmtUInt:
+            case DevFmtFloat:
+                break;
+            }
+        }
+
+        if(depth > 0)
+        {
+            depth = clampi(depth, 2, 24);
+            device->DitherDepth = std::pow(2.0f, static_cast<float>(depth-1));
+        }
+    }
+    if(!(device->DitherDepth > 0.0f))
+        TRACE("Dithering disabled\n");
+    else
+        TRACE("Dithering enabled (%d-bit, %g)\n", float2int(std::log2(device->DitherDepth)+0.5f)+1,
+              device->DitherDepth);
+
+    if(!optlimit)
+        optlimit = device->configValue<bool>(nullptr, "output-limiter");
+
+    /* If the gain limiter is unset, use the limiter for integer-based output
+     * (where samples must be clamped), and don't for floating-point (which can
+     * take unclamped samples).
+     */
+    if(!optlimit)
+    {
+        switch(device->FmtType)
+        {
+        case DevFmtByte:
+        case DevFmtUByte:
+        case DevFmtShort:
+        case DevFmtUShort:
+        case DevFmtInt:
+        case DevFmtUInt:
+            optlimit = true;
+            break;
+        case DevFmtFloat:
+            break;
+        }
+    }
+    if(optlimit.value_or(false) == false)
+        TRACE("Output limiter disabled\n");
+    else
+    {
+        float thrshld{1.0f};
+        switch(device->FmtType)
+        {
+        case DevFmtByte:
+        case DevFmtUByte:
+            thrshld = 127.0f / 128.0f;
+            break;
+        case DevFmtShort:
+        case DevFmtUShort:
+            thrshld = 32767.0f / 32768.0f;
+            break;
+        case DevFmtInt:
+        case DevFmtUInt:
+        case DevFmtFloat:
+            break;
+        }
+        if(device->DitherDepth > 0.0f)
+            thrshld -= 1.0f / device->DitherDepth;
+
+        const float thrshld_dB{std::log10(thrshld) * 20.0f};
+        auto limiter = CreateDeviceLimiter(device, thrshld_dB);
+
+        sample_delay += limiter->getLookAhead();
+        device->Limiter = std::move(limiter);
+        TRACE("Output limiter enabled, %.4fdB limit\n", thrshld_dB);
+    }
+
+    /* Convert the sample delay from samples to nanosamples to nanoseconds. */
+    device->FixedLatency += nanoseconds{seconds{sample_delay}} / device->Frequency;
+    TRACE("Fixed device latency: %" PRId64 "ns\n", int64_t{device->FixedLatency.count()});
+
+    FPUCtl mixer_mode{};
+    for(ContextBase *ctxbase : *device->mContexts.load())
+    {
+        auto *context = static_cast<ALCcontext*>(ctxbase);
+
+        std::unique_lock<std::mutex> proplock{context->mPropLock};
+        std::unique_lock<std::mutex> slotlock{context->mEffectSlotLock};
+
+        /* Clear out unused effect slot clusters. */
+        auto slot_cluster_not_in_use = [](ContextBase::EffectSlotCluster &cluster)
+        {
+            for(size_t i{0};i < ContextBase::EffectSlotClusterSize;++i)
+            {
+                if(cluster[i].InUse)
+                    return false;
+            }
+            return true;
+        };
+        auto slotcluster_iter = std::remove_if(context->mEffectSlotClusters.begin(),
+            context->mEffectSlotClusters.end(), slot_cluster_not_in_use);
+        context->mEffectSlotClusters.erase(slotcluster_iter, context->mEffectSlotClusters.end());
+
+        /* Free all wet buffers. Any in use will be reallocated with an updated
+         * configuration in aluInitEffectPanning.
+         */
+        for(auto&& slots : context->mEffectSlotClusters)
+        {
+            for(size_t i{0};i < ContextBase::EffectSlotClusterSize;++i)
+            {
+                slots[i].mWetBuffer.clear();
+                slots[i].mWetBuffer.shrink_to_fit();
+                slots[i].Wet.Buffer = {};
+            }
+        }
+
+        if(ALeffectslot *slot{context->mDefaultSlot.get()})
+        {
+            aluInitEffectPanning(slot->mSlot, context);
+
+            EffectState *state{slot->Effect.State.get()};
+            state->mOutTarget = device->Dry.Buffer;
+            state->deviceUpdate(device, slot->Buffer);
+            slot->updateProps(context);
+        }
+
+        if(EffectSlotArray *curarray{context->mActiveAuxSlots.load(std::memory_order_relaxed)})
+            std::fill_n(curarray->end(), curarray->size(), nullptr);
+        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);
+
+                aluInitEffectPanning(slot->mSlot, context);
+
+                EffectState *state{slot->Effect.State.get()};
+                state->mOutTarget = device->Dry.Buffer;
+                state->deviceUpdate(device, slot->Buffer);
+                slot->updateProps(context);
+            }
+        }
+        slotlock.unlock();
+
+        const uint num_sends{device->NumAuxSends};
+        std::unique_lock<std::mutex> srclock{context->mSourceLock};
+        for(auto &sublist : context->mSourceList)
+        {
+            uint64_t usemask{~sublist.FreeMask};
+            while(usemask)
+            {
+                const int idx{al::countr_zero(usemask)};
+                ALsource *source{sublist.Sources + idx};
+                usemask &= ~(1_u64 << idx);
+
+                auto clear_send = [](ALsource::SendData &send) -> void
+                {
+                    if(send.Slot)
+                        DecrementRef(send.Slot->ref);
+                    send.Slot = nullptr;
+                    send.Gain = 1.0f;
+                    send.GainHF = 1.0f;
+                    send.HFReference = LOWPASSFREQREF;
+                    send.GainLF = 1.0f;
+                    send.LFReference = HIGHPASSFREQREF;
+                };
+                auto send_begin = source->Send.begin() + static_cast<ptrdiff_t>(num_sends);
+                std::for_each(send_begin, source->Send.end(), clear_send);
+
+                source->mPropsDirty = true;
+            }
+        }
+
+        auto voicelist = context->getVoicesSpan();
+        for(Voice *voice : voicelist)
+        {
+            /* Clear extraneous property set sends. */
+            std::fill(std::begin(voice->mProps.Send)+num_sends, std::end(voice->mProps.Send),
+                VoiceProps::SendData{});
+
+            std::fill(voice->mSend.begin()+num_sends, voice->mSend.end(), Voice::TargetData{});
+            for(auto &chandata : voice->mChans)
+            {
+                std::fill(chandata.mWetParams.begin()+num_sends, chandata.mWetParams.end(),
+                    SendParams{});
+            }
+
+            if(VoicePropsItem *props{voice->mUpdate.exchange(nullptr, std::memory_order_relaxed)})
+                AtomicReplaceHead(context->mFreeVoiceProps, props);
+
+            /* Force the voice to stopped if it was stopping. */
+            Voice::State vstate{Voice::Stopping};
+            voice->mPlayState.compare_exchange_strong(vstate, Voice::Stopped,
+                std::memory_order_acquire, std::memory_order_acquire);
+            if(voice->mSourceID.load(std::memory_order_relaxed) == 0u)
+                continue;
+
+            voice->prepare(device);
+        }
+        /* Clear all voice props to let them get allocated again. */
+        context->mVoicePropClusters.clear();
+        context->mFreeVoiceProps.store(nullptr, std::memory_order_relaxed);
+        srclock.unlock();
+
+        context->mPropsDirty = false;
+        UpdateContextProps(context);
+        UpdateAllSourceProps(context);
+    }
+    mixer_mode.leave();
+
+    if(!device->Flags.test(DevicePaused))
+    {
+        try {
+            auto backend = device->Backend.get();
+            backend->start();
+            device->Flags.set(DeviceRunning);
+        }
+        catch(al::backend_exception& e) {
+            ERR("%s\n", e.what());
+            device->handleDisconnect("%s", e.what());
+            return ALC_INVALID_DEVICE;
+        }
+        TRACE("Post-start: %s, %s, %uhz, %u / %u buffer\n",
+            DevFmtChannelsString(device->FmtChans), DevFmtTypeString(device->FmtType),
+            device->Frequency, device->UpdateSize, device->BufferSize);
+    }
+
+    return ALC_NO_ERROR;
+}
+
+/**
+ * Updates device parameters as above, and also first clears the disconnected
+ * status, if set.
+ */
+bool ResetDeviceParams(ALCdevice *device, const int *attrList)
+{
+    /* If the device was disconnected, reset it since we're opened anew. */
+    if(!device->Connected.load(std::memory_order_relaxed)) UNLIKELY
+    {
+        /* Make sure disconnection is finished before continuing on. */
+        device->waitForMix();
+
+        for(ContextBase *ctxbase : *device->mContexts.load(std::memory_order_acquire))
+        {
+            auto *ctx = static_cast<ALCcontext*>(ctxbase);
+            if(!ctx->mStopVoicesOnDisconnect.load(std::memory_order_acquire))
+                continue;
+
+            /* Clear any pending voice changes and reallocate voices to get a
+             * clean restart.
+             */
+            std::lock_guard<std::mutex> __{ctx->mSourceLock};
+            auto *vchg = ctx->mCurrentVoiceChange.load(std::memory_order_acquire);
+            while(auto *next = vchg->mNext.load(std::memory_order_acquire))
+                vchg = next;
+            ctx->mCurrentVoiceChange.store(vchg, std::memory_order_release);
+
+            ctx->mVoicePropClusters.clear();
+            ctx->mFreeVoiceProps.store(nullptr, std::memory_order_relaxed);
+
+            ctx->mVoiceClusters.clear();
+            ctx->allocVoices(std::max<size_t>(256,
+                ctx->mActiveVoiceCount.load(std::memory_order_relaxed)));
+        }
+
+        device->Connected.store(true);
+    }
+
+    ALCenum err{UpdateDeviceParams(device, attrList)};
+    if(err == ALC_NO_ERROR) LIKELY return ALC_TRUE;
+
+    alcSetError(device, err);
+    return ALC_FALSE;
+}
+
+
+/** Checks if the device handle is valid, and returns a new reference if so. */
+DeviceRef VerifyDevice(ALCdevice *device)
+{
+    std::lock_guard<std::recursive_mutex> _{ListLock};
+    auto iter = std::lower_bound(DeviceList.begin(), DeviceList.end(), device);
+    if(iter != DeviceList.end() && *iter == device)
+    {
+        (*iter)->add_ref();
+        return DeviceRef{*iter};
+    }
+    return nullptr;
+}
+
+
+/**
+ * Checks if the given context is valid, returning a new reference to it if so.
+ */
+ContextRef VerifyContext(ALCcontext *context)
+{
+    std::lock_guard<std::recursive_mutex> _{ListLock};
+    auto iter = std::lower_bound(ContextList.begin(), ContextList.end(), context);
+    if(iter != ContextList.end() && *iter == context)
+    {
+        (*iter)->add_ref();
+        return ContextRef{*iter};
+    }
+    return nullptr;
+}
+
+} // namespace
+
+/** Returns a new reference to the currently active context for this thread. */
+ContextRef GetContextRef(void)
+{
+    ALCcontext *context{ALCcontext::getThreadContext()};
+    if(context)
+        context->add_ref();
+    else
+    {
+        while(ALCcontext::sGlobalContextLock.exchange(true, std::memory_order_acquire)) {
+            /* Wait to make sure another thread isn't trying to change the
+             * current context and bring its refcount to 0.
+             */
+        }
+        context = ALCcontext::sGlobalContext.load(std::memory_order_acquire);
+        if(context) LIKELY context->add_ref();
+        ALCcontext::sGlobalContextLock.store(false, std::memory_order_release);
+    }
+    return ContextRef{context};
+}
+
+
+/************************************************
+ * Standard ALC functions
+ ************************************************/
+
+ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(dev) return dev->LastError.exchange(ALC_NO_ERROR);
+    return LastNullDeviceError.exchange(ALC_NO_ERROR);
+}
+END_API_FUNC
+
+
+ALC_API void ALC_APIENTRY alcSuspendContext(ALCcontext *context)
+START_API_FUNC
+{
+    if(!SuspendDefers)
+        return;
+
+    ContextRef ctx{VerifyContext(context)};
+    if(!ctx)
+        alcSetError(nullptr, ALC_INVALID_CONTEXT);
+    else
+    {
+        std::lock_guard<std::mutex> _{ctx->mPropLock};
+        ctx->deferUpdates();
+    }
+}
+END_API_FUNC
+
+ALC_API void ALC_APIENTRY alcProcessContext(ALCcontext *context)
+START_API_FUNC
+{
+    if(!SuspendDefers)
+        return;
+
+    ContextRef ctx{VerifyContext(context)};
+    if(!ctx)
+        alcSetError(nullptr, ALC_INVALID_CONTEXT);
+    else
+    {
+        std::lock_guard<std::mutex> _{ctx->mPropLock};
+        ctx->processUpdates();
+    }
+}
+END_API_FUNC
+
+
+ALC_API const ALCchar* ALC_APIENTRY alcGetString(ALCdevice *Device, ALCenum param)
+START_API_FUNC
+{
+    const ALCchar *value{nullptr};
+
+    switch(param)
+    {
+    case ALC_NO_ERROR:
+        value = alcNoError;
+        break;
+
+    case ALC_INVALID_ENUM:
+        value = alcErrInvalidEnum;
+        break;
+
+    case ALC_INVALID_VALUE:
+        value = alcErrInvalidValue;
+        break;
+
+    case ALC_INVALID_DEVICE:
+        value = alcErrInvalidDevice;
+        break;
+
+    case ALC_INVALID_CONTEXT:
+        value = alcErrInvalidContext;
+        break;
+
+    case ALC_OUT_OF_MEMORY:
+        value = alcErrOutOfMemory;
+        break;
+
+    case ALC_DEVICE_SPECIFIER:
+        value = alcDefaultName;
+        break;
+
+    case ALC_ALL_DEVICES_SPECIFIER:
+        if(DeviceRef dev{VerifyDevice(Device)})
+        {
+            if(dev->Type == DeviceType::Capture)
+                alcSetError(dev.get(), ALC_INVALID_ENUM);
+            else if(dev->Type == DeviceType::Loopback)
+                value = alcDefaultName;
+            else
+            {
+                std::lock_guard<std::mutex> _{dev->StateLock};
+                value = dev->DeviceName.c_str();
+            }
+        }
+        else
+        {
+            ProbeAllDevicesList();
+            value = alcAllDevicesList.c_str();
+        }
+        break;
+
+    case ALC_CAPTURE_DEVICE_SPECIFIER:
+        if(DeviceRef dev{VerifyDevice(Device)})
+        {
+            if(dev->Type != DeviceType::Capture)
+                alcSetError(dev.get(), ALC_INVALID_ENUM);
+            else
+            {
+                std::lock_guard<std::mutex> _{dev->StateLock};
+                value = dev->DeviceName.c_str();
+            }
+        }
+        else
+        {
+            ProbeCaptureDeviceList();
+            value = alcCaptureDeviceList.c_str();
+        }
+        break;
+
+    /* Default devices are always first in the list */
+    case ALC_DEFAULT_DEVICE_SPECIFIER:
+        value = alcDefaultName;
+        break;
+
+    case ALC_DEFAULT_ALL_DEVICES_SPECIFIER:
+        if(alcAllDevicesList.empty())
+            ProbeAllDevicesList();
+
+        /* Copy first entry as default. */
+        alcDefaultAllDevicesSpecifier = alcAllDevicesList.c_str();
+        value = alcDefaultAllDevicesSpecifier.c_str();
+        break;
+
+    case ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER:
+        if(alcCaptureDeviceList.empty())
+            ProbeCaptureDeviceList();
+
+        /* Copy first entry as default. */
+        alcCaptureDefaultDeviceSpecifier = alcCaptureDeviceList.c_str();
+        value = alcCaptureDefaultDeviceSpecifier.c_str();
+        break;
+
+    case ALC_EXTENSIONS:
+        if(VerifyDevice(Device))
+            value = alcExtensionList;
+        else
+            value = alcNoDeviceExtList;
+        break;
+
+    case ALC_HRTF_SPECIFIER_SOFT:
+        if(DeviceRef dev{VerifyDevice(Device)})
+        {
+            std::lock_guard<std::mutex> _{dev->StateLock};
+            value = (dev->mHrtf ? dev->mHrtfName.c_str() : "");
+        }
+        else
+            alcSetError(nullptr, ALC_INVALID_DEVICE);
+        break;
+
+    default:
+        alcSetError(VerifyDevice(Device).get(), ALC_INVALID_ENUM);
+        break;
+    }
+
+    return value;
+}
+END_API_FUNC
+
+
+static size_t GetIntegerv(ALCdevice *device, ALCenum param, const al::span<int> values)
+{
+    size_t i;
+
+    if(values.empty())
+    {
+        alcSetError(device, ALC_INVALID_VALUE);
+        return 0;
+    }
+
+    if(!device)
+    {
+        switch(param)
+        {
+        case ALC_MAJOR_VERSION:
+            values[0] = alcMajorVersion;
+            return 1;
+        case ALC_MINOR_VERSION:
+            values[0] = alcMinorVersion;
+            return 1;
+
+        case ALC_EFX_MAJOR_VERSION:
+            values[0] = alcEFXMajorVersion;
+            return 1;
+        case ALC_EFX_MINOR_VERSION:
+            values[0] = alcEFXMinorVersion;
+            return 1;
+        case ALC_MAX_AUXILIARY_SENDS:
+            values[0] = MAX_SENDS;
+            return 1;
+
+        case ALC_ATTRIBUTES_SIZE:
+        case ALC_ALL_ATTRIBUTES:
+        case ALC_FREQUENCY:
+        case ALC_REFRESH:
+        case ALC_SYNC:
+        case ALC_MONO_SOURCES:
+        case ALC_STEREO_SOURCES:
+        case ALC_CAPTURE_SAMPLES:
+        case ALC_FORMAT_CHANNELS_SOFT:
+        case ALC_FORMAT_TYPE_SOFT:
+        case ALC_AMBISONIC_LAYOUT_SOFT:
+        case ALC_AMBISONIC_SCALING_SOFT:
+        case ALC_AMBISONIC_ORDER_SOFT:
+        case ALC_MAX_AMBISONIC_ORDER_SOFT:
+            alcSetError(nullptr, ALC_INVALID_DEVICE);
+            return 0;
+
+        default:
+            alcSetError(nullptr, ALC_INVALID_ENUM);
+        }
+        return 0;
+    }
+
+    std::lock_guard<std::mutex> _{device->StateLock};
+    if(device->Type == DeviceType::Capture)
+    {
+        static constexpr int MaxCaptureAttributes{9};
+        switch(param)
+        {
+        case ALC_ATTRIBUTES_SIZE:
+            values[0] = MaxCaptureAttributes;
+            return 1;
+        case ALC_ALL_ATTRIBUTES:
+            i = 0;
+            if(values.size() < MaxCaptureAttributes)
+                alcSetError(device, ALC_INVALID_VALUE);
+            else
+            {
+                values[i++] = ALC_MAJOR_VERSION;
+                values[i++] = alcMajorVersion;
+                values[i++] = ALC_MINOR_VERSION;
+                values[i++] = alcMinorVersion;
+                values[i++] = ALC_CAPTURE_SAMPLES;
+                values[i++] = static_cast<int>(device->Backend->availableSamples());
+                values[i++] = ALC_CONNECTED;
+                values[i++] = device->Connected.load(std::memory_order_relaxed);
+                values[i++] = 0;
+                assert(i == MaxCaptureAttributes);
+            }
+            return i;
+
+        case ALC_MAJOR_VERSION:
+            values[0] = alcMajorVersion;
+            return 1;
+        case ALC_MINOR_VERSION:
+            values[0] = alcMinorVersion;
+            return 1;
+
+        case ALC_CAPTURE_SAMPLES:
+            values[0] = static_cast<int>(device->Backend->availableSamples());
+            return 1;
+
+        case ALC_CONNECTED:
+            values[0] = device->Connected.load(std::memory_order_acquire);
+            return 1;
+
+        default:
+            alcSetError(device, ALC_INVALID_ENUM);
+        }
+        return 0;
+    }
+
+    /* render device */
+    auto NumAttrsForDevice = [](ALCdevice *aldev) noexcept
+    {
+        if(aldev->Type == DeviceType::Loopback && aldev->FmtChans == DevFmtAmbi3D)
+            return 37;
+        return 31;
+    };
+    switch(param)
+    {
+    case ALC_ATTRIBUTES_SIZE:
+        values[0] = NumAttrsForDevice(device);
+        return 1;
+
+    case ALC_ALL_ATTRIBUTES:
+        i = 0;
+        if(values.size() < static_cast<size_t>(NumAttrsForDevice(device)))
+            alcSetError(device, ALC_INVALID_VALUE);
+        else
+        {
+            values[i++] = ALC_MAJOR_VERSION;
+            values[i++] = alcMajorVersion;
+            values[i++] = ALC_MINOR_VERSION;
+            values[i++] = alcMinorVersion;
+            values[i++] = ALC_EFX_MAJOR_VERSION;
+            values[i++] = alcEFXMajorVersion;
+            values[i++] = ALC_EFX_MINOR_VERSION;
+            values[i++] = alcEFXMinorVersion;
+
+            values[i++] = ALC_FREQUENCY;
+            values[i++] = static_cast<int>(device->Frequency);
+            if(device->Type != DeviceType::Loopback)
+            {
+                values[i++] = ALC_REFRESH;
+                values[i++] = static_cast<int>(device->Frequency / device->UpdateSize);
+
+                values[i++] = ALC_SYNC;
+                values[i++] = ALC_FALSE;
+            }
+            else
+            {
+                if(device->FmtChans == DevFmtAmbi3D)
+                {
+                    values[i++] = ALC_AMBISONIC_LAYOUT_SOFT;
+                    values[i++] = EnumFromDevAmbi(device->mAmbiLayout);
+
+                    values[i++] = ALC_AMBISONIC_SCALING_SOFT;
+                    values[i++] = EnumFromDevAmbi(device->mAmbiScale);
+
+                    values[i++] = ALC_AMBISONIC_ORDER_SOFT;
+                    values[i++] = static_cast<int>(device->mAmbiOrder);
+                }
+
+                values[i++] = ALC_FORMAT_CHANNELS_SOFT;
+                values[i++] = EnumFromDevFmt(device->FmtChans);
+
+                values[i++] = ALC_FORMAT_TYPE_SOFT;
+                values[i++] = EnumFromDevFmt(device->FmtType);
+            }
+
+            values[i++] = ALC_MONO_SOURCES;
+            values[i++] = static_cast<int>(device->NumMonoSources);
+
+            values[i++] = ALC_STEREO_SOURCES;
+            values[i++] = static_cast<int>(device->NumStereoSources);
+
+            values[i++] = ALC_MAX_AUXILIARY_SENDS;
+            values[i++] = static_cast<int>(device->NumAuxSends);
+
+            values[i++] = ALC_HRTF_SOFT;
+            values[i++] = (device->mHrtf ? ALC_TRUE : ALC_FALSE);
+
+            values[i++] = ALC_HRTF_STATUS_SOFT;
+            values[i++] = device->mHrtfStatus;
+
+            values[i++] = ALC_OUTPUT_LIMITER_SOFT;
+            values[i++] = device->Limiter ? ALC_TRUE : ALC_FALSE;
+
+            values[i++] = ALC_MAX_AMBISONIC_ORDER_SOFT;
+            values[i++] = MaxAmbiOrder;
+
+            values[i++] = ALC_OUTPUT_MODE_SOFT;
+            values[i++] = static_cast<ALCenum>(device->getOutputMode1());
+
+            values[i++] = 0;
+        }
+        return i;
+
+    case ALC_MAJOR_VERSION:
+        values[0] = alcMajorVersion;
+        return 1;
+
+    case ALC_MINOR_VERSION:
+        values[0] = alcMinorVersion;
+        return 1;
+
+    case ALC_EFX_MAJOR_VERSION:
+        values[0] = alcEFXMajorVersion;
+        return 1;
+
+    case ALC_EFX_MINOR_VERSION:
+        values[0] = alcEFXMinorVersion;
+        return 1;
+
+    case ALC_FREQUENCY:
+        values[0] = static_cast<int>(device->Frequency);
+        return 1;
+
+    case ALC_REFRESH:
+        if(device->Type == DeviceType::Loopback)
+        {
+            alcSetError(device, ALC_INVALID_DEVICE);
+            return 0;
+        }
+        values[0] = static_cast<int>(device->Frequency / device->UpdateSize);
+        return 1;
+
+    case ALC_SYNC:
+        if(device->Type == DeviceType::Loopback)
+        {
+            alcSetError(device, ALC_INVALID_DEVICE);
+            return 0;
+        }
+        values[0] = ALC_FALSE;
+        return 1;
+
+    case ALC_FORMAT_CHANNELS_SOFT:
+        if(device->Type != DeviceType::Loopback)
+        {
+            alcSetError(device, ALC_INVALID_DEVICE);
+            return 0;
+        }
+        values[0] = EnumFromDevFmt(device->FmtChans);
+        return 1;
+
+    case ALC_FORMAT_TYPE_SOFT:
+        if(device->Type != DeviceType::Loopback)
+        {
+            alcSetError(device, ALC_INVALID_DEVICE);
+            return 0;
+        }
+        values[0] = EnumFromDevFmt(device->FmtType);
+        return 1;
+
+    case ALC_AMBISONIC_LAYOUT_SOFT:
+        if(device->Type != DeviceType::Loopback || device->FmtChans != DevFmtAmbi3D)
+        {
+            alcSetError(device, ALC_INVALID_DEVICE);
+            return 0;
+        }
+        values[0] = EnumFromDevAmbi(device->mAmbiLayout);
+        return 1;
+
+    case ALC_AMBISONIC_SCALING_SOFT:
+        if(device->Type != DeviceType::Loopback || device->FmtChans != DevFmtAmbi3D)
+        {
+            alcSetError(device, ALC_INVALID_DEVICE);
+            return 0;
+        }
+        values[0] = EnumFromDevAmbi(device->mAmbiScale);
+        return 1;
+
+    case ALC_AMBISONIC_ORDER_SOFT:
+        if(device->Type != DeviceType::Loopback || device->FmtChans != DevFmtAmbi3D)
+        {
+            alcSetError(device, ALC_INVALID_DEVICE);
+            return 0;
+        }
+        values[0] = static_cast<int>(device->mAmbiOrder);
+        return 1;
+
+    case ALC_MONO_SOURCES:
+        values[0] = static_cast<int>(device->NumMonoSources);
+        return 1;
+
+    case ALC_STEREO_SOURCES:
+        values[0] = static_cast<int>(device->NumStereoSources);
+        return 1;
+
+    case ALC_MAX_AUXILIARY_SENDS:
+        values[0] = static_cast<int>(device->NumAuxSends);
+        return 1;
+
+    case ALC_CONNECTED:
+        values[0] = device->Connected.load(std::memory_order_acquire);
+        return 1;
+
+    case ALC_HRTF_SOFT:
+        values[0] = (device->mHrtf ? ALC_TRUE : ALC_FALSE);
+        return 1;
+
+    case ALC_HRTF_STATUS_SOFT:
+        values[0] = device->mHrtfStatus;
+        return 1;
+
+    case ALC_NUM_HRTF_SPECIFIERS_SOFT:
+        device->enumerateHrtfs();
+        values[0] = static_cast<int>(minz(device->mHrtfList.size(),
+            std::numeric_limits<int>::max()));
+        return 1;
+
+    case ALC_OUTPUT_LIMITER_SOFT:
+        values[0] = device->Limiter ? ALC_TRUE : ALC_FALSE;
+        return 1;
+
+    case ALC_MAX_AMBISONIC_ORDER_SOFT:
+        values[0] = MaxAmbiOrder;
+        return 1;
+
+    case ALC_OUTPUT_MODE_SOFT:
+        values[0] = static_cast<ALCenum>(device->getOutputMode1());
+        return 1;
+
+    default:
+        alcSetError(device, ALC_INVALID_ENUM);
+    }
+    return 0;
+}
+
+ALC_API void ALC_APIENTRY alcGetIntegerv(ALCdevice *device, ALCenum param, ALCsizei size, ALCint *values)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(size <= 0 || values == nullptr)
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+    else
+        GetIntegerv(dev.get(), param, {values, static_cast<uint>(size)});
+}
+END_API_FUNC
+
+ALC_API void ALC_APIENTRY alcGetInteger64vSOFT(ALCdevice *device, ALCenum pname, ALCsizei size, ALCint64SOFT *values)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(size <= 0 || values == nullptr)
+    {
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+        return;
+    }
+    if(!dev || dev->Type == DeviceType::Capture)
+    {
+        auto ivals = al::vector<int>(static_cast<uint>(size));
+        if(size_t got{GetIntegerv(dev.get(), pname, ivals)})
+            std::copy_n(ivals.begin(), got, values);
+        return;
+    }
+    /* render device */
+    auto NumAttrsForDevice = [](ALCdevice *aldev) noexcept
+    {
+        if(aldev->Type == DeviceType::Loopback && aldev->FmtChans == DevFmtAmbi3D)
+            return 41;
+        return 35;
+    };
+    std::lock_guard<std::mutex> _{dev->StateLock};
+    switch(pname)
+    {
+    case ALC_ATTRIBUTES_SIZE:
+        *values = NumAttrsForDevice(dev.get());
+        break;
+
+    case ALC_ALL_ATTRIBUTES:
+        if(size < NumAttrsForDevice(dev.get()))
+            alcSetError(dev.get(), ALC_INVALID_VALUE);
+        else
+        {
+            size_t i{0};
+            values[i++] = ALC_FREQUENCY;
+            values[i++] = dev->Frequency;
+
+            if(dev->Type != DeviceType::Loopback)
+            {
+                values[i++] = ALC_REFRESH;
+                values[i++] = dev->Frequency / dev->UpdateSize;
+
+                values[i++] = ALC_SYNC;
+                values[i++] = ALC_FALSE;
+            }
+            else
+            {
+                values[i++] = ALC_FORMAT_CHANNELS_SOFT;
+                values[i++] = EnumFromDevFmt(dev->FmtChans);
+
+                values[i++] = ALC_FORMAT_TYPE_SOFT;
+                values[i++] = EnumFromDevFmt(dev->FmtType);
+
+                if(dev->FmtChans == DevFmtAmbi3D)
+                {
+                    values[i++] = ALC_AMBISONIC_LAYOUT_SOFT;
+                    values[i++] = EnumFromDevAmbi(dev->mAmbiLayout);
+
+                    values[i++] = ALC_AMBISONIC_SCALING_SOFT;
+                    values[i++] = EnumFromDevAmbi(dev->mAmbiScale);
+
+                    values[i++] = ALC_AMBISONIC_ORDER_SOFT;
+                    values[i++] = dev->mAmbiOrder;
+                }
+            }
+
+            values[i++] = ALC_MONO_SOURCES;
+            values[i++] = dev->NumMonoSources;
+
+            values[i++] = ALC_STEREO_SOURCES;
+            values[i++] = dev->NumStereoSources;
+
+            values[i++] = ALC_MAX_AUXILIARY_SENDS;
+            values[i++] = dev->NumAuxSends;
+
+            values[i++] = ALC_HRTF_SOFT;
+            values[i++] = (dev->mHrtf ? ALC_TRUE : ALC_FALSE);
+
+            values[i++] = ALC_HRTF_STATUS_SOFT;
+            values[i++] = dev->mHrtfStatus;
+
+            values[i++] = ALC_OUTPUT_LIMITER_SOFT;
+            values[i++] = dev->Limiter ? ALC_TRUE : ALC_FALSE;
+
+            ClockLatency clock{GetClockLatency(dev.get(), dev->Backend.get())};
+            values[i++] = ALC_DEVICE_CLOCK_SOFT;
+            values[i++] = clock.ClockTime.count();
+
+            values[i++] = ALC_DEVICE_LATENCY_SOFT;
+            values[i++] = clock.Latency.count();
+
+            values[i++] = ALC_OUTPUT_MODE_SOFT;
+            values[i++] = static_cast<ALCenum>(device->getOutputMode1());
+
+            values[i++] = 0;
+        }
+        break;
+
+    case ALC_DEVICE_CLOCK_SOFT:
+        {
+            uint samplecount, refcount;
+            nanoseconds basecount;
+            do {
+                refcount = dev->waitForMix();
+                basecount = dev->ClockBase;
+                samplecount = dev->SamplesDone;
+            } while(refcount != ReadRef(dev->MixCount));
+            basecount += nanoseconds{seconds{samplecount}} / dev->Frequency;
+            *values = basecount.count();
+        }
+        break;
+
+    case ALC_DEVICE_LATENCY_SOFT:
+        *values = GetClockLatency(dev.get(), dev->Backend.get()).Latency.count();
+        break;
+
+    case ALC_DEVICE_CLOCK_LATENCY_SOFT:
+        if(size < 2)
+            alcSetError(dev.get(), ALC_INVALID_VALUE);
+        else
+        {
+            ClockLatency clock{GetClockLatency(dev.get(), dev->Backend.get())};
+            values[0] = clock.ClockTime.count();
+            values[1] = clock.Latency.count();
+        }
+        break;
+
+    default:
+        auto ivals = al::vector<int>(static_cast<uint>(size));
+        if(size_t got{GetIntegerv(dev.get(), pname, ivals)})
+            std::copy_n(ivals.begin(), got, values);
+        break;
+    }
+}
+END_API_FUNC
+
+
+ALC_API ALCboolean ALC_APIENTRY alcIsExtensionPresent(ALCdevice *device, const ALCchar *extName)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!extName)
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+    else
+    {
+        size_t len = strlen(extName);
+        const char *ptr = (dev ? alcExtensionList : alcNoDeviceExtList);
+        while(ptr && *ptr)
+        {
+            if(al::strncasecmp(ptr, extName, len) == 0 && (ptr[len] == '\0' || isspace(ptr[len])))
+                return ALC_TRUE;
+
+            if((ptr=strchr(ptr, ' ')) != nullptr)
+            {
+                do {
+                    ++ptr;
+                } while(isspace(*ptr));
+            }
+        }
+    }
+    return ALC_FALSE;
+}
+END_API_FUNC
+
+
+ALC_API ALCvoid* ALC_APIENTRY alcGetProcAddress(ALCdevice *device, const ALCchar *funcName)
+START_API_FUNC
+{
+    if(!funcName)
+    {
+        DeviceRef dev{VerifyDevice(device)};
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+        return nullptr;
+    }
+#ifdef ALSOFT_EAX
+    if(eax_g_is_enabled)
+    {
+        for(const auto &func : eaxFunctions)
+        {
+            if(strcmp(func.funcName, funcName) == 0)
+                return func.address;
+        }
+    }
+#endif
+    for(const auto &func : alcFunctions)
+    {
+        if(strcmp(func.funcName, funcName) == 0)
+            return func.address;
+    }
+    return nullptr;
+}
+END_API_FUNC
+
+
+ALC_API ALCenum ALC_APIENTRY alcGetEnumValue(ALCdevice *device, const ALCchar *enumName)
+START_API_FUNC
+{
+    if(!enumName)
+    {
+        DeviceRef dev{VerifyDevice(device)};
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+        return 0;
+    }
+#ifdef ALSOFT_EAX
+    if(eax_g_is_enabled)
+    {
+        for(const auto &enm : eaxEnumerations)
+        {
+            if(strcmp(enm.enumName, enumName) == 0)
+                return enm.value;
+        }
+    }
+#endif
+    for(const auto &enm : alcEnumerations)
+    {
+        if(strcmp(enm.enumName, enumName) == 0)
+            return enm.value;
+    }
+
+    return 0;
+}
+END_API_FUNC
+
+
+ALC_API ALCcontext* ALC_APIENTRY alcCreateContext(ALCdevice *device, const ALCint *attrList)
+START_API_FUNC
+{
+    /* Explicitly hold the list lock while taking the StateLock in case the
+     * device is asynchronously destroyed, to ensure this new context is
+     * properly cleaned up after being made.
+     */
+    std::unique_lock<std::recursive_mutex> listlock{ListLock};
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type == DeviceType::Capture || !dev->Connected.load(std::memory_order_relaxed))
+    {
+        listlock.unlock();
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        return nullptr;
+    }
+    std::unique_lock<std::mutex> statelock{dev->StateLock};
+    listlock.unlock();
+
+    dev->LastError.store(ALC_NO_ERROR);
+
+    ALCenum err{UpdateDeviceParams(dev.get(), attrList)};
+    if(err != ALC_NO_ERROR)
+    {
+        alcSetError(dev.get(), err);
+        return nullptr;
+    }
+
+    ContextRef context{new ALCcontext{dev}};
+    context->init();
+
+    if(auto volopt = dev->configValue<float>(nullptr, "volume-adjust"))
+    {
+        const float valf{*volopt};
+        if(!std::isfinite(valf))
+            ERR("volume-adjust must be finite: %f\n", valf);
+        else
+        {
+            const float db{clampf(valf, -24.0f, 24.0f)};
+            if(db != valf)
+                WARN("volume-adjust clamped: %f, range: +/-%f\n", valf, 24.0f);
+            context->mGainBoost = std::pow(10.0f, db/20.0f);
+            TRACE("volume-adjust gain: %f\n", context->mGainBoost);
+        }
+    }
+
+    {
+        using ContextArray = al::FlexArray<ContextBase*>;
+
+        /* Allocate a new context array, which holds 1 more than the current/
+         * old array.
+         */
+        auto *oldarray = device->mContexts.load();
+        const size_t newcount{oldarray->size()+1};
+        std::unique_ptr<ContextArray> newarray{ContextArray::Create(newcount)};
+
+        /* Copy the current/old context handles to the new array, appending the
+         * new context.
+         */
+        auto iter = std::copy(oldarray->begin(), oldarray->end(), newarray->begin());
+        *iter = context.get();
+
+        /* Store the new context array in the device. Wait for any current mix
+         * to finish before deleting the old array.
+         */
+        dev->mContexts.store(newarray.release());
+        if(oldarray != &DeviceBase::sEmptyContextArray)
+        {
+            dev->waitForMix();
+            delete oldarray;
+        }
+    }
+    statelock.unlock();
+
+    {
+        std::lock_guard<std::recursive_mutex> _{ListLock};
+        auto iter = std::lower_bound(ContextList.cbegin(), ContextList.cend(), context.get());
+        ContextList.emplace(iter, context.get());
+    }
+
+    if(ALeffectslot *slot{context->mDefaultSlot.get()})
+    {
+        ALenum sloterr{slot->initEffect(ALCcontext::sDefaultEffect.type,
+            ALCcontext::sDefaultEffect.Props, context.get())};
+        if(sloterr == AL_NO_ERROR)
+            slot->updateProps(context.get());
+        else
+            ERR("Failed to initialize the default effect\n");
+    }
+
+    TRACE("Created context %p\n", voidp{context.get()});
+    return context.release();
+}
+END_API_FUNC
+
+ALC_API void ALC_APIENTRY alcDestroyContext(ALCcontext *context)
+START_API_FUNC
+{
+    std::unique_lock<std::recursive_mutex> listlock{ListLock};
+    auto iter = std::lower_bound(ContextList.begin(), ContextList.end(), context);
+    if(iter == ContextList.end() || *iter != context)
+    {
+        listlock.unlock();
+        alcSetError(nullptr, ALC_INVALID_CONTEXT);
+        return;
+    }
+
+    /* Hold a reference to this context so it remains valid until the ListLock
+     * is released.
+     */
+    ContextRef ctx{*iter};
+    ContextList.erase(iter);
+
+    ALCdevice *Device{ctx->mALDevice.get()};
+
+    std::lock_guard<std::mutex> _{Device->StateLock};
+    if(!ctx->deinit() && Device->Flags.test(DeviceRunning))
+    {
+        Device->Backend->stop();
+        Device->Flags.reset(DeviceRunning);
+    }
+}
+END_API_FUNC
+
+
+ALC_API ALCcontext* ALC_APIENTRY alcGetCurrentContext(void)
+START_API_FUNC
+{
+    ALCcontext *Context{ALCcontext::getThreadContext()};
+    if(!Context) Context = ALCcontext::sGlobalContext.load();
+    return Context;
+}
+END_API_FUNC
+
+/** Returns the currently active thread-local context. */
+ALC_API ALCcontext* ALC_APIENTRY alcGetThreadContext(void)
+START_API_FUNC
+{ return ALCcontext::getThreadContext(); }
+END_API_FUNC
+
+ALC_API ALCboolean ALC_APIENTRY alcMakeContextCurrent(ALCcontext *context)
+START_API_FUNC
+{
+    /* context must be valid or nullptr */
+    ContextRef ctx;
+    if(context)
+    {
+        ctx = VerifyContext(context);
+        if(!ctx)
+        {
+            alcSetError(nullptr, ALC_INVALID_CONTEXT);
+            return ALC_FALSE;
+        }
+    }
+    /* Release this reference (if any) to store it in the GlobalContext
+     * pointer. Take ownership of the reference (if any) that was previously
+     * stored there, and let the reference go.
+     */
+    while(ALCcontext::sGlobalContextLock.exchange(true, std::memory_order_acquire)) {
+        /* Wait to make sure another thread isn't getting or trying to change
+         * the current context as its refcount is decremented.
+         */
+    }
+    ContextRef{ALCcontext::sGlobalContext.exchange(ctx.release())};
+    ALCcontext::sGlobalContextLock.store(false, std::memory_order_release);
+
+    /* Take ownership of the thread-local context reference (if any), clearing
+     * the storage to null.
+     */
+    ctx = ContextRef{ALCcontext::getThreadContext()};
+    if(ctx) ALCcontext::setThreadContext(nullptr);
+    /* Reset (decrement) the previous thread-local reference. */
+
+    return ALC_TRUE;
+}
+END_API_FUNC
+
+/** Makes the given context the active context for the current thread. */
+ALC_API ALCboolean ALC_APIENTRY alcSetThreadContext(ALCcontext *context)
+START_API_FUNC
+{
+    /* context must be valid or nullptr */
+    ContextRef ctx;
+    if(context)
+    {
+        ctx = VerifyContext(context);
+        if(!ctx)
+        {
+            alcSetError(nullptr, ALC_INVALID_CONTEXT);
+            return ALC_FALSE;
+        }
+    }
+    /* context's reference count is already incremented */
+    ContextRef old{ALCcontext::getThreadContext()};
+    ALCcontext::setThreadContext(ctx.release());
+
+    return ALC_TRUE;
+}
+END_API_FUNC
+
+
+ALC_API ALCdevice* ALC_APIENTRY alcGetContextsDevice(ALCcontext *Context)
+START_API_FUNC
+{
+    ContextRef ctx{VerifyContext(Context)};
+    if(!ctx)
+    {
+        alcSetError(nullptr, ALC_INVALID_CONTEXT);
+        return nullptr;
+    }
+    return ctx->mALDevice.get();
+}
+END_API_FUNC
+
+
+ALC_API ALCdevice* ALC_APIENTRY alcOpenDevice(const ALCchar *deviceName)
+START_API_FUNC
+{
+    InitConfig();
+
+    if(!PlaybackFactory)
+    {
+        alcSetError(nullptr, ALC_INVALID_VALUE);
+        return nullptr;
+    }
+
+    if(deviceName)
+    {
+        TRACE("Opening playback device \"%s\"\n", deviceName);
+        if(!deviceName[0] || al::strcasecmp(deviceName, alcDefaultName) == 0
+#ifdef _WIN32
+            /* Some old Windows apps hardcode these expecting OpenAL to use a
+             * specific audio API, even when they're not enumerated. Creative's
+             * router effectively ignores them too.
+             */
+            || al::strcasecmp(deviceName, "DirectSound3D") == 0
+            || al::strcasecmp(deviceName, "DirectSound") == 0
+            || al::strcasecmp(deviceName, "MMSYSTEM") == 0
+#endif
+            /* Some old Linux apps hardcode configuration strings that were
+             * supported by the OpenAL SI. We can't really do anything useful
+             * with them, so just ignore.
+             */
+            || (deviceName[0] == '\'' && deviceName[1] == '(')
+            || al::strcasecmp(deviceName, "openal-soft") == 0)
+            deviceName = nullptr;
+    }
+    else
+        TRACE("Opening default playback device\n");
+
+    const uint DefaultSends{
+#ifdef ALSOFT_EAX
+        eax_g_is_enabled ? uint{EAX_MAX_FXSLOTS} :
+#endif // ALSOFT_EAX
+        DEFAULT_SENDS
+    };
+
+    DeviceRef device{new ALCdevice{DeviceType::Playback}};
+
+    /* Set output format */
+    device->FmtChans = DevFmtChannelsDefault;
+    device->FmtType = DevFmtTypeDefault;
+    device->Frequency = DEFAULT_OUTPUT_RATE;
+    device->UpdateSize = DEFAULT_UPDATE_SIZE;
+    device->BufferSize = DEFAULT_UPDATE_SIZE * DEFAULT_NUM_UPDATES;
+
+    device->SourcesMax = 256;
+    device->NumStereoSources = 1;
+    device->NumMonoSources = device->SourcesMax - device->NumStereoSources;
+    device->AuxiliaryEffectSlotMax = 64;
+    device->NumAuxSends = DefaultSends;
+
+    try {
+        auto backend = PlaybackFactory->createBackend(device.get(), BackendType::Playback);
+        std::lock_guard<std::recursive_mutex> _{ListLock};
+        backend->open(deviceName);
+        device->Backend = std::move(backend);
+    }
+    catch(al::backend_exception &e) {
+        WARN("Failed to open playback device: %s\n", e.what());
+        alcSetError(nullptr, (e.errorCode() == al::backend_error::OutOfMemory)
+            ? ALC_OUT_OF_MEMORY : ALC_INVALID_VALUE);
+        return nullptr;
+    }
+
+    {
+        std::lock_guard<std::recursive_mutex> _{ListLock};
+        auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device.get());
+        DeviceList.emplace(iter, device.get());
+    }
+
+    TRACE("Created device %p, \"%s\"\n", voidp{device.get()}, device->DeviceName.c_str());
+    return device.release();
+}
+END_API_FUNC
+
+ALC_API ALCboolean ALC_APIENTRY alcCloseDevice(ALCdevice *device)
+START_API_FUNC
+{
+    std::unique_lock<std::recursive_mutex> listlock{ListLock};
+    auto iter = std::lower_bound(DeviceList.begin(), DeviceList.end(), device);
+    if(iter == DeviceList.end() || *iter != device)
+    {
+        alcSetError(nullptr, ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+    if((*iter)->Type == DeviceType::Capture)
+    {
+        alcSetError(*iter, ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+
+    /* Erase the device, and any remaining contexts left on it, from their
+     * respective lists.
+     */
+    DeviceRef dev{*iter};
+    DeviceList.erase(iter);
+
+    std::unique_lock<std::mutex> statelock{dev->StateLock};
+    al::vector<ContextRef> orphanctxs;
+    for(ContextBase *ctx : *dev->mContexts.load())
+    {
+        auto ctxiter = std::lower_bound(ContextList.begin(), ContextList.end(), ctx);
+        if(ctxiter != ContextList.end() && *ctxiter == ctx)
+        {
+            orphanctxs.emplace_back(ContextRef{*ctxiter});
+            ContextList.erase(ctxiter);
+        }
+    }
+    listlock.unlock();
+
+    for(ContextRef &context : orphanctxs)
+    {
+        WARN("Releasing orphaned context %p\n", voidp{context.get()});
+        context->deinit();
+    }
+    orphanctxs.clear();
+
+    if(dev->Flags.test(DeviceRunning))
+        dev->Backend->stop();
+    dev->Flags.reset(DeviceRunning);
+
+    return ALC_TRUE;
+}
+END_API_FUNC
+
+
+/************************************************
+ * ALC capture functions
+ ************************************************/
+ALC_API ALCdevice* ALC_APIENTRY alcCaptureOpenDevice(const ALCchar *deviceName, ALCuint frequency, ALCenum format, ALCsizei samples)
+START_API_FUNC
+{
+    InitConfig();
+
+    if(!CaptureFactory)
+    {
+        alcSetError(nullptr, ALC_INVALID_VALUE);
+        return nullptr;
+    }
+
+    if(samples <= 0)
+    {
+        alcSetError(nullptr, ALC_INVALID_VALUE);
+        return nullptr;
+    }
+
+    if(deviceName)
+    {
+        TRACE("Opening capture device \"%s\"\n", deviceName);
+        if(!deviceName[0] || al::strcasecmp(deviceName, alcDefaultName) == 0
+            || al::strcasecmp(deviceName, "openal-soft") == 0)
+            deviceName = nullptr;
+    }
+    else
+        TRACE("Opening default capture device\n");
+
+    DeviceRef device{new ALCdevice{DeviceType::Capture}};
+
+    auto decompfmt = DecomposeDevFormat(format);
+    if(!decompfmt)
+    {
+        alcSetError(nullptr, ALC_INVALID_ENUM);
+        return nullptr;
+    }
+
+    device->Frequency = frequency;
+    device->FmtChans = decompfmt->chans;
+    device->FmtType = decompfmt->type;
+    device->Flags.set(FrequencyRequest);
+    device->Flags.set(ChannelsRequest);
+    device->Flags.set(SampleTypeRequest);
+
+    device->UpdateSize = static_cast<uint>(samples);
+    device->BufferSize = static_cast<uint>(samples);
+
+    try {
+        TRACE("Capture format: %s, %s, %uhz, %u / %u buffer\n",
+            DevFmtChannelsString(device->FmtChans), DevFmtTypeString(device->FmtType),
+            device->Frequency, device->UpdateSize, device->BufferSize);
+
+        auto backend = CaptureFactory->createBackend(device.get(), BackendType::Capture);
+        std::lock_guard<std::recursive_mutex> _{ListLock};
+        backend->open(deviceName);
+        device->Backend = std::move(backend);
+    }
+    catch(al::backend_exception &e) {
+        WARN("Failed to open capture device: %s\n", e.what());
+        alcSetError(nullptr, (e.errorCode() == al::backend_error::OutOfMemory)
+            ? ALC_OUT_OF_MEMORY : ALC_INVALID_VALUE);
+        return nullptr;
+    }
+
+    {
+        std::lock_guard<std::recursive_mutex> _{ListLock};
+        auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device.get());
+        DeviceList.emplace(iter, device.get());
+    }
+
+    TRACE("Created capture device %p, \"%s\"\n", voidp{device.get()}, device->DeviceName.c_str());
+    return device.release();
+}
+END_API_FUNC
+
+ALC_API ALCboolean ALC_APIENTRY alcCaptureCloseDevice(ALCdevice *device)
+START_API_FUNC
+{
+    std::unique_lock<std::recursive_mutex> listlock{ListLock};
+    auto iter = std::lower_bound(DeviceList.begin(), DeviceList.end(), device);
+    if(iter == DeviceList.end() || *iter != device)
+    {
+        alcSetError(nullptr, ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+    if((*iter)->Type != DeviceType::Capture)
+    {
+        alcSetError(*iter, ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+
+    DeviceRef dev{*iter};
+    DeviceList.erase(iter);
+    listlock.unlock();
+
+    std::lock_guard<std::mutex> _{dev->StateLock};
+    if(dev->Flags.test(DeviceRunning))
+        dev->Backend->stop();
+    dev->Flags.reset(DeviceRunning);
+
+    return ALC_TRUE;
+}
+END_API_FUNC
+
+ALC_API void ALC_APIENTRY alcCaptureStart(ALCdevice *device)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type != DeviceType::Capture)
+    {
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        return;
+    }
+
+    std::lock_guard<std::mutex> _{dev->StateLock};
+    if(!dev->Connected.load(std::memory_order_acquire))
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+    else if(!dev->Flags.test(DeviceRunning))
+    {
+        try {
+            auto backend = dev->Backend.get();
+            backend->start();
+            dev->Flags.set(DeviceRunning);
+        }
+        catch(al::backend_exception& e) {
+            ERR("%s\n", e.what());
+            dev->handleDisconnect("%s", e.what());
+            alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        }
+    }
+}
+END_API_FUNC
+
+ALC_API void ALC_APIENTRY alcCaptureStop(ALCdevice *device)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type != DeviceType::Capture)
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+    else
+    {
+        std::lock_guard<std::mutex> _{dev->StateLock};
+        if(dev->Flags.test(DeviceRunning))
+            dev->Backend->stop();
+        dev->Flags.reset(DeviceRunning);
+    }
+}
+END_API_FUNC
+
+ALC_API void ALC_APIENTRY alcCaptureSamples(ALCdevice *device, ALCvoid *buffer, ALCsizei samples)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type != DeviceType::Capture)
+    {
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        return;
+    }
+
+    if(samples < 0 || (samples > 0 && buffer == nullptr))
+    {
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+        return;
+    }
+    if(samples < 1)
+        return;
+
+    std::lock_guard<std::mutex> _{dev->StateLock};
+    BackendBase *backend{dev->Backend.get()};
+
+    const auto usamples = static_cast<uint>(samples);
+    if(usamples > backend->availableSamples())
+    {
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+        return;
+    }
+
+    backend->captureSamples(static_cast<al::byte*>(buffer), usamples);
+}
+END_API_FUNC
+
+
+/************************************************
+ * ALC loopback functions
+ ************************************************/
+
+/** Open a loopback device, for manual rendering. */
+ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar *deviceName)
+START_API_FUNC
+{
+    InitConfig();
+
+    /* Make sure the device name, if specified, is us. */
+    if(deviceName && strcmp(deviceName, alcDefaultName) != 0)
+    {
+        alcSetError(nullptr, ALC_INVALID_VALUE);
+        return nullptr;
+    }
+
+    const uint DefaultSends{
+#ifdef ALSOFT_EAX
+        eax_g_is_enabled ? uint{EAX_MAX_FXSLOTS} :
+#endif // ALSOFT_EAX
+        DEFAULT_SENDS
+    };
+
+    DeviceRef device{new ALCdevice{DeviceType::Loopback}};
+
+    device->SourcesMax = 256;
+    device->AuxiliaryEffectSlotMax = 64;
+    device->NumAuxSends = DefaultSends;
+
+    //Set output format
+    device->BufferSize = 0;
+    device->UpdateSize = 0;
+
+    device->Frequency = DEFAULT_OUTPUT_RATE;
+    device->FmtChans = DevFmtChannelsDefault;
+    device->FmtType = DevFmtTypeDefault;
+
+    device->NumStereoSources = 1;
+    device->NumMonoSources = device->SourcesMax - device->NumStereoSources;
+
+    try {
+        auto backend = LoopbackBackendFactory::getFactory().createBackend(device.get(),
+            BackendType::Playback);
+        backend->open("Loopback");
+        device->Backend = std::move(backend);
+    }
+    catch(al::backend_exception &e) {
+        WARN("Failed to open loopback device: %s\n", e.what());
+        alcSetError(nullptr, (e.errorCode() == al::backend_error::OutOfMemory)
+            ? ALC_OUT_OF_MEMORY : ALC_INVALID_VALUE);
+        return nullptr;
+    }
+
+    {
+        std::lock_guard<std::recursive_mutex> _{ListLock};
+        auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device.get());
+        DeviceList.emplace(iter, device.get());
+    }
+
+    TRACE("Created loopback device %p\n", voidp{device.get()});
+    return device.release();
+}
+END_API_FUNC
+
+/**
+ * Determines if the loopback device supports the given format for rendering.
+ */
+ALC_API ALCboolean ALC_APIENTRY alcIsRenderFormatSupportedSOFT(ALCdevice *device, ALCsizei freq, ALCenum channels, ALCenum type)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type != DeviceType::Loopback)
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+    else if(freq <= 0)
+        alcSetError(dev.get(), ALC_INVALID_VALUE);
+    else
+    {
+        if(DevFmtTypeFromEnum(type).has_value() && DevFmtChannelsFromEnum(channels).has_value()
+            && freq >= MIN_OUTPUT_RATE && freq <= MAX_OUTPUT_RATE)
+            return ALC_TRUE;
+    }
+
+    return ALC_FALSE;
+}
+END_API_FUNC
+
+/**
+ * Renders some samples into a buffer, using the format last set by the
+ * attributes given to alcCreateContext.
+ */
+FORCE_ALIGN ALC_API void ALC_APIENTRY alcRenderSamplesSOFT(ALCdevice *device, ALCvoid *buffer, ALCsizei samples)
+START_API_FUNC
+{
+    if(!device || device->Type != DeviceType::Loopback)
+        alcSetError(device, ALC_INVALID_DEVICE);
+    else if(samples < 0 || (samples > 0 && buffer == nullptr))
+        alcSetError(device, ALC_INVALID_VALUE);
+    else
+        device->renderSamples(buffer, static_cast<uint>(samples), device->channelsFromFmt());
+}
+END_API_FUNC
+
+
+/************************************************
+ * ALC DSP pause/resume functions
+ ************************************************/
+
+/** Pause the DSP to stop audio processing. */
+ALC_API void ALC_APIENTRY alcDevicePauseSOFT(ALCdevice *device)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type != DeviceType::Playback)
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+    else
+    {
+        std::lock_guard<std::mutex> _{dev->StateLock};
+        if(dev->Flags.test(DeviceRunning))
+            dev->Backend->stop();
+        dev->Flags.reset(DeviceRunning);
+        dev->Flags.set(DevicePaused);
+    }
+}
+END_API_FUNC
+
+/** Resume the DSP to restart audio processing. */
+ALC_API void ALC_APIENTRY alcDeviceResumeSOFT(ALCdevice *device)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type != DeviceType::Playback)
+    {
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        return;
+    }
+
+    std::lock_guard<std::mutex> _{dev->StateLock};
+    if(!dev->Flags.test(DevicePaused))
+        return;
+    dev->Flags.reset(DevicePaused);
+    if(dev->mContexts.load()->empty())
+        return;
+
+    try {
+        auto backend = dev->Backend.get();
+        backend->start();
+        dev->Flags.set(DeviceRunning);
+    }
+    catch(al::backend_exception& e) {
+        ERR("%s\n", e.what());
+        dev->handleDisconnect("%s", e.what());
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        return;
+    }
+    TRACE("Post-resume: %s, %s, %uhz, %u / %u buffer\n",
+        DevFmtChannelsString(device->FmtChans), DevFmtTypeString(device->FmtType),
+        device->Frequency, device->UpdateSize, device->BufferSize);
+}
+END_API_FUNC
+
+
+/************************************************
+ * ALC HRTF functions
+ ************************************************/
+
+/** Gets a string parameter at the given index. */
+ALC_API const ALCchar* ALC_APIENTRY alcGetStringiSOFT(ALCdevice *device, ALCenum paramName, ALCsizei index)
+START_API_FUNC
+{
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type == DeviceType::Capture)
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+    else switch(paramName)
+    {
+        case ALC_HRTF_SPECIFIER_SOFT:
+            if(index >= 0 && static_cast<uint>(index) < dev->mHrtfList.size())
+                return dev->mHrtfList[static_cast<uint>(index)].c_str();
+            alcSetError(dev.get(), ALC_INVALID_VALUE);
+            break;
+
+        default:
+            alcSetError(dev.get(), ALC_INVALID_ENUM);
+            break;
+    }
+
+    return nullptr;
+}
+END_API_FUNC
+
+/** Resets the given device output, using the specified attribute list. */
+ALC_API ALCboolean ALC_APIENTRY alcResetDeviceSOFT(ALCdevice *device, const ALCint *attribs)
+START_API_FUNC
+{
+    std::unique_lock<std::recursive_mutex> listlock{ListLock};
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type == DeviceType::Capture)
+    {
+        listlock.unlock();
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+    std::lock_guard<std::mutex> _{dev->StateLock};
+    listlock.unlock();
+
+    /* Force the backend to stop mixing first since we're resetting. Also reset
+     * the connected state so lost devices can attempt recover.
+     */
+    if(dev->Flags.test(DeviceRunning))
+        dev->Backend->stop();
+    dev->Flags.reset(DeviceRunning);
+
+    return ResetDeviceParams(dev.get(), attribs) ? ALC_TRUE : ALC_FALSE;
+}
+END_API_FUNC
+
+
+/************************************************
+ * ALC device reopen functions
+ ************************************************/
+
+/** Reopens the given device output, using the specified name and attribute list. */
+FORCE_ALIGN ALCboolean ALC_APIENTRY alcReopenDeviceSOFT(ALCdevice *device,
+    const ALCchar *deviceName, const ALCint *attribs)
+START_API_FUNC
+{
+    if(deviceName)
+    {
+        if(!deviceName[0] || al::strcasecmp(deviceName, alcDefaultName) == 0)
+            deviceName = nullptr;
+    }
+
+    std::unique_lock<std::recursive_mutex> listlock{ListLock};
+    DeviceRef dev{VerifyDevice(device)};
+    if(!dev || dev->Type != DeviceType::Playback)
+    {
+        listlock.unlock();
+        alcSetError(dev.get(), ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+    std::lock_guard<std::mutex> _{dev->StateLock};
+
+    /* Force the backend to stop mixing first since we're reopening. */
+    if(dev->Flags.test(DeviceRunning))
+    {
+        auto backend = dev->Backend.get();
+        backend->stop();
+        dev->Flags.reset(DeviceRunning);
+    }
+
+    BackendPtr newbackend;
+    try {
+        newbackend = PlaybackFactory->createBackend(dev.get(), BackendType::Playback);
+        newbackend->open(deviceName);
+    }
+    catch(al::backend_exception &e) {
+        listlock.unlock();
+        newbackend = nullptr;
+
+        WARN("Failed to reopen playback device: %s\n", e.what());
+        alcSetError(dev.get(), (e.errorCode() == al::backend_error::OutOfMemory)
+            ? ALC_OUT_OF_MEMORY : ALC_INVALID_VALUE);
+
+        /* If the device is connected, not paused, and has contexts, ensure it
+         * continues playing.
+         */
+        if(dev->Connected.load(std::memory_order_relaxed) && !dev->Flags.test(DevicePaused)
+            && !dev->mContexts.load(std::memory_order_relaxed)->empty())
+        {
+            try {
+                auto backend = dev->Backend.get();
+                backend->start();
+                dev->Flags.set(DeviceRunning);
+            }
+            catch(al::backend_exception &be) {
+                ERR("%s\n", be.what());
+                dev->handleDisconnect("%s", be.what());
+            }
+        }
+        return ALC_FALSE;
+    }
+    listlock.unlock();
+    dev->Backend = std::move(newbackend);
+    TRACE("Reopened device %p, \"%s\"\n", voidp{dev.get()}, dev->DeviceName.c_str());
+
+    /* Always return true even if resetting fails. It shouldn't fail, but this
+     * is primarily to avoid confusion by the app seeing the function return
+     * false while the device is on the new output anyway. We could try to
+     * restore the old backend if this fails, but the configuration would be
+     * changed with the new backend and would need to be reset again with the
+     * old one, and the provided attributes may not be appropriate or desirable
+     * for the old device.
+     *
+     * In this way, we essentially act as if the function succeeded, but
+     * immediately disconnects following it.
+     */
+    ResetDeviceParams(dev.get(), attribs);
+    return ALC_TRUE;
+}
+END_API_FUNC
diff --git a/alc/alconfig.cpp b/alc/alconfig.cpp
new file mode 100644 (file)
index 0000000..b0544b8
--- /dev/null
@@ -0,0 +1,528 @@
+/**
+ * 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 "alconfig.h"
+
+#include <cstdlib>
+#include <cctype>
+#include <cstring>
+#ifdef _WIN32
+#include <windows.h>
+#include <shlobj.h>
+#endif
+#ifdef __APPLE__
+#include <CoreFoundation/CoreFoundation.h>
+#endif
+
+#include <algorithm>
+#include <cstdio>
+#include <string>
+#include <utility>
+
+#include "alfstream.h"
+#include "alstring.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "strutils.h"
+#include "vector.h"
+
+
+namespace {
+
+struct ConfigEntry {
+    std::string key;
+    std::string value;
+};
+al::vector<ConfigEntry> ConfOpts;
+
+
+std::string &lstrip(std::string &line)
+{
+    size_t pos{0};
+    while(pos < line.length() && std::isspace(line[pos]))
+        ++pos;
+    line.erase(0, pos);
+    return line;
+}
+
+bool readline(std::istream &f, std::string &output)
+{
+    while(f.good() && f.peek() == '\n')
+        f.ignore();
+
+    return std::getline(f, output) && !output.empty();
+}
+
+std::string expdup(const char *str)
+{
+    std::string output;
+
+    std::string envval;
+    while(*str != '\0')
+    {
+        const char *addstr;
+        size_t addstrlen;
+
+        if(str[0] != '$')
+        {
+            const char *next = std::strchr(str, '$');
+            addstr = str;
+            addstrlen = next ? static_cast<size_t>(next-str) : std::strlen(str);
+
+            str += addstrlen;
+        }
+        else
+        {
+            str++;
+            if(*str == '$')
+            {
+                const char *next = std::strchr(str+1, '$');
+                addstr = str;
+                addstrlen = next ? static_cast<size_t>(next-str) : std::strlen(str);
+
+                str += addstrlen;
+            }
+            else
+            {
+                const bool hasbraces{(*str == '{')};
+
+                if(hasbraces) str++;
+                const char *envstart = str;
+                while(std::isalnum(*str) || *str == '_')
+                    ++str;
+                if(hasbraces && *str != '}')
+                    continue;
+                const std::string envname{envstart, str};
+                if(hasbraces) str++;
+
+                envval = al::getenv(envname.c_str()).value_or(std::string{});
+                addstr = envval.data();
+                addstrlen = envval.length();
+            }
+        }
+        if(addstrlen == 0)
+            continue;
+
+        output.append(addstr, addstrlen);
+    }
+
+    return output;
+}
+
+void LoadConfigFromFile(std::istream &f)
+{
+    std::string curSection;
+    std::string buffer;
+
+    while(readline(f, buffer))
+    {
+        if(lstrip(buffer).empty())
+            continue;
+
+        if(buffer[0] == '[')
+        {
+            auto line = const_cast<char*>(buffer.data());
+            char *section = line+1;
+            char *endsection;
+
+            endsection = std::strchr(section, ']');
+            if(!endsection || section == endsection)
+            {
+                ERR(" config parse error: bad line \"%s\"\n", line);
+                continue;
+            }
+            if(endsection[1] != 0)
+            {
+                char *end = endsection+1;
+                while(std::isspace(*end))
+                    ++end;
+                if(*end != 0 && *end != '#')
+                {
+                    ERR(" config parse error: bad line \"%s\"\n", line);
+                    continue;
+                }
+            }
+            *endsection = 0;
+
+            curSection.clear();
+            if(al::strcasecmp(section, "general") != 0)
+            {
+                do {
+                    char *nextp = std::strchr(section, '%');
+                    if(!nextp)
+                    {
+                        curSection += section;
+                        break;
+                    }
+
+                    curSection.append(section, nextp);
+                    section = nextp;
+
+                    if(((section[1] >= '0' && section[1] <= '9') ||
+                        (section[1] >= 'a' && section[1] <= 'f') ||
+                        (section[1] >= 'A' && section[1] <= 'F')) &&
+                       ((section[2] >= '0' && section[2] <= '9') ||
+                        (section[2] >= 'a' && section[2] <= 'f') ||
+                        (section[2] >= 'A' && section[2] <= 'F')))
+                    {
+                        int b{0};
+                        if(section[1] >= '0' && section[1] <= '9')
+                            b = (section[1]-'0') << 4;
+                        else if(section[1] >= 'a' && section[1] <= 'f')
+                            b = (section[1]-'a'+0xa) << 4;
+                        else if(section[1] >= 'A' && section[1] <= 'F')
+                            b = (section[1]-'A'+0x0a) << 4;
+                        if(section[2] >= '0' && section[2] <= '9')
+                            b |= (section[2]-'0');
+                        else if(section[2] >= 'a' && section[2] <= 'f')
+                            b |= (section[2]-'a'+0xa);
+                        else if(section[2] >= 'A' && section[2] <= 'F')
+                            b |= (section[2]-'A'+0x0a);
+                        curSection += static_cast<char>(b);
+                        section += 3;
+                    }
+                    else if(section[1] == '%')
+                    {
+                        curSection += '%';
+                        section += 2;
+                    }
+                    else
+                    {
+                        curSection += '%';
+                        section += 1;
+                    }
+                } while(*section != 0);
+            }
+
+            continue;
+        }
+
+        auto cmtpos = std::min(buffer.find('#'), buffer.size());
+        while(cmtpos > 0 && std::isspace(buffer[cmtpos-1]))
+            --cmtpos;
+        if(!cmtpos) continue;
+        buffer.erase(cmtpos);
+
+        auto sep = buffer.find('=');
+        if(sep == std::string::npos)
+        {
+            ERR(" config parse error: malformed option line: \"%s\"\n", buffer.c_str());
+            continue;
+        }
+        auto keyend = sep++;
+        while(keyend > 0 && std::isspace(buffer[keyend-1]))
+            --keyend;
+        if(!keyend)
+        {
+            ERR(" config parse error: malformed option line: \"%s\"\n", buffer.c_str());
+            continue;
+        }
+        while(sep < buffer.size() && std::isspace(buffer[sep]))
+            sep++;
+
+        std::string fullKey;
+        if(!curSection.empty())
+        {
+            fullKey += curSection;
+            fullKey += '/';
+        }
+        fullKey += buffer.substr(0u, keyend);
+
+        std::string value{(sep < buffer.size()) ? buffer.substr(sep) : std::string{}};
+        if(value.size() > 1)
+        {
+            if((value.front() == '"' && value.back() == '"')
+                || (value.front() == '\'' && value.back() == '\''))
+            {
+                value.pop_back();
+                value.erase(value.begin());
+            }
+        }
+
+        TRACE(" found '%s' = '%s'\n", fullKey.c_str(), value.c_str());
+
+        /* Check if we already have this option set */
+        auto find_key = [&fullKey](const ConfigEntry &entry) -> bool
+        { return entry.key == fullKey; };
+        auto ent = std::find_if(ConfOpts.begin(), ConfOpts.end(), find_key);
+        if(ent != ConfOpts.end())
+        {
+            if(!value.empty())
+                ent->value = expdup(value.c_str());
+            else
+                ConfOpts.erase(ent);
+        }
+        else if(!value.empty())
+            ConfOpts.emplace_back(ConfigEntry{std::move(fullKey), expdup(value.c_str())});
+    }
+    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
+
+
+#ifdef _WIN32
+void ReadALConfig()
+{
+    WCHAR buffer[MAX_PATH];
+    if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE)
+    {
+        std::string filepath{wstr_to_utf8(buffer)};
+        filepath += "\\alsoft.ini";
+
+        TRACE("Loading config %s...\n", filepath.c_str());
+        al::ifstream f{filepath};
+        if(f.is_open())
+            LoadConfigFromFile(f);
+    }
+
+    std::string ppath{GetProcBinary().path};
+    if(!ppath.empty())
+    {
+        ppath += "\\alsoft.ini";
+        TRACE("Loading config %s...\n", ppath.c_str());
+        al::ifstream f{ppath};
+        if(f.is_open())
+            LoadConfigFromFile(f);
+    }
+
+    if(auto confpath = al::getenv(L"ALSOFT_CONF"))
+    {
+        TRACE("Loading config %s...\n", wstr_to_utf8(confpath->c_str()).c_str());
+        al::ifstream f{*confpath};
+        if(f.is_open())
+            LoadConfigFromFile(f);
+    }
+}
+
+#else
+
+void ReadALConfig()
+{
+    const char *str{"/etc/openal/alsoft.conf"};
+
+    TRACE("Loading config %s...\n", str);
+    al::ifstream f{str};
+    if(f.is_open())
+        LoadConfigFromFile(f);
+    f.close();
+
+    std::string confpaths{al::getenv("XDG_CONFIG_DIRS").value_or("/etc/xdg")};
+    /* Go through the list in reverse, since "the order of base directories
+     * denotes their importance; the first directory listed is the most
+     * important". Ergo, we need to load the settings from the later dirs
+     * first so that the settings in the earlier dirs override them.
+     */
+    std::string fname;
+    while(!confpaths.empty())
+    {
+        auto next = confpaths.find_last_of(':');
+        if(next < confpaths.length())
+        {
+            fname = confpaths.substr(next+1);
+            confpaths.erase(next);
+        }
+        else
+        {
+            fname = confpaths;
+            confpaths.clear();
+        }
+
+        if(fname.empty() || fname.front() != '/')
+            WARN("Ignoring XDG config dir: %s\n", fname.c_str());
+        else
+        {
+            if(fname.back() != '/') fname += "/alsoft.conf";
+            else fname += "alsoft.conf";
+
+            TRACE("Loading config %s...\n", fname.c_str());
+            f = al::ifstream{fname};
+            if(f.is_open())
+                LoadConfigFromFile(f);
+        }
+        fname.clear();
+    }
+
+#ifdef __APPLE__
+    CFBundleRef mainBundle = CFBundleGetMainBundle();
+    if(mainBundle)
+    {
+        unsigned char fileName[PATH_MAX];
+        CFURLRef configURL;
+
+        if((configURL=CFBundleCopyResourceURL(mainBundle, CFSTR(".alsoftrc"), CFSTR(""), nullptr)) &&
+           CFURLGetFileSystemRepresentation(configURL, true, fileName, sizeof(fileName)))
+        {
+            f = al::ifstream{reinterpret_cast<char*>(fileName)};
+            if(f.is_open())
+                LoadConfigFromFile(f);
+        }
+    }
+#endif
+
+    if(auto homedir = al::getenv("HOME"))
+    {
+        fname = *homedir;
+        if(fname.back() != '/') fname += "/.alsoftrc";
+        else fname += ".alsoftrc";
+
+        TRACE("Loading config %s...\n", fname.c_str());
+        f = al::ifstream{fname};
+        if(f.is_open())
+            LoadConfigFromFile(f);
+    }
+
+    if(auto configdir = al::getenv("XDG_CONFIG_HOME"))
+    {
+        fname = *configdir;
+        if(fname.back() != '/') fname += "/alsoft.conf";
+        else fname += "alsoft.conf";
+    }
+    else
+    {
+        fname.clear();
+        if(auto homedir = al::getenv("HOME"))
+        {
+            fname = *homedir;
+            if(fname.back() != '/') fname += "/.config/alsoft.conf";
+            else fname += ".config/alsoft.conf";
+        }
+    }
+    if(!fname.empty())
+    {
+        TRACE("Loading config %s...\n", fname.c_str());
+        f = al::ifstream{fname};
+        if(f.is_open())
+            LoadConfigFromFile(f);
+    }
+
+    std::string ppath{GetProcBinary().path};
+    if(!ppath.empty())
+    {
+        if(ppath.back() != '/') ppath += "/alsoft.conf";
+        else ppath += "alsoft.conf";
+
+        TRACE("Loading config %s...\n", ppath.c_str());
+        f = al::ifstream{ppath};
+        if(f.is_open())
+            LoadConfigFromFile(f);
+    }
+
+    if(auto confname = al::getenv("ALSOFT_CONF"))
+    {
+        TRACE("Loading config %s...\n", confname->c_str());
+        f = al::ifstream{*confname};
+        if(f.is_open())
+            LoadConfigFromFile(f);
+    }
+}
+#endif
+
+al::optional<std::string> ConfigValueStr(const char *devName, const char *blockName, const char *keyName)
+{
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return val;
+    return al::nullopt;
+}
+
+al::optional<int> ConfigValueInt(const char *devName, const char *blockName, const char *keyName)
+{
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return 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)
+{
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return 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)
+{
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return std::strtof(val, nullptr);
+    return al::nullopt;
+}
+
+al::optional<bool> ConfigValueBool(const char *devName, const char *blockName, const char *keyName)
+{
+    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 al::nullopt;
+}
+
+bool GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, bool def)
+{
+    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;
+}
diff --git a/alc/alconfig.h b/alc/alconfig.h
new file mode 100644 (file)
index 0000000..df2830c
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef ALCONFIG_H
+#define ALCONFIG_H
+
+#include <string>
+
+#include "aloptional.h"
+
+void ReadALConfig();
+
+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);
+al::optional<unsigned int> ConfigValueUInt(const char *devName, const char *blockName, const char *keyName);
+al::optional<float> ConfigValueFloat(const char *devName, const char *blockName, const char *keyName);
+al::optional<bool> ConfigValueBool(const char *devName, const char *blockName, const char *keyName);
+
+#endif /* ALCONFIG_H */
diff --git a/alc/alu.cpp b/alc/alu.cpp
new file mode 100644 (file)
index 0000000..e9ad68b
--- /dev/null
@@ -0,0 +1,2210 @@
+/**
+ * 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 "alu.h"
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cassert>
+#include <chrono>
+#include <climits>
+#include <cstdarg>
+#include <cstdio>
+#include <cstdlib>
+#include <functional>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <new>
+#include <stdint.h>
+#include <utility>
+
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "atomic.h"
+#include "core/ambidefs.h"
+#include "core/async_event.h"
+#include "core/bformatdec.h"
+#include "core/bs2b.h"
+#include "core/bsinc_defs.h"
+#include "core/bsinc_tables.h"
+#include "core/bufferline.h"
+#include "core/buffer_storage.h"
+#include "core/context.h"
+#include "core/cpu_caps.h"
+#include "core/cubic_tables.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effects/base.h"
+#include "core/effectslot.h"
+#include "core/filters/biquad.h"
+#include "core/filters/nfc.h"
+#include "core/fpu_ctrl.h"
+#include "core/hrtf.h"
+#include "core/mastering.h"
+#include "core/mixer.h"
+#include "core/mixer/defs.h"
+#include "core/mixer/hrtfdefs.h"
+#include "core/resampler_limits.h"
+#include "core/uhjfilter.h"
+#include "core/voice.h"
+#include "core/voice_change.h"
+#include "intrusive_ptr.h"
+#include "opthelpers.h"
+#include "ringbuffer.h"
+#include "strutils.h"
+#include "threads.h"
+#include "vecmat.h"
+#include "vector.h"
+
+struct CTag;
+#ifdef HAVE_SSE
+struct SSETag;
+#endif
+#ifdef HAVE_SSE2
+struct SSE2Tag;
+#endif
+#ifdef HAVE_SSE4_1
+struct SSE4Tag;
+#endif
+#ifdef HAVE_NEON
+struct NEONTag;
+#endif
+struct PointTag;
+struct LerpTag;
+struct CubicTag;
+struct BSincTag;
+struct FastBSincTag;
+
+
+static_assert(!(MaxResamplerPadding&1), "MaxResamplerPadding is not a multiple of two");
+
+
+namespace {
+
+using uint = unsigned int;
+using namespace std::chrono;
+
+using namespace std::placeholders;
+
+float InitConeScale()
+{
+    float ret{1.0f};
+    if(auto optval = al::getenv("__ALSOFT_HALF_ANGLE_CONES"))
+    {
+        if(al::strcasecmp(optval->c_str(), "true") == 0
+            || strtol(optval->c_str(), nullptr, 0) == 1)
+            ret *= 0.5f;
+    }
+    return ret;
+}
+/* Cone scalar */
+const float ConeScale{InitConeScale()};
+
+/* Localized scalars for mono sources (initialized in aluInit, after
+ * configuration is loaded).
+ */
+float XScale{1.0f};
+float YScale{1.0f};
+float ZScale{1.0f};
+
+/* Source distance scale for NFC filters. */
+float NfcScale{1.0f};
+
+
+struct ChanMap {
+    Channel channel;
+    float angle;
+    float elevation;
+};
+
+using HrtfDirectMixerFunc = void(*)(const FloatBufferSpan LeftOut, const FloatBufferSpan RightOut,
+    const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples, float *TempBuf,
+    HrtfChannelState *ChanState, const size_t IrSize, const size_t BufferSize);
+
+HrtfDirectMixerFunc MixDirectHrtf{MixDirectHrtf_<CTag>};
+
+inline HrtfDirectMixerFunc SelectHrtfMixer(void)
+{
+#ifdef HAVE_NEON
+    if((CPUCapFlags&CPU_CAP_NEON))
+        return MixDirectHrtf_<NEONTag>;
+#endif
+#ifdef HAVE_SSE
+    if((CPUCapFlags&CPU_CAP_SSE))
+        return MixDirectHrtf_<SSETag>;
+#endif
+
+    return MixDirectHrtf_<CTag>;
+}
+
+
+inline void BsincPrepare(const uint increment, BsincState *state, const BSincTable *table)
+{
+    size_t si{BSincScaleCount - 1};
+    float sf{0.0f};
+
+    if(increment > MixerFracOne)
+    {
+        sf = MixerFracOne/static_cast<float>(increment) - table->scaleBase;
+        sf = maxf(0.0f, BSincScaleCount*sf*table->scaleRange - 1.0f);
+        si = float2uint(sf);
+        /* The interpolation factor is fit to this diagonally-symmetric curve
+         * to reduce the transition ripple caused by interpolating different
+         * scales of the sinc function.
+         */
+        sf = 1.0f - std::cos(std::asin(sf - static_cast<float>(si)));
+    }
+
+    state->sf = sf;
+    state->m = table->m[si];
+    state->l = (state->m/2) - 1;
+    state->filter = table->Tab + table->filterOffset[si];
+}
+
+inline ResamplerFunc SelectResampler(Resampler resampler, uint increment)
+{
+    switch(resampler)
+    {
+    case Resampler::Point:
+        return Resample_<PointTag,CTag>;
+    case Resampler::Linear:
+#ifdef HAVE_NEON
+        if((CPUCapFlags&CPU_CAP_NEON))
+            return Resample_<LerpTag,NEONTag>;
+#endif
+#ifdef HAVE_SSE4_1
+        if((CPUCapFlags&CPU_CAP_SSE4_1))
+            return Resample_<LerpTag,SSE4Tag>;
+#endif
+#ifdef HAVE_SSE2
+        if((CPUCapFlags&CPU_CAP_SSE2))
+            return Resample_<LerpTag,SSE2Tag>;
+#endif
+        return Resample_<LerpTag,CTag>;
+    case Resampler::Cubic:
+#ifdef HAVE_NEON
+        if((CPUCapFlags&CPU_CAP_NEON))
+            return Resample_<CubicTag,NEONTag>;
+#endif
+#ifdef HAVE_SSE
+        if((CPUCapFlags&CPU_CAP_SSE))
+            return Resample_<CubicTag,SSETag>;
+#endif
+        return Resample_<CubicTag,CTag>;
+    case Resampler::BSinc12:
+    case Resampler::BSinc24:
+        if(increment > MixerFracOne)
+        {
+#ifdef HAVE_NEON
+            if((CPUCapFlags&CPU_CAP_NEON))
+                return Resample_<BSincTag,NEONTag>;
+#endif
+#ifdef HAVE_SSE
+            if((CPUCapFlags&CPU_CAP_SSE))
+                return Resample_<BSincTag,SSETag>;
+#endif
+            return Resample_<BSincTag,CTag>;
+        }
+        /* fall-through */
+    case Resampler::FastBSinc12:
+    case Resampler::FastBSinc24:
+#ifdef HAVE_NEON
+        if((CPUCapFlags&CPU_CAP_NEON))
+            return Resample_<FastBSincTag,NEONTag>;
+#endif
+#ifdef HAVE_SSE
+        if((CPUCapFlags&CPU_CAP_SSE))
+            return Resample_<FastBSincTag,SSETag>;
+#endif
+        return Resample_<FastBSincTag,CTag>;
+    }
+
+    return Resample_<PointTag,CTag>;
+}
+
+} // namespace
+
+void aluInit(CompatFlagBitset flags, const float nfcscale)
+{
+    MixDirectHrtf = SelectHrtfMixer();
+    XScale = flags.test(CompatFlags::ReverseX) ? -1.0f : 1.0f;
+    YScale = flags.test(CompatFlags::ReverseY) ? -1.0f : 1.0f;
+    ZScale = flags.test(CompatFlags::ReverseZ) ? -1.0f : 1.0f;
+
+    NfcScale = clampf(nfcscale, 0.0001f, 10000.0f);
+}
+
+
+ResamplerFunc PrepareResampler(Resampler resampler, uint increment, InterpState *state)
+{
+    switch(resampler)
+    {
+    case Resampler::Point:
+    case Resampler::Linear:
+        break;
+    case Resampler::Cubic:
+        state->cubic.filter = gCubicSpline.Tab.data();
+        break;
+    case Resampler::FastBSinc12:
+    case Resampler::BSinc12:
+        BsincPrepare(increment, &state->bsinc, &gBSinc12);
+        break;
+    case Resampler::FastBSinc24:
+    case Resampler::BSinc24:
+        BsincPrepare(increment, &state->bsinc, &gBSinc24);
+        break;
+    }
+    return SelectResampler(resampler, increment);
+}
+
+
+void DeviceBase::ProcessHrtf(const size_t SamplesToDo)
+{
+    /* HRTF is stereo output only. */
+    const uint lidx{RealOut.ChannelIndex[FrontLeft]};
+    const uint ridx{RealOut.ChannelIndex[FrontRight]};
+
+    MixDirectHrtf(RealOut.Buffer[lidx], RealOut.Buffer[ridx], Dry.Buffer, HrtfAccumData,
+        mHrtfState->mTemp.data(), mHrtfState->mChannels.data(), mHrtfState->mIrSize, SamplesToDo);
+}
+
+void DeviceBase::ProcessAmbiDec(const size_t SamplesToDo)
+{
+    AmbiDecoder->process(RealOut.Buffer, Dry.Buffer.data(), SamplesToDo);
+}
+
+void DeviceBase::ProcessAmbiDecStablized(const size_t SamplesToDo)
+{
+    /* Decode with front image stablization. */
+    const uint lidx{RealOut.ChannelIndex[FrontLeft]};
+    const uint ridx{RealOut.ChannelIndex[FrontRight]};
+    const uint cidx{RealOut.ChannelIndex[FrontCenter]};
+
+    AmbiDecoder->processStablize(RealOut.Buffer, Dry.Buffer.data(), lidx, ridx, cidx,
+        SamplesToDo);
+}
+
+void DeviceBase::ProcessUhj(const size_t SamplesToDo)
+{
+    /* UHJ is stereo output only. */
+    const uint lidx{RealOut.ChannelIndex[FrontLeft]};
+    const uint ridx{RealOut.ChannelIndex[FrontRight]};
+
+    /* Encode to stereo-compatible 2-channel UHJ output. */
+    mUhjEncoder->encode(RealOut.Buffer[lidx].data(), RealOut.Buffer[ridx].data(),
+        {{Dry.Buffer[0].data(), Dry.Buffer[1].data(), Dry.Buffer[2].data()}}, SamplesToDo);
+}
+
+void DeviceBase::ProcessBs2b(const size_t SamplesToDo)
+{
+    /* First, decode the ambisonic mix to the "real" output. */
+    AmbiDecoder->process(RealOut.Buffer, Dry.Buffer.data(), SamplesToDo);
+
+    /* BS2B is stereo output only. */
+    const uint lidx{RealOut.ChannelIndex[FrontLeft]};
+    const uint ridx{RealOut.ChannelIndex[FrontRight]};
+
+    /* Now apply the BS2B binaural/crossfeed filter. */
+    bs2b_cross_feed(Bs2b.get(), RealOut.Buffer[lidx].data(), RealOut.Buffer[ridx].data(),
+        SamplesToDo);
+}
+
+
+namespace {
+
+/* This RNG method was created based on the math found in opusdec. It's quick,
+ * and starting with a seed value of 22222, is suitable for generating
+ * whitenoise.
+ */
+inline uint dither_rng(uint *seed) noexcept
+{
+    *seed = (*seed * 96314165) + 907633515;
+    return *seed;
+}
+
+
+/* Ambisonic upsampler function. It's effectively a matrix multiply. It takes
+ * an 'upsampler' and 'rotator' as the input matrices, and creates a matrix
+ * that behaves as if the B-Format input was first decoded to a speaker array
+ * at its input order, encoded back into the higher order mix, then finally
+ * rotated.
+ */
+void UpsampleBFormatTransform(
+    const al::span<std::array<float,MaxAmbiChannels>,MaxAmbiChannels> output,
+    const al::span<const std::array<float,MaxAmbiChannels>> upsampler,
+    const al::span<std::array<float,MaxAmbiChannels>,MaxAmbiChannels> rotator, size_t coeffs_order)
+{
+    const size_t num_chans{AmbiChannelsFromOrder(coeffs_order)};
+    for(size_t i{0};i < upsampler.size();++i)
+        output[i].fill(0.0f);
+    for(size_t i{0};i < upsampler.size();++i)
+    {
+        for(size_t k{0};k < num_chans;++k)
+        {
+            float *RESTRICT out{output[i].data()};
+            /* Write the full number of channels. The compiler will have an
+             * easier time optimizing if it has a fixed length.
+             */
+            for(size_t j{0};j < MaxAmbiChannels;++j)
+                out[j] += upsampler[i][k] * rotator[k][j];
+        }
+    }
+}
+
+
+inline auto& GetAmbiScales(AmbiScaling scaletype) noexcept
+{
+    switch(scaletype)
+    {
+    case AmbiScaling::FuMa: return AmbiScale::FromFuMa();
+    case AmbiScaling::SN3D: return AmbiScale::FromSN3D();
+    case AmbiScaling::UHJ: return AmbiScale::FromUHJ();
+    case AmbiScaling::N3D: break;
+    }
+    return AmbiScale::FromN3D();
+}
+
+inline auto& GetAmbiLayout(AmbiLayout layouttype) noexcept
+{
+    if(layouttype == AmbiLayout::FuMa) return AmbiIndex::FromFuMa();
+    return AmbiIndex::FromACN();
+}
+
+inline auto& GetAmbi2DLayout(AmbiLayout layouttype) noexcept
+{
+    if(layouttype == AmbiLayout::FuMa) return AmbiIndex::FromFuMa2D();
+    return AmbiIndex::FromACN2D();
+}
+
+
+bool CalcContextParams(ContextBase *ctx)
+{
+    ContextProps *props{ctx->mParams.ContextUpdate.exchange(nullptr, std::memory_order_acq_rel)};
+    if(!props) return false;
+
+    const alu::Vector pos{props->Position[0], props->Position[1], props->Position[2], 1.0f};
+    ctx->mParams.Position = pos;
+
+    /* AT then UP */
+    alu::Vector N{props->OrientAt[0], props->OrientAt[1], props->OrientAt[2], 0.0f};
+    N.normalize();
+    alu::Vector V{props->OrientUp[0], props->OrientUp[1], props->OrientUp[2], 0.0f};
+    V.normalize();
+    /* Build and normalize right-vector */
+    alu::Vector U{N.cross_product(V)};
+    U.normalize();
+
+    const alu::Matrix rot{
+        U[0], V[0], -N[0], 0.0,
+        U[1], V[1], -N[1], 0.0,
+        U[2], V[2], -N[2], 0.0,
+         0.0,  0.0,   0.0, 1.0};
+    const alu::Vector vel{props->Velocity[0], props->Velocity[1], props->Velocity[2], 0.0};
+
+    ctx->mParams.Matrix = rot;
+    ctx->mParams.Velocity = rot * vel;
+
+    ctx->mParams.Gain = props->Gain * ctx->mGainBoost;
+    ctx->mParams.MetersPerUnit = props->MetersPerUnit;
+    ctx->mParams.AirAbsorptionGainHF = props->AirAbsorptionGainHF;
+
+    ctx->mParams.DopplerFactor = props->DopplerFactor;
+    ctx->mParams.SpeedOfSound = props->SpeedOfSound * props->DopplerVelocity;
+
+    ctx->mParams.SourceDistanceModel = props->SourceDistanceModel;
+    ctx->mParams.mDistanceModel = props->mDistanceModel;
+
+    AtomicReplaceHead(ctx->mFreeContextProps, props);
+    return true;
+}
+
+bool CalcEffectSlotParams(EffectSlot *slot, EffectSlot **sorted_slots, ContextBase *context)
+{
+    EffectSlotProps *props{slot->Update.exchange(nullptr, std::memory_order_acq_rel)};
+    if(!props) return false;
+
+    /* If the effect slot target changed, clear the first sorted entry to force
+     * a re-sort.
+     */
+    if(slot->Target != props->Target)
+        *sorted_slots = nullptr;
+    slot->Gain = props->Gain;
+    slot->AuxSendAuto = props->AuxSendAuto;
+    slot->Target = props->Target;
+    slot->EffectType = props->Type;
+    slot->mEffectProps = props->Props;
+    if(props->Type == EffectSlotType::Reverb || props->Type == EffectSlotType::EAXReverb)
+    {
+        slot->RoomRolloff = props->Props.Reverb.RoomRolloffFactor;
+        slot->DecayTime = props->Props.Reverb.DecayTime;
+        slot->DecayLFRatio = props->Props.Reverb.DecayLFRatio;
+        slot->DecayHFRatio = props->Props.Reverb.DecayHFRatio;
+        slot->DecayHFLimit = props->Props.Reverb.DecayHFLimit;
+        slot->AirAbsorptionGainHF = props->Props.Reverb.AirAbsorptionGainHF;
+    }
+    else
+    {
+        slot->RoomRolloff = 0.0f;
+        slot->DecayTime = 0.0f;
+        slot->DecayLFRatio = 0.0f;
+        slot->DecayHFRatio = 0.0f;
+        slot->DecayHFLimit = false;
+        slot->AirAbsorptionGainHF = 1.0f;
+    }
+
+    EffectState *state{props->State.release()};
+    EffectState *oldstate{slot->mEffectState.release()};
+    slot->mEffectState.reset(state);
+
+    /* Only release the old state if it won't get deleted, since we can't be
+     * deleting/freeing anything in the mixer.
+     */
+    if(!oldstate->releaseIfNoDelete())
+    {
+        /* Otherwise, if it would be deleted send it off with a release event. */
+        RingBuffer *ring{context->mAsyncEvents.get()};
+        auto evt_vec = ring->getWriteVector();
+        if(evt_vec.first.len > 0) LIKELY
+        {
+            AsyncEvent *evt{al::construct_at(reinterpret_cast<AsyncEvent*>(evt_vec.first.buf),
+                AsyncEvent::ReleaseEffectState)};
+            evt->u.mEffectState = oldstate;
+            ring->writeAdvance(1);
+        }
+        else
+        {
+            /* If writing the event failed, the queue was probably full. Store
+             * the old state in the property object where it can eventually be
+             * cleaned up sometime later (not ideal, but better than blocking
+             * or leaking).
+             */
+            props->State.reset(oldstate);
+        }
+    }
+
+    AtomicReplaceHead(context->mFreeEffectslotProps, props);
+
+    EffectTarget output;
+    if(EffectSlot *target{slot->Target})
+        output = EffectTarget{&target->Wet, nullptr};
+    else
+    {
+        DeviceBase *device{context->mDevice};
+        output = EffectTarget{&device->Dry, &device->RealOut};
+    }
+    state->update(context, slot, &slot->mEffectProps, output);
+    return true;
+}
+
+
+/* Scales the given azimuth toward the side (+/- pi/2 radians) for positions in
+ * front.
+ */
+inline float ScaleAzimuthFront(float azimuth, float scale)
+{
+    const float abs_azi{std::fabs(azimuth)};
+    if(!(abs_azi >= al::numbers::pi_v<float>*0.5f))
+        return std::copysign(minf(abs_azi*scale, al::numbers::pi_v<float>*0.5f), azimuth);
+    return azimuth;
+}
+
+/* Wraps the given value in radians to stay between [-pi,+pi] */
+inline float WrapRadians(float r)
+{
+    static constexpr float Pi{al::numbers::pi_v<float>};
+    static constexpr float Pi2{Pi*2.0f};
+    if(r >  Pi) return std::fmod(Pi+r, Pi2) - Pi;
+    if(r < -Pi) return Pi - std::fmod(Pi-r, Pi2);
+    return r;
+}
+
+/* Begin ambisonic rotation helpers.
+ *
+ * Rotating first-order B-Format just needs a straight-forward X/Y/Z rotation
+ * matrix. Higher orders, however, are more complicated. The method implemented
+ * here is a recursive algorithm (the rotation for first-order is used to help
+ * generate the second-order rotation, which helps generate the third-order
+ * rotation, etc).
+ *
+ * Adapted from
+ * <https://github.com/polarch/Spherical-Harmonic-Transform/blob/master/getSHrotMtx.m>,
+ * provided under the BSD 3-Clause license.
+ *
+ * Copyright (c) 2015, Archontis Politis
+ * Copyright (c) 2019, Christopher Robinson
+ *
+ * The u, v, and w coefficients used for generating higher-order rotations are
+ * precomputed since they're constant. The second-order coefficients are
+ * followed by the third-order coefficients, etc.
+ */
+template<size_t L>
+constexpr size_t CalcRotatorSize()
+{ return (L*2 + 1)*(L*2 + 1) + CalcRotatorSize<L-1>(); }
+
+template<> constexpr size_t CalcRotatorSize<0>() = delete;
+template<> constexpr size_t CalcRotatorSize<1>() = delete;
+template<> constexpr size_t CalcRotatorSize<2>() { return 5*5; }
+
+struct RotatorCoeffs {
+    struct CoeffValues {
+        float u, v, w;
+    };
+    std::array<CoeffValues,CalcRotatorSize<MaxAmbiOrder>()> mCoeffs{};
+
+    RotatorCoeffs()
+    {
+        auto coeffs = mCoeffs.begin();
+
+        for(int l=2;l <= MaxAmbiOrder;++l)
+        {
+            for(int n{-l};n <= l;++n)
+            {
+                for(int m{-l};m <= l;++m)
+                {
+                    // compute u,v,w terms of Eq.8.1 (Table I)
+                    const bool d{m == 0}; // the delta function d_m0
+                    const float denom{static_cast<float>((std::abs(n) == l) ?
+                        (2*l) * (2*l - 1) : (l*l - n*n))};
+
+                    const int abs_m{std::abs(m)};
+                    coeffs->u = std::sqrt(static_cast<float>(l*l - m*m)/denom);
+                    coeffs->v = std::sqrt(static_cast<float>(l+abs_m-1) *
+                        static_cast<float>(l+abs_m) / denom) * (1.0f+d) * (1.0f - 2.0f*d) * 0.5f;
+                    coeffs->w = std::sqrt(static_cast<float>(l-abs_m-1) *
+                        static_cast<float>(l-abs_m) / denom) * (1.0f-d) * -0.5f;
+                    ++coeffs;
+                }
+            }
+        }
+    }
+};
+const RotatorCoeffs RotatorCoeffArray{};
+
+/**
+ * Given the matrix, pre-filled with the (zeroth- and) first-order rotation
+ * coefficients, this fills in the coefficients for the higher orders up to and
+ * including the given order. The matrix is in ACN layout.
+ */
+void AmbiRotator(AmbiRotateMatrix &matrix, const int order)
+{
+    /* Don't do anything for < 2nd order. */
+    if(order < 2) return;
+
+    auto P = [](const int i, const int l, const int a, const int n, const size_t last_band,
+        const AmbiRotateMatrix &R)
+    {
+        const float ri1{ R[ 1+2][static_cast<size_t>(i+2)]};
+        const float rim1{R[-1+2][static_cast<size_t>(i+2)]};
+        const float ri0{ R[ 0+2][static_cast<size_t>(i+2)]};
+
+        const size_t y{last_band + static_cast<size_t>(a+l-1)};
+        if(n == -l)
+            return ri1*R[last_band][y] + rim1*R[last_band + static_cast<size_t>(l-1)*2][y];
+        if(n == l)
+            return ri1*R[last_band + static_cast<size_t>(l-1)*2][y] - rim1*R[last_band][y];
+        return ri0*R[last_band + static_cast<size_t>(n+l-1)][y];
+    };
+
+    auto U = [P](const int l, const int m, const int n, const size_t last_band,
+        const AmbiRotateMatrix &R)
+    {
+        return P(0, l, m, n, last_band, R);
+    };
+    auto V = [P](const int l, const int m, const int n, const size_t last_band,
+        const AmbiRotateMatrix &R)
+    {
+        using namespace al::numbers;
+        if(m > 0)
+        {
+            const bool d{m == 1};
+            const float p0{P( 1, l,  m-1, n, last_band, R)};
+            const float p1{P(-1, l, -m+1, n, last_band, R)};
+            return d ? p0*sqrt2_v<float> : (p0 - p1);
+        }
+        const bool d{m == -1};
+        const float p0{P( 1, l,  m+1, n, last_band, R)};
+        const float p1{P(-1, l, -m-1, n, last_band, R)};
+        return d ? p1*sqrt2_v<float> : (p0 + p1);
+    };
+    auto W = [P](const int l, const int m, const int n, const size_t last_band,
+        const AmbiRotateMatrix &R)
+    {
+        assert(m != 0);
+        if(m > 0)
+        {
+            const float p0{P( 1, l,  m+1, n, last_band, R)};
+            const float p1{P(-1, l, -m-1, n, last_band, R)};
+            return p0 + p1;
+        }
+        const float p0{P( 1, l,  m-1, n, last_band, R)};
+        const float p1{P(-1, l, -m+1, n, last_band, R)};
+        return p0 - p1;
+    };
+
+    // compute rotation matrix of each subsequent band recursively
+    auto coeffs = RotatorCoeffArray.mCoeffs.cbegin();
+    size_t band_idx{4}, last_band{1};
+    for(int l{2};l <= order;++l)
+    {
+        size_t y{band_idx};
+        for(int n{-l};n <= l;++n,++y)
+        {
+            size_t x{band_idx};
+            for(int m{-l};m <= l;++m,++x)
+            {
+                float r{0.0f};
+
+                // computes Eq.8.1
+                const float u{coeffs->u};
+                if(u != 0.0f) r += u * U(l, m, n, last_band, matrix);
+                const float v{coeffs->v};
+                if(v != 0.0f) r += v * V(l, m, n, last_band, matrix);
+                const float w{coeffs->w};
+                if(w != 0.0f) r += w * W(l, m, n, last_band, matrix);
+
+                matrix[y][x] = r;
+                ++coeffs;
+            }
+        }
+        last_band = band_idx;
+        band_idx += static_cast<uint>(l)*size_t{2} + 1;
+    }
+}
+/* End ambisonic rotation helpers. */
+
+
+constexpr float Deg2Rad(float x) noexcept
+{ return static_cast<float>(al::numbers::pi / 180.0 * x); }
+
+struct GainTriplet { float Base, HF, LF; };
+
+void CalcPanningAndFilters(Voice *voice, const float xpos, const float ypos, const float zpos,
+    const float Distance, const float Spread, const GainTriplet &DryGain,
+    const al::span<const GainTriplet,MAX_SENDS> WetGain, EffectSlot *(&SendSlots)[MAX_SENDS],
+    const VoiceProps *props, const ContextParams &Context, DeviceBase *Device)
+{
+    static constexpr ChanMap MonoMap[1]{
+        { FrontCenter, 0.0f, 0.0f }
+    }, RearMap[2]{
+        { BackLeft,  Deg2Rad(-150.0f), Deg2Rad(0.0f) },
+        { BackRight, Deg2Rad( 150.0f), Deg2Rad(0.0f) }
+    }, QuadMap[4]{
+        { FrontLeft,  Deg2Rad( -45.0f), Deg2Rad(0.0f) },
+        { FrontRight, Deg2Rad(  45.0f), Deg2Rad(0.0f) },
+        { BackLeft,   Deg2Rad(-135.0f), Deg2Rad(0.0f) },
+        { BackRight,  Deg2Rad( 135.0f), Deg2Rad(0.0f) }
+    }, X51Map[6]{
+        { FrontLeft,   Deg2Rad( -30.0f), Deg2Rad(0.0f) },
+        { FrontRight,  Deg2Rad(  30.0f), Deg2Rad(0.0f) },
+        { FrontCenter, Deg2Rad(   0.0f), Deg2Rad(0.0f) },
+        { LFE, 0.0f, 0.0f },
+        { SideLeft,    Deg2Rad(-110.0f), Deg2Rad(0.0f) },
+        { SideRight,   Deg2Rad( 110.0f), Deg2Rad(0.0f) }
+    }, X61Map[7]{
+        { FrontLeft,   Deg2Rad(-30.0f), Deg2Rad(0.0f) },
+        { FrontRight,  Deg2Rad( 30.0f), Deg2Rad(0.0f) },
+        { FrontCenter, Deg2Rad(  0.0f), Deg2Rad(0.0f) },
+        { LFE, 0.0f, 0.0f },
+        { BackCenter,  Deg2Rad(180.0f), Deg2Rad(0.0f) },
+        { SideLeft,    Deg2Rad(-90.0f), Deg2Rad(0.0f) },
+        { SideRight,   Deg2Rad( 90.0f), Deg2Rad(0.0f) }
+    }, X71Map[8]{
+        { FrontLeft,   Deg2Rad( -30.0f), Deg2Rad(0.0f) },
+        { FrontRight,  Deg2Rad(  30.0f), Deg2Rad(0.0f) },
+        { FrontCenter, Deg2Rad(   0.0f), Deg2Rad(0.0f) },
+        { LFE, 0.0f, 0.0f },
+        { BackLeft,    Deg2Rad(-150.0f), Deg2Rad(0.0f) },
+        { BackRight,   Deg2Rad( 150.0f), Deg2Rad(0.0f) },
+        { SideLeft,    Deg2Rad( -90.0f), Deg2Rad(0.0f) },
+        { SideRight,   Deg2Rad(  90.0f), Deg2Rad(0.0f) }
+    };
+
+    ChanMap StereoMap[2]{
+        { FrontLeft,  Deg2Rad(-30.0f), Deg2Rad(0.0f) },
+        { FrontRight, Deg2Rad( 30.0f), Deg2Rad(0.0f) }
+    };
+
+    const auto Frequency = static_cast<float>(Device->Frequency);
+    const uint NumSends{Device->NumAuxSends};
+
+    const size_t num_channels{voice->mChans.size()};
+    ASSUME(num_channels > 0);
+
+    for(auto &chandata : voice->mChans)
+    {
+        chandata.mDryParams.Hrtf.Target = HrtfFilter{};
+        chandata.mDryParams.Gains.Target.fill(0.0f);
+        std::for_each(chandata.mWetParams.begin(), chandata.mWetParams.begin()+NumSends,
+            [](SendParams &params) -> void { params.Gains.Target.fill(0.0f); });
+    }
+
+    DirectMode DirectChannels{props->DirectChannels};
+    const ChanMap *chans{nullptr};
+    switch(voice->mFmtChannels)
+    {
+    case FmtMono:
+        chans = MonoMap;
+        /* Mono buffers are never played direct. */
+        DirectChannels = DirectMode::Off;
+        break;
+
+    case FmtStereo:
+        if(DirectChannels == DirectMode::Off)
+        {
+            /* Convert counter-clockwise to clock-wise, and wrap between
+             * [-pi,+pi].
+             */
+            StereoMap[0].angle = WrapRadians(-props->StereoPan[0]);
+            StereoMap[1].angle = WrapRadians(-props->StereoPan[1]);
+        }
+        chans = StereoMap;
+        break;
+
+    case FmtRear: chans = RearMap; break;
+    case FmtQuad: chans = QuadMap; break;
+    case FmtX51: chans = X51Map; break;
+    case FmtX61: chans = X61Map; break;
+    case FmtX71: chans = X71Map; break;
+
+    case FmtBFormat2D:
+    case FmtBFormat3D:
+    case FmtUHJ2:
+    case FmtUHJ3:
+    case FmtUHJ4:
+    case FmtSuperStereo:
+        DirectChannels = DirectMode::Off;
+        break;
+    }
+
+    voice->mFlags.reset(VoiceHasHrtf).reset(VoiceHasNfc);
+    if(auto *decoder{voice->mDecoder.get()})
+        decoder->mWidthControl = minf(props->EnhWidth, 0.7f);
+
+    if(IsAmbisonic(voice->mFmtChannels))
+    {
+        /* Special handling for B-Format and UHJ sources. */
+
+        if(Device->AvgSpeakerDist > 0.0f && voice->mFmtChannels != FmtUHJ2
+            && voice->mFmtChannels != FmtSuperStereo)
+        {
+            if(!(Distance > std::numeric_limits<float>::epsilon()))
+            {
+                /* NOTE: The NFCtrlFilters were created with a w0 of 0, which
+                 * is what we want for FOA input. The first channel may have
+                 * been previously re-adjusted if panned, so reset it.
+                 */
+                voice->mChans[0].mDryParams.NFCtrlFilter.adjust(0.0f);
+            }
+            else
+            {
+                /* Clamp the distance for really close sources, to prevent
+                 * excessive bass.
+                 */
+                const float mdist{maxf(Distance*NfcScale, Device->AvgSpeakerDist/4.0f)};
+                const float w0{SpeedOfSoundMetersPerSec / (mdist * Frequency)};
+
+                /* Only need to adjust the first channel of a B-Format source. */
+                voice->mChans[0].mDryParams.NFCtrlFilter.adjust(w0);
+            }
+
+            voice->mFlags.set(VoiceHasNfc);
+        }
+
+        /* Panning a B-Format sound toward some direction is easy. Just pan the
+         * first (W) channel as a normal mono sound. The angular spread is used
+         * as a directional scalar to blend between full coverage and full
+         * panning.
+         */
+        const float coverage{!(Distance > std::numeric_limits<float>::epsilon()) ? 1.0f :
+            (al::numbers::inv_pi_v<float>/2.0f * Spread)};
+
+        auto calc_coeffs = [xpos,ypos,zpos](RenderMode mode)
+        {
+            if(mode != RenderMode::Pairwise)
+                return CalcDirectionCoeffs({xpos, ypos, zpos});
+
+            /* Clamp Y, in case rounding errors caused it to end up outside
+             * of -1...+1.
+             */
+            const float ev{std::asin(clampf(ypos, -1.0f, 1.0f))};
+            /* Negate Z for right-handed coords with -Z in front. */
+            const float az{std::atan2(xpos, -zpos)};
+
+            /* A scalar of 1.5 for plain stereo results in +/-60 degrees
+             * being moved to +/-90 degrees for direct right and left
+             * speaker responses.
+             */
+            return CalcAngleCoeffs(ScaleAzimuthFront(az, 1.5f), ev, 0.0f);
+        };
+        auto&& scales = GetAmbiScales(voice->mAmbiScaling);
+        auto coeffs = calc_coeffs(Device->mRenderMode);
+
+        if(!(coverage > 0.0f))
+        {
+            ComputePanGains(&Device->Dry, coeffs.data(), DryGain.Base*scales[0],
+                voice->mChans[0].mDryParams.Gains.Target);
+            for(uint i{0};i < NumSends;i++)
+            {
+                if(const EffectSlot *Slot{SendSlots[i]})
+                    ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base*scales[0],
+                        voice->mChans[0].mWetParams[i].Gains.Target);
+            }
+        }
+        else
+        {
+            /* Local B-Format sources have their XYZ channels rotated according
+             * to the orientation.
+             */
+            /* AT then UP */
+            alu::Vector N{props->OrientAt[0], props->OrientAt[1], props->OrientAt[2], 0.0f};
+            N.normalize();
+            alu::Vector V{props->OrientUp[0], props->OrientUp[1], props->OrientUp[2], 0.0f};
+            V.normalize();
+            if(!props->HeadRelative)
+            {
+                N = Context.Matrix * N;
+                V = Context.Matrix * V;
+            }
+            /* Build and normalize right-vector */
+            alu::Vector U{N.cross_product(V)};
+            U.normalize();
+
+            /* Build a rotation matrix. Manually fill the zeroth- and first-
+             * order elements, then construct the rotation for the higher
+             * orders.
+             */
+            AmbiRotateMatrix &shrot = Device->mAmbiRotateMatrix;
+            shrot.fill(AmbiRotateMatrix::value_type{});
+
+            shrot[0][0] = 1.0f;
+            shrot[1][1] =  U[0]; shrot[1][2] = -U[1]; shrot[1][3] =  U[2];
+            shrot[2][1] = -V[0]; shrot[2][2] =  V[1]; shrot[2][3] = -V[2];
+            shrot[3][1] = -N[0]; shrot[3][2] =  N[1]; shrot[3][3] = -N[2];
+            AmbiRotator(shrot, static_cast<int>(Device->mAmbiOrder));
+
+            /* If the device is higher order than the voice, "upsample" the
+             * matrix.
+             *
+             * NOTE: Starting with second-order, a 2D upsample needs to be
+             * applied with a 2D source and 3D output, even when they're the
+             * same order. This is because higher orders have a height offset
+             * on various channels (i.e. when elevation=0, those height-related
+             * channels should be non-0).
+             */
+            AmbiRotateMatrix &mixmatrix = Device->mAmbiRotateMatrix2;
+            if(Device->mAmbiOrder > voice->mAmbiOrder
+                || (Device->mAmbiOrder >= 2 && !Device->m2DMixing
+                    && Is2DAmbisonic(voice->mFmtChannels)))
+            {
+                if(voice->mAmbiOrder == 1)
+                {
+                    auto&& upsampler = Is2DAmbisonic(voice->mFmtChannels) ?
+                        AmbiScale::FirstOrder2DUp : AmbiScale::FirstOrderUp;
+                    UpsampleBFormatTransform(mixmatrix, upsampler, shrot, Device->mAmbiOrder);
+                }
+                else if(voice->mAmbiOrder == 2)
+                {
+                    auto&& upsampler = Is2DAmbisonic(voice->mFmtChannels) ?
+                        AmbiScale::SecondOrder2DUp : AmbiScale::SecondOrderUp;
+                    UpsampleBFormatTransform(mixmatrix, upsampler, shrot, Device->mAmbiOrder);
+                }
+                else if(voice->mAmbiOrder == 3)
+                {
+                    auto&& upsampler = Is2DAmbisonic(voice->mFmtChannels) ?
+                        AmbiScale::ThirdOrder2DUp : AmbiScale::ThirdOrderUp;
+                    UpsampleBFormatTransform(mixmatrix, upsampler, shrot, Device->mAmbiOrder);
+                }
+                else if(voice->mAmbiOrder == 4)
+                {
+                    auto&& upsampler = AmbiScale::FourthOrder2DUp;
+                    UpsampleBFormatTransform(mixmatrix, upsampler, shrot, Device->mAmbiOrder);
+                }
+                else
+                    al::unreachable();
+            }
+            else
+                mixmatrix = shrot;
+
+            /* Convert the rotation matrix for input ordering and scaling, and
+             * whether input is 2D or 3D.
+             */
+            const uint8_t *index_map{Is2DAmbisonic(voice->mFmtChannels) ?
+                GetAmbi2DLayout(voice->mAmbiLayout).data() :
+                GetAmbiLayout(voice->mAmbiLayout).data()};
+
+            /* Scale the panned W signal inversely to coverage (full coverage
+             * means no panned signal), and according to the channel scaling.
+             */
+            std::for_each(coeffs.begin(), coeffs.end(),
+                [scale=(1.0f-coverage)*scales[0]](float &coeff) noexcept { coeff *= scale; });
+
+            for(size_t c{0};c < num_channels;c++)
+            {
+                const size_t acn{index_map[c]};
+                const float scale{scales[acn] * coverage};
+
+                /* For channel 0, combine the B-Format signal (scaled according
+                 * to the coverage amount) with the directional pan. For all
+                 * other channels, use just the (scaled) B-Format signal.
+                 */
+                for(size_t x{0};x < MaxAmbiChannels;++x)
+                    coeffs[x] += mixmatrix[acn][x] * scale;
+
+                ComputePanGains(&Device->Dry, coeffs.data(), DryGain.Base,
+                    voice->mChans[c].mDryParams.Gains.Target);
+
+                for(uint i{0};i < NumSends;i++)
+                {
+                    if(const EffectSlot *Slot{SendSlots[i]})
+                        ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                            voice->mChans[c].mWetParams[i].Gains.Target);
+                }
+
+                coeffs = std::array<float,MaxAmbiChannels>{};
+            }
+        }
+    }
+    else if(DirectChannels != DirectMode::Off && !Device->RealOut.RemixMap.empty())
+    {
+        /* Direct source channels always play local. Skip the virtual channels
+         * and write inputs to the matching real outputs.
+         */
+        voice->mDirect.Buffer = Device->RealOut.Buffer;
+
+        for(size_t c{0};c < num_channels;c++)
+        {
+            uint idx{Device->channelIdxByName(chans[c].channel)};
+            if(idx != InvalidChannelIndex)
+                voice->mChans[c].mDryParams.Gains.Target[idx] = DryGain.Base;
+            else if(DirectChannels == DirectMode::RemixMismatch)
+            {
+                auto match_channel = [chans,c](const InputRemixMap &map) noexcept -> bool
+                { return chans[c].channel == map.channel; };
+                auto remap = std::find_if(Device->RealOut.RemixMap.cbegin(),
+                    Device->RealOut.RemixMap.cend(), match_channel);
+                if(remap != Device->RealOut.RemixMap.cend())
+                {
+                    for(const auto &target : remap->targets)
+                    {
+                        idx = Device->channelIdxByName(target.channel);
+                        if(idx != InvalidChannelIndex)
+                            voice->mChans[c].mDryParams.Gains.Target[idx] = DryGain.Base *
+                                target.mix;
+                    }
+                }
+            }
+        }
+
+        /* Auxiliary sends still use normal channel panning since they mix to
+         * B-Format, which can't channel-match.
+         */
+        for(size_t c{0};c < num_channels;c++)
+        {
+            /* Skip LFE */
+            if(chans[c].channel == LFE)
+                continue;
+
+            const auto coeffs = CalcAngleCoeffs(chans[c].angle, chans[c].elevation, 0.0f);
+
+            for(uint i{0};i < NumSends;i++)
+            {
+                if(const EffectSlot *Slot{SendSlots[i]})
+                    ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                        voice->mChans[c].mWetParams[i].Gains.Target);
+            }
+        }
+    }
+    else if(Device->mRenderMode == RenderMode::Hrtf)
+    {
+        /* Full HRTF rendering. Skip the virtual channels and render to the
+         * real outputs.
+         */
+        voice->mDirect.Buffer = Device->RealOut.Buffer;
+
+        if(Distance > std::numeric_limits<float>::epsilon())
+        {
+            const float src_ev{std::asin(clampf(ypos, -1.0f, 1.0f))};
+            const float src_az{std::atan2(xpos, -zpos)};
+
+            if(voice->mFmtChannels == FmtMono)
+            {
+                Device->mHrtf->getCoeffs(src_ev, src_az, Distance*NfcScale, Spread,
+                    voice->mChans[0].mDryParams.Hrtf.Target.Coeffs,
+                    voice->mChans[0].mDryParams.Hrtf.Target.Delay);
+                voice->mChans[0].mDryParams.Hrtf.Target.Gain = DryGain.Base;
+
+                const auto coeffs = CalcAngleCoeffs(src_az, src_ev, Spread);
+                for(uint i{0};i < NumSends;i++)
+                {
+                    if(const EffectSlot *Slot{SendSlots[i]})
+                        ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                            voice->mChans[0].mWetParams[i].Gains.Target);
+                }
+            }
+            else for(size_t c{0};c < num_channels;c++)
+            {
+                using namespace al::numbers;
+
+                /* Skip LFE */
+                if(chans[c].channel == LFE) continue;
+
+                /* Warp the channel position toward the source position as the
+                 * source spread decreases. With no spread, all channels are at
+                 * the source position, at full spread (pi*2), each channel is
+                 * left unchanged.
+                 */
+                const float ev{lerpf(src_ev, chans[c].elevation, inv_pi_v<float>/2.0f * Spread)};
+
+                float az{chans[c].angle - src_az};
+                if(az < -pi_v<float>) az += pi_v<float>*2.0f;
+                else if(az > pi_v<float>) az -= pi_v<float>*2.0f;
+
+                az *= inv_pi_v<float>/2.0f * Spread;
+
+                az += src_az;
+                if(az < -pi_v<float>) az += pi_v<float>*2.0f;
+                else if(az > pi_v<float>) az -= pi_v<float>*2.0f;
+
+                Device->mHrtf->getCoeffs(ev, az, Distance*NfcScale, 0.0f,
+                    voice->mChans[c].mDryParams.Hrtf.Target.Coeffs,
+                    voice->mChans[c].mDryParams.Hrtf.Target.Delay);
+                voice->mChans[c].mDryParams.Hrtf.Target.Gain = DryGain.Base;
+
+                const auto coeffs = CalcAngleCoeffs(az, ev, 0.0f);
+                for(uint i{0};i < NumSends;i++)
+                {
+                    if(const EffectSlot *Slot{SendSlots[i]})
+                        ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                            voice->mChans[c].mWetParams[i].Gains.Target);
+                }
+            }
+        }
+        else
+        {
+            /* With no distance, spread is only meaningful for mono sources
+             * where it can be 0 or full (non-mono sources are always full
+             * spread here).
+             */
+            const float spread{Spread * (voice->mFmtChannels == FmtMono)};
+
+            /* Local sources on HRTF play with each channel panned to its
+             * relative location around the listener, providing "virtual
+             * speaker" responses.
+             */
+            for(size_t c{0};c < num_channels;c++)
+            {
+                /* Skip LFE */
+                if(chans[c].channel == LFE)
+                    continue;
+
+                /* Get the HRIR coefficients and delays for this channel
+                 * position.
+                 */
+                Device->mHrtf->getCoeffs(chans[c].elevation, chans[c].angle,
+                    std::numeric_limits<float>::infinity(), spread,
+                    voice->mChans[c].mDryParams.Hrtf.Target.Coeffs,
+                    voice->mChans[c].mDryParams.Hrtf.Target.Delay);
+                voice->mChans[c].mDryParams.Hrtf.Target.Gain = DryGain.Base;
+
+                /* Normal panning for auxiliary sends. */
+                const auto coeffs = CalcAngleCoeffs(chans[c].angle, chans[c].elevation, spread);
+
+                for(uint i{0};i < NumSends;i++)
+                {
+                    if(const EffectSlot *Slot{SendSlots[i]})
+                        ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                            voice->mChans[c].mWetParams[i].Gains.Target);
+                }
+            }
+        }
+
+        voice->mFlags.set(VoiceHasHrtf);
+    }
+    else
+    {
+        /* Non-HRTF rendering. Use normal panning to the output. */
+
+        if(Distance > std::numeric_limits<float>::epsilon())
+        {
+            /* Calculate NFC filter coefficient if needed. */
+            if(Device->AvgSpeakerDist > 0.0f)
+            {
+                /* Clamp the distance for really close sources, to prevent
+                 * excessive bass.
+                 */
+                const float mdist{maxf(Distance*NfcScale, Device->AvgSpeakerDist/4.0f)};
+                const float w0{SpeedOfSoundMetersPerSec / (mdist * Frequency)};
+
+                /* Adjust NFC filters. */
+                for(size_t c{0};c < num_channels;c++)
+                    voice->mChans[c].mDryParams.NFCtrlFilter.adjust(w0);
+
+                voice->mFlags.set(VoiceHasNfc);
+            }
+
+            if(voice->mFmtChannels == FmtMono)
+            {
+                auto calc_coeffs = [xpos,ypos,zpos,Spread](RenderMode mode)
+                {
+                    if(mode != RenderMode::Pairwise)
+                        return CalcDirectionCoeffs({xpos, ypos, zpos}, Spread);
+                    const float ev{std::asin(clampf(ypos, -1.0f, 1.0f))};
+                    const float az{std::atan2(xpos, -zpos)};
+                    return CalcAngleCoeffs(ScaleAzimuthFront(az, 1.5f), ev, Spread);
+                };
+                const auto coeffs = calc_coeffs(Device->mRenderMode);
+
+                ComputePanGains(&Device->Dry, coeffs.data(), DryGain.Base,
+                    voice->mChans[0].mDryParams.Gains.Target);
+                for(uint i{0};i < NumSends;i++)
+                {
+                    if(const EffectSlot *Slot{SendSlots[i]})
+                        ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                            voice->mChans[0].mWetParams[i].Gains.Target);
+                }
+            }
+            else
+            {
+                using namespace al::numbers;
+
+                const float src_ev{std::asin(clampf(ypos, -1.0f, 1.0f))};
+                const float src_az{std::atan2(xpos, -zpos)};
+
+                for(size_t c{0};c < num_channels;c++)
+                {
+                    /* Special-case LFE */
+                    if(chans[c].channel == LFE)
+                    {
+                        if(Device->Dry.Buffer.data() == Device->RealOut.Buffer.data())
+                        {
+                            const uint idx{Device->channelIdxByName(chans[c].channel)};
+                            if(idx != InvalidChannelIndex)
+                                voice->mChans[c].mDryParams.Gains.Target[idx] = DryGain.Base;
+                        }
+                        continue;
+                    }
+
+                    /* Warp the channel position toward the source position as
+                     * the spread decreases. With no spread, all channels are
+                     * at the source position, at full spread (pi*2), each
+                     * channel position is left unchanged.
+                     */
+                    const float ev{lerpf(src_ev, chans[c].elevation,
+                        inv_pi_v<float>/2.0f * Spread)};
+
+                    float az{chans[c].angle - src_az};
+                    if(az < -pi_v<float>) az += pi_v<float>*2.0f;
+                    else if(az > pi_v<float>) az -= pi_v<float>*2.0f;
+
+                    az *= inv_pi_v<float>/2.0f * Spread;
+
+                    az += src_az;
+                    if(az < -pi_v<float>) az += pi_v<float>*2.0f;
+                    else if(az > pi_v<float>) az -= pi_v<float>*2.0f;
+
+                    if(Device->mRenderMode == RenderMode::Pairwise)
+                        az = ScaleAzimuthFront(az, 3.0f);
+                    const auto coeffs = CalcAngleCoeffs(az, ev, 0.0f);
+
+                    ComputePanGains(&Device->Dry, coeffs.data(), DryGain.Base,
+                        voice->mChans[c].mDryParams.Gains.Target);
+                    for(uint i{0};i < NumSends;i++)
+                    {
+                        if(const EffectSlot *Slot{SendSlots[i]})
+                            ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                                voice->mChans[c].mWetParams[i].Gains.Target);
+                    }
+                }
+            }
+        }
+        else
+        {
+            if(Device->AvgSpeakerDist > 0.0f)
+            {
+                /* If the source distance is 0, simulate a plane-wave by using
+                 * infinite distance, which results in a w0 of 0.
+                 */
+                static constexpr float w0{0.0f};
+                for(size_t c{0};c < num_channels;c++)
+                    voice->mChans[c].mDryParams.NFCtrlFilter.adjust(w0);
+
+                voice->mFlags.set(VoiceHasNfc);
+            }
+
+            /* With no distance, spread is only meaningful for mono sources
+             * where it can be 0 or full (non-mono sources are always full
+             * spread here).
+             */
+            const float spread{Spread * (voice->mFmtChannels == FmtMono)};
+            for(size_t c{0};c < num_channels;c++)
+            {
+                /* Special-case LFE */
+                if(chans[c].channel == LFE)
+                {
+                    if(Device->Dry.Buffer.data() == Device->RealOut.Buffer.data())
+                    {
+                        const uint idx{Device->channelIdxByName(chans[c].channel)};
+                        if(idx != InvalidChannelIndex)
+                            voice->mChans[c].mDryParams.Gains.Target[idx] = DryGain.Base;
+                    }
+                    continue;
+                }
+
+                const auto coeffs = CalcAngleCoeffs((Device->mRenderMode == RenderMode::Pairwise)
+                    ? ScaleAzimuthFront(chans[c].angle, 3.0f) : chans[c].angle,
+                    chans[c].elevation, spread);
+
+                ComputePanGains(&Device->Dry, coeffs.data(), DryGain.Base,
+                    voice->mChans[c].mDryParams.Gains.Target);
+                for(uint i{0};i < NumSends;i++)
+                {
+                    if(const EffectSlot *Slot{SendSlots[i]})
+                        ComputePanGains(&Slot->Wet, coeffs.data(), WetGain[i].Base,
+                            voice->mChans[c].mWetParams[i].Gains.Target);
+                }
+            }
+        }
+    }
+
+    {
+        const float hfNorm{props->Direct.HFReference / Frequency};
+        const float lfNorm{props->Direct.LFReference / Frequency};
+
+        voice->mDirect.FilterType = AF_None;
+        if(DryGain.HF != 1.0f) voice->mDirect.FilterType |= AF_LowPass;
+        if(DryGain.LF != 1.0f) voice->mDirect.FilterType |= AF_HighPass;
+
+        auto &lowpass = voice->mChans[0].mDryParams.LowPass;
+        auto &highpass = voice->mChans[0].mDryParams.HighPass;
+        lowpass.setParamsFromSlope(BiquadType::HighShelf, hfNorm, DryGain.HF, 1.0f);
+        highpass.setParamsFromSlope(BiquadType::LowShelf, lfNorm, DryGain.LF, 1.0f);
+        for(size_t c{1};c < num_channels;c++)
+        {
+            voice->mChans[c].mDryParams.LowPass.copyParamsFrom(lowpass);
+            voice->mChans[c].mDryParams.HighPass.copyParamsFrom(highpass);
+        }
+    }
+    for(uint i{0};i < NumSends;i++)
+    {
+        const float hfNorm{props->Send[i].HFReference / Frequency};
+        const float lfNorm{props->Send[i].LFReference / Frequency};
+
+        voice->mSend[i].FilterType = AF_None;
+        if(WetGain[i].HF != 1.0f) voice->mSend[i].FilterType |= AF_LowPass;
+        if(WetGain[i].LF != 1.0f) voice->mSend[i].FilterType |= AF_HighPass;
+
+        auto &lowpass = voice->mChans[0].mWetParams[i].LowPass;
+        auto &highpass = voice->mChans[0].mWetParams[i].HighPass;
+        lowpass.setParamsFromSlope(BiquadType::HighShelf, hfNorm, WetGain[i].HF, 1.0f);
+        highpass.setParamsFromSlope(BiquadType::LowShelf, lfNorm, WetGain[i].LF, 1.0f);
+        for(size_t c{1};c < num_channels;c++)
+        {
+            voice->mChans[c].mWetParams[i].LowPass.copyParamsFrom(lowpass);
+            voice->mChans[c].mWetParams[i].HighPass.copyParamsFrom(highpass);
+        }
+    }
+}
+
+void CalcNonAttnSourceParams(Voice *voice, const VoiceProps *props, const ContextBase *context)
+{
+    DeviceBase *Device{context->mDevice};
+    EffectSlot *SendSlots[MAX_SENDS];
+
+    voice->mDirect.Buffer = Device->Dry.Buffer;
+    for(uint i{0};i < Device->NumAuxSends;i++)
+    {
+        SendSlots[i] = props->Send[i].Slot;
+        if(!SendSlots[i] || SendSlots[i]->EffectType == EffectSlotType::None)
+        {
+            SendSlots[i] = nullptr;
+            voice->mSend[i].Buffer = {};
+        }
+        else
+            voice->mSend[i].Buffer = SendSlots[i]->Wet.Buffer;
+    }
+
+    /* Calculate the stepping value */
+    const auto Pitch = static_cast<float>(voice->mFrequency) /
+        static_cast<float>(Device->Frequency) * props->Pitch;
+    if(Pitch > float{MaxPitch})
+        voice->mStep = MaxPitch<<MixerFracBits;
+    else
+        voice->mStep = maxu(fastf2u(Pitch * MixerFracOne), 1);
+    voice->mResampler = PrepareResampler(props->mResampler, voice->mStep, &voice->mResampleState);
+
+    /* Calculate gains */
+    GainTriplet DryGain;
+    DryGain.Base  = minf(clampf(props->Gain, props->MinGain, props->MaxGain) * props->Direct.Gain *
+        context->mParams.Gain, GainMixMax);
+    DryGain.HF = props->Direct.GainHF;
+    DryGain.LF = props->Direct.GainLF;
+    GainTriplet WetGain[MAX_SENDS];
+    for(uint i{0};i < Device->NumAuxSends;i++)
+    {
+        WetGain[i].Base = minf(clampf(props->Gain, props->MinGain, props->MaxGain) *
+            props->Send[i].Gain * context->mParams.Gain, GainMixMax);
+        WetGain[i].HF = props->Send[i].GainHF;
+        WetGain[i].LF = props->Send[i].GainLF;
+    }
+
+    CalcPanningAndFilters(voice, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, DryGain, WetGain, SendSlots, props,
+        context->mParams, Device);
+}
+
+void CalcAttnSourceParams(Voice *voice, const VoiceProps *props, const ContextBase *context)
+{
+    DeviceBase *Device{context->mDevice};
+    const uint NumSends{Device->NumAuxSends};
+
+    /* Set mixing buffers and get send parameters. */
+    voice->mDirect.Buffer = Device->Dry.Buffer;
+    EffectSlot *SendSlots[MAX_SENDS];
+    uint UseDryAttnForRoom{0};
+    for(uint i{0};i < NumSends;i++)
+    {
+        SendSlots[i] = props->Send[i].Slot;
+        if(!SendSlots[i] || SendSlots[i]->EffectType == EffectSlotType::None)
+            SendSlots[i] = nullptr;
+        else if(!SendSlots[i]->AuxSendAuto)
+        {
+            /* If the slot's auxiliary send auto is off, the data sent to the
+             * effect slot is the same as the dry path, sans filter effects.
+             */
+            UseDryAttnForRoom |= 1u<<i;
+        }
+
+        if(!SendSlots[i])
+            voice->mSend[i].Buffer = {};
+        else
+            voice->mSend[i].Buffer = SendSlots[i]->Wet.Buffer;
+    }
+
+    /* Transform source to listener space (convert to head relative) */
+    alu::Vector Position{props->Position[0], props->Position[1], props->Position[2], 1.0f};
+    alu::Vector Velocity{props->Velocity[0], props->Velocity[1], props->Velocity[2], 0.0f};
+    alu::Vector Direction{props->Direction[0], props->Direction[1], props->Direction[2], 0.0f};
+    if(!props->HeadRelative)
+    {
+        /* Transform source vectors */
+        Position = context->mParams.Matrix * (Position - context->mParams.Position);
+        Velocity = context->mParams.Matrix * Velocity;
+        Direction = context->mParams.Matrix * Direction;
+    }
+    else
+    {
+        /* Offset the source velocity to be relative of the listener velocity */
+        Velocity += context->mParams.Velocity;
+    }
+
+    const bool directional{Direction.normalize() > 0.0f};
+    alu::Vector ToSource{Position[0], Position[1], Position[2], 0.0f};
+    const float Distance{ToSource.normalize()};
+
+    /* Calculate distance attenuation */
+    float ClampedDist{Distance};
+    float DryGainBase{props->Gain};
+    float WetGainBase{props->Gain};
+
+    switch(context->mParams.SourceDistanceModel ? props->mDistanceModel
+        : context->mParams.mDistanceModel)
+    {
+        case DistanceModel::InverseClamped:
+            if(props->MaxDistance < props->RefDistance) break;
+            ClampedDist = clampf(ClampedDist, props->RefDistance, props->MaxDistance);
+            /*fall-through*/
+        case DistanceModel::Inverse:
+            if(props->RefDistance > 0.0f)
+            {
+                float dist{lerpf(props->RefDistance, ClampedDist, props->RolloffFactor)};
+                if(dist > 0.0f) DryGainBase *= props->RefDistance / dist;
+
+                dist = lerpf(props->RefDistance, ClampedDist, props->RoomRolloffFactor);
+                if(dist > 0.0f) WetGainBase *= props->RefDistance / dist;
+            }
+            break;
+
+        case DistanceModel::LinearClamped:
+            if(props->MaxDistance < props->RefDistance) break;
+            ClampedDist = clampf(ClampedDist, props->RefDistance, props->MaxDistance);
+            /*fall-through*/
+        case DistanceModel::Linear:
+            if(props->MaxDistance != props->RefDistance)
+            {
+                float attn{(ClampedDist-props->RefDistance) /
+                    (props->MaxDistance-props->RefDistance) * props->RolloffFactor};
+                DryGainBase *= maxf(1.0f - attn, 0.0f);
+
+                attn = (ClampedDist-props->RefDistance) /
+                    (props->MaxDistance-props->RefDistance) * props->RoomRolloffFactor;
+                WetGainBase *= maxf(1.0f - attn, 0.0f);
+            }
+            break;
+
+        case DistanceModel::ExponentClamped:
+            if(props->MaxDistance < props->RefDistance) break;
+            ClampedDist = clampf(ClampedDist, props->RefDistance, props->MaxDistance);
+            /*fall-through*/
+        case DistanceModel::Exponent:
+            if(ClampedDist > 0.0f && props->RefDistance > 0.0f)
+            {
+                const float dist_ratio{ClampedDist/props->RefDistance};
+                DryGainBase *= std::pow(dist_ratio, -props->RolloffFactor);
+                WetGainBase *= std::pow(dist_ratio, -props->RoomRolloffFactor);
+            }
+            break;
+
+        case DistanceModel::Disable:
+            break;
+    }
+
+    /* Calculate directional soundcones */
+    float ConeHF{1.0f}, WetConeHF{1.0f};
+    if(directional && props->InnerAngle < 360.0f)
+    {
+        static constexpr float Rad2Deg{static_cast<float>(180.0 / al::numbers::pi)};
+        const float Angle{Rad2Deg*2.0f * std::acos(-Direction.dot_product(ToSource)) * ConeScale};
+
+        float ConeGain{1.0f};
+        if(Angle >= props->OuterAngle)
+        {
+            ConeGain = props->OuterGain;
+            ConeHF = lerpf(1.0f, props->OuterGainHF, props->DryGainHFAuto);
+        }
+        else if(Angle >= props->InnerAngle)
+        {
+            const float scale{(Angle-props->InnerAngle) / (props->OuterAngle-props->InnerAngle)};
+            ConeGain = lerpf(1.0f, props->OuterGain, scale);
+            ConeHF = lerpf(1.0f, props->OuterGainHF, scale * props->DryGainHFAuto);
+        }
+
+        DryGainBase *= ConeGain;
+        WetGainBase *= lerpf(1.0f, ConeGain, props->WetGainAuto);
+
+        WetConeHF = lerpf(1.0f, ConeHF, props->WetGainHFAuto);
+    }
+
+    /* Apply gain and frequency filters */
+    DryGainBase = clampf(DryGainBase, props->MinGain, props->MaxGain) * context->mParams.Gain;
+    WetGainBase = clampf(WetGainBase, props->MinGain, props->MaxGain) * context->mParams.Gain;
+
+    GainTriplet DryGain{};
+    DryGain.Base = minf(DryGainBase * props->Direct.Gain, GainMixMax);
+    DryGain.HF = ConeHF * props->Direct.GainHF;
+    DryGain.LF = props->Direct.GainLF;
+    GainTriplet WetGain[MAX_SENDS]{};
+    for(uint i{0};i < NumSends;i++)
+    {
+        /* If this effect slot's Auxiliary Send Auto is off, then use the dry
+         * path distance and cone attenuation, otherwise use the wet (room)
+         * path distance and cone attenuation. The send filter is used instead
+         * of the direct filter, regardless.
+         */
+        const bool use_room{!(UseDryAttnForRoom&(1u<<i))};
+        const float gain{use_room ? WetGainBase : DryGainBase};
+        WetGain[i].Base = minf(gain * props->Send[i].Gain, GainMixMax);
+        WetGain[i].HF = (use_room ? WetConeHF : ConeHF) * props->Send[i].GainHF;
+        WetGain[i].LF = props->Send[i].GainLF;
+    }
+
+    /* Distance-based air absorption and initial send decay. */
+    if(Distance > props->RefDistance) LIKELY
+    {
+        const float distance_base{(Distance-props->RefDistance) * props->RolloffFactor};
+        const float distance_meters{distance_base * context->mParams.MetersPerUnit};
+        const float dryabsorb{distance_meters * props->AirAbsorptionFactor};
+        if(dryabsorb > std::numeric_limits<float>::epsilon())
+            DryGain.HF *= std::pow(context->mParams.AirAbsorptionGainHF, dryabsorb);
+
+        /* If the source's Auxiliary Send Filter Gain Auto is off, no extra
+         * adjustment is applied to the send gains.
+         */
+        for(uint i{props->WetGainAuto ? 0u : NumSends};i < NumSends;++i)
+        {
+            if(!SendSlots[i] || !(SendSlots[i]->DecayTime > 0.0f))
+                continue;
+
+            auto calc_attenuation = [](float distance, float refdist, float rolloff) noexcept
+            {
+                const float dist{lerpf(refdist, distance, rolloff)};
+                if(dist > refdist) return refdist / dist;
+                return 1.0f;
+            };
+
+            /* The reverb effect's room rolloff factor always applies to an
+             * inverse distance rolloff model.
+             */
+            WetGain[i].Base *= calc_attenuation(Distance, props->RefDistance,
+                SendSlots[i]->RoomRolloff);
+
+            if(distance_meters > std::numeric_limits<float>::epsilon())
+                WetGain[i].HF *= std::pow(SendSlots[i]->AirAbsorptionGainHF, distance_meters);
+
+            /* If this effect slot's Auxiliary Send Auto is off, don't apply
+             * the automatic initial reverb decay (should the reverb's room
+             * rolloff still apply?).
+             */
+            if(!SendSlots[i]->AuxSendAuto)
+                continue;
+
+            GainTriplet DecayDistance;
+            /* Calculate the distances to where this effect's decay reaches
+             * -60dB.
+             */
+            DecayDistance.Base = SendSlots[i]->DecayTime * SpeedOfSoundMetersPerSec;
+            DecayDistance.LF = DecayDistance.Base * SendSlots[i]->DecayLFRatio;
+            DecayDistance.HF = DecayDistance.Base * SendSlots[i]->DecayHFRatio;
+            if(SendSlots[i]->DecayHFLimit)
+            {
+                const float airAbsorption{SendSlots[i]->AirAbsorptionGainHF};
+                if(airAbsorption < 1.0f)
+                {
+                    /* Calculate the distance to where this effect's air
+                     * absorption reaches -60dB, and limit the effect's HF
+                     * decay distance (so it doesn't take any longer to decay
+                     * than the air would allow).
+                     */
+                    static constexpr float log10_decaygain{-3.0f/*std::log10(ReverbDecayGain)*/};
+                    const float absorb_dist{log10_decaygain / std::log10(airAbsorption)};
+                    DecayDistance.HF = minf(absorb_dist, DecayDistance.HF);
+                }
+            }
+
+            const float baseAttn = calc_attenuation(Distance, props->RefDistance,
+                props->RolloffFactor);
+
+            /* Apply a decay-time transformation to the wet path, based on the
+             * source distance. The initial decay of the reverb effect is
+             * calculated and applied to the wet path.
+             */
+            const float fact{distance_base / DecayDistance.Base};
+            const float gain{std::pow(ReverbDecayGain, fact)*(1.0f-baseAttn) + baseAttn};
+            WetGain[i].Base *= gain;
+
+            if(gain > 0.0f)
+            {
+                const float hffact{distance_base / DecayDistance.HF};
+                const float gainhf{std::pow(ReverbDecayGain, hffact)*(1.0f-baseAttn) + baseAttn};
+                WetGain[i].HF *= minf(gainhf/gain, 1.0f);
+                const float lffact{distance_base / DecayDistance.LF};
+                const float gainlf{std::pow(ReverbDecayGain, lffact)*(1.0f-baseAttn) + baseAttn};
+                WetGain[i].LF *= minf(gainlf/gain, 1.0f);
+            }
+        }
+    }
+
+
+    /* Initial source pitch */
+    float Pitch{props->Pitch};
+
+    /* Calculate velocity-based doppler effect */
+    float DopplerFactor{props->DopplerFactor * context->mParams.DopplerFactor};
+    if(DopplerFactor > 0.0f)
+    {
+        const alu::Vector &lvelocity = context->mParams.Velocity;
+        float vss{Velocity.dot_product(ToSource) * -DopplerFactor};
+        float vls{lvelocity.dot_product(ToSource) * -DopplerFactor};
+
+        const float SpeedOfSound{context->mParams.SpeedOfSound};
+        if(!(vls < SpeedOfSound))
+        {
+            /* Listener moving away from the source at the speed of sound.
+             * Sound waves can't catch it.
+             */
+            Pitch = 0.0f;
+        }
+        else if(!(vss < SpeedOfSound))
+        {
+            /* Source moving toward the listener at the speed of sound. Sound
+             * waves bunch up to extreme frequencies.
+             */
+            Pitch = std::numeric_limits<float>::infinity();
+        }
+        else
+        {
+            /* Source and listener movement is nominal. Calculate the proper
+             * doppler shift.
+             */
+            Pitch *= (SpeedOfSound-vls) / (SpeedOfSound-vss);
+        }
+    }
+
+    /* Adjust pitch based on the buffer and output frequencies, and calculate
+     * fixed-point stepping value.
+     */
+    Pitch *= static_cast<float>(voice->mFrequency) / static_cast<float>(Device->Frequency);
+    if(Pitch > float{MaxPitch})
+        voice->mStep = MaxPitch<<MixerFracBits;
+    else
+        voice->mStep = maxu(fastf2u(Pitch * MixerFracOne), 1);
+    voice->mResampler = PrepareResampler(props->mResampler, voice->mStep, &voice->mResampleState);
+
+    float spread{0.0f};
+    if(props->Radius > Distance)
+        spread = al::numbers::pi_v<float>*2.0f - Distance/props->Radius*al::numbers::pi_v<float>;
+    else if(Distance > 0.0f)
+        spread = std::asin(props->Radius/Distance) * 2.0f;
+
+    CalcPanningAndFilters(voice, ToSource[0]*XScale, ToSource[1]*YScale, ToSource[2]*ZScale,
+        Distance, spread, DryGain, WetGain, SendSlots, props, context->mParams, Device);
+}
+
+void CalcSourceParams(Voice *voice, ContextBase *context, bool force)
+{
+    VoicePropsItem *props{voice->mUpdate.exchange(nullptr, std::memory_order_acq_rel)};
+    if(!props && !force) return;
+
+    if(props)
+    {
+        voice->mProps = *props;
+
+        AtomicReplaceHead(context->mFreeVoiceProps, props);
+    }
+
+    if((voice->mProps.DirectChannels != DirectMode::Off && voice->mFmtChannels != FmtMono
+            && !IsAmbisonic(voice->mFmtChannels))
+        || voice->mProps.mSpatializeMode == SpatializeMode::Off
+        || (voice->mProps.mSpatializeMode==SpatializeMode::Auto && voice->mFmtChannels != FmtMono))
+        CalcNonAttnSourceParams(voice, &voice->mProps, context);
+    else
+        CalcAttnSourceParams(voice, &voice->mProps, context);
+}
+
+
+void SendSourceStateEvent(ContextBase *context, uint id, VChangeState state)
+{
+    RingBuffer *ring{context->mAsyncEvents.get()};
+    auto evt_vec = ring->getWriteVector();
+    if(evt_vec.first.len < 1) return;
+
+    AsyncEvent *evt{al::construct_at(reinterpret_cast<AsyncEvent*>(evt_vec.first.buf),
+        AsyncEvent::SourceStateChange)};
+    evt->u.srcstate.id = id;
+    switch(state)
+    {
+    case VChangeState::Reset:
+        evt->u.srcstate.state = AsyncEvent::SrcState::Reset;
+        break;
+    case VChangeState::Stop:
+        evt->u.srcstate.state = AsyncEvent::SrcState::Stop;
+        break;
+    case VChangeState::Play:
+        evt->u.srcstate.state = AsyncEvent::SrcState::Play;
+        break;
+    case VChangeState::Pause:
+        evt->u.srcstate.state = AsyncEvent::SrcState::Pause;
+        break;
+    /* Shouldn't happen. */
+    case VChangeState::Restart:
+        al::unreachable();
+    }
+
+    ring->writeAdvance(1);
+}
+
+void ProcessVoiceChanges(ContextBase *ctx)
+{
+    VoiceChange *cur{ctx->mCurrentVoiceChange.load(std::memory_order_acquire)};
+    VoiceChange *next{cur->mNext.load(std::memory_order_acquire)};
+    if(!next) return;
+
+    const auto enabledevt = ctx->mEnabledEvts.load(std::memory_order_acquire);
+    do {
+        cur = next;
+
+        bool sendevt{false};
+        if(cur->mState == VChangeState::Reset || cur->mState == VChangeState::Stop)
+        {
+            if(Voice *voice{cur->mVoice})
+            {
+                voice->mCurrentBuffer.store(nullptr, std::memory_order_relaxed);
+                voice->mLoopBuffer.store(nullptr, std::memory_order_relaxed);
+                /* A source ID indicates the voice was playing or paused, which
+                 * gets a reset/stop event.
+                 */
+                sendevt = voice->mSourceID.exchange(0u, std::memory_order_relaxed) != 0u;
+                Voice::State oldvstate{Voice::Playing};
+                voice->mPlayState.compare_exchange_strong(oldvstate, Voice::Stopping,
+                    std::memory_order_relaxed, std::memory_order_acquire);
+                voice->mPendingChange.store(false, std::memory_order_release);
+            }
+            /* Reset state change events are always sent, even if the voice is
+             * already stopped or even if there is no voice.
+             */
+            sendevt |= (cur->mState == VChangeState::Reset);
+        }
+        else if(cur->mState == VChangeState::Pause)
+        {
+            Voice *voice{cur->mVoice};
+            Voice::State oldvstate{Voice::Playing};
+            sendevt = voice->mPlayState.compare_exchange_strong(oldvstate, Voice::Stopping,
+                std::memory_order_release, std::memory_order_acquire);
+        }
+        else if(cur->mState == VChangeState::Play)
+        {
+            /* NOTE: When playing a voice, sending a source state change event
+             * depends if there's an old voice to stop and if that stop is
+             * successful. If there is no old voice, a playing event is always
+             * sent. If there is an old voice, an event is sent only if the
+             * voice is already stopped.
+             */
+            if(Voice *oldvoice{cur->mOldVoice})
+            {
+                oldvoice->mCurrentBuffer.store(nullptr, std::memory_order_relaxed);
+                oldvoice->mLoopBuffer.store(nullptr, std::memory_order_relaxed);
+                oldvoice->mSourceID.store(0u, std::memory_order_relaxed);
+                Voice::State oldvstate{Voice::Playing};
+                sendevt = !oldvoice->mPlayState.compare_exchange_strong(oldvstate, Voice::Stopping,
+                    std::memory_order_relaxed, std::memory_order_acquire);
+                oldvoice->mPendingChange.store(false, std::memory_order_release);
+            }
+            else
+                sendevt = true;
+
+            Voice *voice{cur->mVoice};
+            voice->mPlayState.store(Voice::Playing, std::memory_order_release);
+        }
+        else if(cur->mState == VChangeState::Restart)
+        {
+            /* Restarting a voice never sends a source change event. */
+            Voice *oldvoice{cur->mOldVoice};
+            oldvoice->mCurrentBuffer.store(nullptr, std::memory_order_relaxed);
+            oldvoice->mLoopBuffer.store(nullptr, std::memory_order_relaxed);
+            /* If there's no sourceID, the old voice finished so don't start
+             * the new one at its new offset.
+             */
+            if(oldvoice->mSourceID.exchange(0u, std::memory_order_relaxed) != 0u)
+            {
+                /* Otherwise, set the voice to stopping if it's not already (it
+                 * might already be, if paused), and play the new voice as
+                 * appropriate.
+                 */
+                Voice::State oldvstate{Voice::Playing};
+                oldvoice->mPlayState.compare_exchange_strong(oldvstate, Voice::Stopping,
+                    std::memory_order_relaxed, std::memory_order_acquire);
+
+                Voice *voice{cur->mVoice};
+                voice->mPlayState.store((oldvstate == Voice::Playing) ? Voice::Playing
+                    : Voice::Stopped, std::memory_order_release);
+            }
+            oldvoice->mPendingChange.store(false, std::memory_order_release);
+        }
+        if(sendevt && enabledevt.test(AsyncEvent::SourceStateChange))
+            SendSourceStateEvent(ctx, cur->mSourceID, cur->mState);
+
+        next = cur->mNext.load(std::memory_order_acquire);
+    } while(next);
+    ctx->mCurrentVoiceChange.store(cur, std::memory_order_release);
+}
+
+void ProcessParamUpdates(ContextBase *ctx, const EffectSlotArray &slots,
+    const al::span<Voice*> voices)
+{
+    ProcessVoiceChanges(ctx);
+
+    IncrementRef(ctx->mUpdateCount);
+    if(!ctx->mHoldUpdates.load(std::memory_order_acquire)) LIKELY
+    {
+        bool force{CalcContextParams(ctx)};
+        auto sorted_slots = const_cast<EffectSlot**>(slots.data() + slots.size());
+        for(EffectSlot *slot : slots)
+            force |= CalcEffectSlotParams(slot, sorted_slots, ctx);
+
+        for(Voice *voice : voices)
+        {
+            /* Only update voices that have a source. */
+            if(voice->mSourceID.load(std::memory_order_relaxed) != 0)
+                CalcSourceParams(voice, ctx, force);
+        }
+    }
+    IncrementRef(ctx->mUpdateCount);
+}
+
+void ProcessContexts(DeviceBase *device, const uint SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    const nanoseconds curtime{device->ClockBase +
+        nanoseconds{seconds{device->SamplesDone}}/device->Frequency};
+
+    for(ContextBase *ctx : *device->mContexts.load(std::memory_order_acquire))
+    {
+        const EffectSlotArray &auxslots = *ctx->mActiveAuxSlots.load(std::memory_order_acquire);
+        const al::span<Voice*> voices{ctx->getVoicesSpanAcquired()};
+
+        /* Process pending propery updates for objects on the context. */
+        ProcessParamUpdates(ctx, auxslots, voices);
+
+        /* Clear auxiliary effect slot mixing buffers. */
+        for(EffectSlot *slot : auxslots)
+        {
+            for(auto &buffer : slot->Wet.Buffer)
+                buffer.fill(0.0f);
+        }
+
+        /* Process voices that have a playing source. */
+        for(Voice *voice : voices)
+        {
+            const Voice::State vstate{voice->mPlayState.load(std::memory_order_acquire)};
+            if(vstate != Voice::Stopped && vstate != Voice::Pending)
+                voice->mix(vstate, ctx, curtime, SamplesToDo);
+        }
+
+        /* Process effects. */
+        if(const size_t num_slots{auxslots.size()})
+        {
+            auto slots = auxslots.data();
+            auto slots_end = slots + num_slots;
+
+            /* Sort the slots into extra storage, so that effect slots come
+             * before their effect slot target (or their targets' target).
+             */
+            const al::span<EffectSlot*> sorted_slots{const_cast<EffectSlot**>(slots_end),
+                num_slots};
+            /* Skip sorting if it has already been done. */
+            if(!sorted_slots[0])
+            {
+                /* First, copy the slots to the sorted list, then partition the
+                 * sorted list so that all slots without a target slot go to
+                 * the end.
+                 */
+                std::copy(slots, slots_end, sorted_slots.begin());
+                auto split_point = std::partition(sorted_slots.begin(), sorted_slots.end(),
+                    [](const EffectSlot *slot) noexcept -> bool
+                    { return slot->Target != nullptr; });
+                /* There must be at least one slot without a slot target. */
+                assert(split_point != sorted_slots.end());
+
+                /* Simple case: no more than 1 slot has a target slot. Either
+                 * all slots go right to the output, or the remaining one must
+                 * target an already-partitioned slot.
+                 */
+                if(split_point - sorted_slots.begin() > 1)
+                {
+                    /* At least two slots target other slots. Starting from the
+                     * back of the sorted list, continue partitioning the front
+                     * of the list given each target until all targets are
+                     * accounted for. This ensures all slots without a target
+                     * go last, all slots directly targeting those last slots
+                     * go second-to-last, all slots directly targeting those
+                     * second-last slots go third-to-last, etc.
+                     */
+                    auto next_target = sorted_slots.end();
+                    do {
+                        /* This shouldn't happen, but if there's unsorted slots
+                         * left that don't target any sorted slots, they can't
+                         * contribute to the output, so leave them.
+                         */
+                        if(next_target == split_point) UNLIKELY
+                            break;
+
+                        --next_target;
+                        split_point = std::partition(sorted_slots.begin(), split_point,
+                            [next_target](const EffectSlot *slot) noexcept -> bool
+                            { return slot->Target != *next_target; });
+                    } while(split_point - sorted_slots.begin() > 1);
+                }
+            }
+
+            for(const EffectSlot *slot : sorted_slots)
+            {
+                EffectState *state{slot->mEffectState.get()};
+                state->process(SamplesToDo, slot->Wet.Buffer, state->mOutTarget);
+            }
+        }
+
+        /* Signal the event handler if there are any events to read. */
+        RingBuffer *ring{ctx->mAsyncEvents.get()};
+        if(ring->readSpace() > 0)
+            ctx->mEventSem.post();
+    }
+}
+
+
+void ApplyDistanceComp(const al::span<FloatBufferLine> Samples, const size_t SamplesToDo,
+    const DistanceComp::ChanData *distcomp)
+{
+    ASSUME(SamplesToDo > 0);
+
+    for(auto &chanbuffer : Samples)
+    {
+        const float gain{distcomp->Gain};
+        const size_t base{distcomp->Length};
+        float *distbuf{al::assume_aligned<16>(distcomp->Buffer)};
+        ++distcomp;
+
+        if(base < 1)
+            continue;
+
+        float *inout{al::assume_aligned<16>(chanbuffer.data())};
+        auto inout_end = inout + SamplesToDo;
+        if(SamplesToDo >= base) LIKELY
+        {
+            auto delay_end = std::rotate(inout, inout_end - base, inout_end);
+            std::swap_ranges(inout, delay_end, distbuf);
+        }
+        else
+        {
+            auto delay_start = std::swap_ranges(inout, inout_end, distbuf);
+            std::rotate(distbuf, delay_start, distbuf + base);
+        }
+        std::transform(inout, inout_end, inout, [gain](float s) { return s * gain; });
+    }
+}
+
+void ApplyDither(const al::span<FloatBufferLine> Samples, uint *dither_seed,
+    const float quant_scale, const size_t SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    /* Dithering. Generate whitenoise (uniform distribution of random values
+     * between -1 and +1) and add it to the sample values, after scaling up to
+     * the desired quantization depth amd before rounding.
+     */
+    const float invscale{1.0f / quant_scale};
+    uint seed{*dither_seed};
+    auto dither_sample = [&seed,invscale,quant_scale](const float sample) noexcept -> float
+    {
+        float val{sample * quant_scale};
+        uint rng0{dither_rng(&seed)};
+        uint rng1{dither_rng(&seed)};
+        val += static_cast<float>(rng0*(1.0/UINT_MAX) - rng1*(1.0/UINT_MAX));
+        return fast_roundf(val) * invscale;
+    };
+    for(FloatBufferLine &inout : Samples)
+        std::transform(inout.begin(), inout.begin()+SamplesToDo, inout.begin(), dither_sample);
+    *dither_seed = seed;
+}
+
+
+/* Base template left undefined. Should be marked =delete, but Clang 3.8.1
+ * chokes on that given the inline specializations.
+ */
+template<typename T>
+inline T SampleConv(float) noexcept;
+
+template<> inline float SampleConv(float val) noexcept
+{ return val; }
+template<> inline int32_t SampleConv(float val) noexcept
+{
+    /* Floats have a 23-bit mantissa, plus an implied 1 bit and a sign bit.
+     * This means a normalized float has at most 25 bits of signed precision.
+     * When scaling and clamping for a signed 32-bit integer, these following
+     * values are the best a float can give.
+     */
+    return fastf2i(clampf(val*2147483648.0f, -2147483648.0f, 2147483520.0f));
+}
+template<> inline int16_t SampleConv(float val) noexcept
+{ return static_cast<int16_t>(fastf2i(clampf(val*32768.0f, -32768.0f, 32767.0f))); }
+template<> inline int8_t SampleConv(float val) noexcept
+{ return static_cast<int8_t>(fastf2i(clampf(val*128.0f, -128.0f, 127.0f))); }
+
+/* Define unsigned output variations. */
+template<> inline uint32_t SampleConv(float val) noexcept
+{ return static_cast<uint32_t>(SampleConv<int32_t>(val)) + 2147483648u; }
+template<> inline uint16_t SampleConv(float val) noexcept
+{ return static_cast<uint16_t>(SampleConv<int16_t>(val) + 32768); }
+template<> inline uint8_t SampleConv(float val) noexcept
+{ return static_cast<uint8_t>(SampleConv<int8_t>(val) + 128); }
+
+template<DevFmtType T>
+void Write(const al::span<const FloatBufferLine> InBuffer, void *OutBuffer, const size_t Offset,
+    const size_t SamplesToDo, const size_t FrameStep)
+{
+    ASSUME(FrameStep > 0);
+    ASSUME(SamplesToDo > 0);
+
+    DevFmtType_t<T> *outbase{static_cast<DevFmtType_t<T>*>(OutBuffer) + Offset*FrameStep};
+    size_t c{0};
+    for(const FloatBufferLine &inbuf : InBuffer)
+    {
+        DevFmtType_t<T> *out{outbase++};
+        auto conv_sample = [FrameStep,&out](const float s) noexcept -> void
+        {
+            *out = SampleConv<DevFmtType_t<T>>(s);
+            out += FrameStep;
+        };
+        std::for_each(inbuf.begin(), inbuf.begin()+SamplesToDo, conv_sample);
+        ++c;
+    }
+    if(const size_t extra{FrameStep - c})
+    {
+        const auto silence = SampleConv<DevFmtType_t<T>>(0.0f);
+        for(size_t i{0};i < SamplesToDo;++i)
+        {
+            std::fill_n(outbase, extra, silence);
+            outbase += FrameStep;
+        }
+    }
+}
+
+} // namespace
+
+uint DeviceBase::renderSamples(const uint numSamples)
+{
+    const uint samplesToDo{minu(numSamples, BufferLineSize)};
+
+    /* Clear main mixing buffers. */
+    for(FloatBufferLine &buffer : MixBuffer)
+        buffer.fill(0.0f);
+
+    /* Increment the mix count at the start (lsb should now be 1). */
+    IncrementRef(MixCount);
+
+    /* Process and mix each context's sources and effects. */
+    ProcessContexts(this, samplesToDo);
+
+    /* Increment the clock time. Every second's worth of samples is converted
+     * and added to clock base so that large sample counts don't overflow
+     * during conversion. This also guarantees a stable conversion.
+     */
+    SamplesDone += samplesToDo;
+    ClockBase += std::chrono::seconds{SamplesDone / Frequency};
+    SamplesDone %= Frequency;
+
+    /* Increment the mix count at the end (lsb should now be 0). */
+    IncrementRef(MixCount);
+
+    /* Apply any needed post-process for finalizing the Dry mix to the RealOut
+     * (Ambisonic decode, UHJ encode, etc).
+     */
+    postProcess(samplesToDo);
+
+    /* Apply compression, limiting sample amplitude if needed or desired. */
+    if(Limiter) Limiter->process(samplesToDo, RealOut.Buffer.data());
+
+    /* Apply delays and attenuation for mismatched speaker distances. */
+    if(ChannelDelays)
+        ApplyDistanceComp(RealOut.Buffer, samplesToDo, ChannelDelays->mChannels.data());
+
+    /* Apply dithering. The compressor should have left enough headroom for the
+     * dither noise to not saturate.
+     */
+    if(DitherDepth > 0.0f)
+        ApplyDither(RealOut.Buffer, &DitherSeed, DitherDepth, samplesToDo);
+
+    return samplesToDo;
+}
+
+void DeviceBase::renderSamples(const al::span<float*> outBuffers, const uint numSamples)
+{
+    FPUCtl mixer_mode{};
+    uint total{0};
+    while(const uint todo{numSamples - total})
+    {
+        const uint samplesToDo{renderSamples(todo)};
+
+        auto *srcbuf = RealOut.Buffer.data();
+        for(auto *dstbuf : outBuffers)
+        {
+            std::copy_n(srcbuf->data(), samplesToDo, dstbuf + total);
+            ++srcbuf;
+        }
+
+        total += samplesToDo;
+    }
+}
+
+void DeviceBase::renderSamples(void *outBuffer, const uint numSamples, const size_t frameStep)
+{
+    FPUCtl mixer_mode{};
+    uint total{0};
+    while(const uint todo{numSamples - total})
+    {
+        const uint samplesToDo{renderSamples(todo)};
+
+        if(outBuffer) LIKELY
+        {
+            /* Finally, interleave and convert samples, writing to the device's
+             * output buffer.
+             */
+            switch(FmtType)
+            {
+#define HANDLE_WRITE(T) case T:                                               \
+    Write<T>(RealOut.Buffer, outBuffer, total, samplesToDo, frameStep); break;
+            HANDLE_WRITE(DevFmtByte)
+            HANDLE_WRITE(DevFmtUByte)
+            HANDLE_WRITE(DevFmtShort)
+            HANDLE_WRITE(DevFmtUShort)
+            HANDLE_WRITE(DevFmtInt)
+            HANDLE_WRITE(DevFmtUInt)
+            HANDLE_WRITE(DevFmtFloat)
+#undef HANDLE_WRITE
+            }
+        }
+
+        total += samplesToDo;
+    }
+}
+
+void DeviceBase::handleDisconnect(const char *msg, ...)
+{
+    IncrementRef(MixCount);
+    if(Connected.exchange(false, std::memory_order_acq_rel))
+    {
+        AsyncEvent evt{AsyncEvent::Disconnected};
+
+        va_list args;
+        va_start(args, msg);
+        int msglen{vsnprintf(evt.u.disconnect.msg, sizeof(evt.u.disconnect.msg), msg, args)};
+        va_end(args);
+
+        if(msglen < 0 || static_cast<size_t>(msglen) >= sizeof(evt.u.disconnect.msg))
+            evt.u.disconnect.msg[sizeof(evt.u.disconnect.msg)-1] = 0;
+
+        for(ContextBase *ctx : *mContexts.load())
+        {
+            if(ctx->mEnabledEvts.load(std::memory_order_acquire).test(AsyncEvent::Disconnected))
+            {
+                RingBuffer *ring{ctx->mAsyncEvents.get()};
+                auto evt_data = ring->getWriteVector().first;
+                if(evt_data.len > 0)
+                {
+                    al::construct_at(reinterpret_cast<AsyncEvent*>(evt_data.buf), evt);
+                    ring->writeAdvance(1);
+                    ctx->mEventSem.post();
+                }
+            }
+
+            if(!ctx->mStopVoicesOnDisconnect)
+            {
+                ProcessVoiceChanges(ctx);
+                continue;
+            }
+
+            auto voicelist = ctx->getVoicesSpanAcquired();
+            auto stop_voice = [](Voice *voice) -> void
+            {
+                voice->mCurrentBuffer.store(nullptr, std::memory_order_relaxed);
+                voice->mLoopBuffer.store(nullptr, std::memory_order_relaxed);
+                voice->mSourceID.store(0u, std::memory_order_relaxed);
+                voice->mPlayState.store(Voice::Stopped, std::memory_order_release);
+            };
+            std::for_each(voicelist.begin(), voicelist.end(), stop_voice);
+        }
+    }
+    IncrementRef(MixCount);
+}
diff --git a/alc/alu.h b/alc/alu.h
new file mode 100644 (file)
index 0000000..67fd09e
--- /dev/null
+++ b/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, const float nfcscale);
+
+/* 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
diff --git a/alc/backends/alsa.cpp b/alc/backends/alsa.cpp
new file mode 100644 (file)
index 0000000..d620a83
--- /dev/null
@@ -0,0 +1,1272 @@
+/**
+ * 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 "alsa.h"
+
+#include <algorithm>
+#include <atomic>
+#include <cassert>
+#include <cerrno>
+#include <chrono>
+#include <cstring>
+#include <exception>
+#include <functional>
+#include <memory>
+#include <string>
+#include <thread>
+#include <utility>
+
+#include "albyte.h"
+#include "alc/alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "dynload.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "vector.h"
+
+#include <alsa/asoundlib.h>
+
+
+namespace {
+
+constexpr char alsaDevice[] = "ALSA Default";
+
+
+#ifdef HAVE_DYNLOAD
+#define ALSA_FUNCS(MAGIC)                                                     \
+    MAGIC(snd_strerror);                                                      \
+    MAGIC(snd_pcm_open);                                                      \
+    MAGIC(snd_pcm_close);                                                     \
+    MAGIC(snd_pcm_nonblock);                                                  \
+    MAGIC(snd_pcm_frames_to_bytes);                                           \
+    MAGIC(snd_pcm_bytes_to_frames);                                           \
+    MAGIC(snd_pcm_hw_params_malloc);                                          \
+    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_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_test_format);                                     \
+    MAGIC(snd_pcm_hw_params_test_channels);                                   \
+    MAGIC(snd_pcm_hw_params);                                                 \
+    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_prepare);                                                   \
+    MAGIC(snd_pcm_start);                                                     \
+    MAGIC(snd_pcm_resume);                                                    \
+    MAGIC(snd_pcm_reset);                                                     \
+    MAGIC(snd_pcm_wait);                                                      \
+    MAGIC(snd_pcm_delay);                                                     \
+    MAGIC(snd_pcm_state);                                                     \
+    MAGIC(snd_pcm_avail_update);                                              \
+    MAGIC(snd_pcm_mmap_begin);                                                \
+    MAGIC(snd_pcm_mmap_commit);                                               \
+    MAGIC(snd_pcm_readi);                                                     \
+    MAGIC(snd_pcm_writei);                                                    \
+    MAGIC(snd_pcm_drain);                                                     \
+    MAGIC(snd_pcm_drop);                                                      \
+    MAGIC(snd_pcm_recover);                                                   \
+    MAGIC(snd_pcm_info_malloc);                                               \
+    MAGIC(snd_pcm_info_free);                                                 \
+    MAGIC(snd_pcm_info_set_device);                                           \
+    MAGIC(snd_pcm_info_set_subdevice);                                        \
+    MAGIC(snd_pcm_info_set_stream);                                           \
+    MAGIC(snd_pcm_info_get_name);                                             \
+    MAGIC(snd_ctl_pcm_next_device);                                           \
+    MAGIC(snd_ctl_pcm_info);                                                  \
+    MAGIC(snd_ctl_open);                                                      \
+    MAGIC(snd_ctl_close);                                                     \
+    MAGIC(snd_ctl_card_info_malloc);                                          \
+    MAGIC(snd_ctl_card_info_free);                                            \
+    MAGIC(snd_ctl_card_info);                                                 \
+    MAGIC(snd_ctl_card_info_get_name);                                        \
+    MAGIC(snd_ctl_card_info_get_id);                                          \
+    MAGIC(snd_card_next);                                                     \
+    MAGIC(snd_config_update_free_global)
+
+void *alsa_handle;
+#define MAKE_FUNC(f) decltype(f) * p##f
+ALSA_FUNCS(MAKE_FUNC);
+#undef MAKE_FUNC
+
+#ifndef IN_IDE_PARSER
+#define snd_strerror psnd_strerror
+#define snd_pcm_open psnd_pcm_open
+#define snd_pcm_close psnd_pcm_close
+#define snd_pcm_nonblock psnd_pcm_nonblock
+#define snd_pcm_frames_to_bytes psnd_pcm_frames_to_bytes
+#define snd_pcm_bytes_to_frames psnd_pcm_bytes_to_frames
+#define snd_pcm_hw_params_malloc psnd_pcm_hw_params_malloc
+#define snd_pcm_hw_params_free psnd_pcm_hw_params_free
+#define snd_pcm_hw_params_any psnd_pcm_hw_params_any
+#define snd_pcm_hw_params_current psnd_pcm_hw_params_current
+#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
+#define snd_pcm_hw_params_set_rate_resample psnd_pcm_hw_params_set_rate_resample
+#define snd_pcm_hw_params_set_buffer_time_near psnd_pcm_hw_params_set_buffer_time_near
+#define snd_pcm_hw_params_set_period_time_near psnd_pcm_hw_params_set_period_time_near
+#define snd_pcm_hw_params_set_buffer_size_near psnd_pcm_hw_params_set_buffer_size_near
+#define snd_pcm_hw_params_set_period_size_near psnd_pcm_hw_params_set_period_size_near
+#define snd_pcm_hw_params_set_buffer_size_min psnd_pcm_hw_params_set_buffer_size_min
+#define snd_pcm_hw_params_get_buffer_time_min psnd_pcm_hw_params_get_buffer_time_min
+#define snd_pcm_hw_params_get_buffer_time_max psnd_pcm_hw_params_get_buffer_time_max
+#define snd_pcm_hw_params_get_period_time_min psnd_pcm_hw_params_get_period_time_min
+#define snd_pcm_hw_params_get_period_time_max psnd_pcm_hw_params_get_period_time_max
+#define snd_pcm_hw_params_get_buffer_size psnd_pcm_hw_params_get_buffer_size
+#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
+#define snd_pcm_sw_params_malloc psnd_pcm_sw_params_malloc
+#define snd_pcm_sw_params_current psnd_pcm_sw_params_current
+#define snd_pcm_sw_params_set_avail_min psnd_pcm_sw_params_set_avail_min
+#define snd_pcm_sw_params_set_stop_threshold psnd_pcm_sw_params_set_stop_threshold
+#define snd_pcm_sw_params psnd_pcm_sw_params
+#define snd_pcm_sw_params_free psnd_pcm_sw_params_free
+#define snd_pcm_prepare psnd_pcm_prepare
+#define snd_pcm_start psnd_pcm_start
+#define snd_pcm_resume psnd_pcm_resume
+#define snd_pcm_reset psnd_pcm_reset
+#define snd_pcm_wait psnd_pcm_wait
+#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_mmap_begin psnd_pcm_mmap_begin
+#define snd_pcm_mmap_commit psnd_pcm_mmap_commit
+#define snd_pcm_readi psnd_pcm_readi
+#define snd_pcm_writei psnd_pcm_writei
+#define snd_pcm_drain psnd_pcm_drain
+#define snd_pcm_drop psnd_pcm_drop
+#define snd_pcm_recover psnd_pcm_recover
+#define snd_pcm_info_malloc psnd_pcm_info_malloc
+#define snd_pcm_info_free psnd_pcm_info_free
+#define snd_pcm_info_set_device psnd_pcm_info_set_device
+#define snd_pcm_info_set_subdevice psnd_pcm_info_set_subdevice
+#define snd_pcm_info_set_stream psnd_pcm_info_set_stream
+#define snd_pcm_info_get_name psnd_pcm_info_get_name
+#define snd_ctl_pcm_next_device psnd_ctl_pcm_next_device
+#define snd_ctl_pcm_info psnd_ctl_pcm_info
+#define snd_ctl_open psnd_ctl_open
+#define snd_ctl_close psnd_ctl_close
+#define snd_ctl_card_info_malloc psnd_ctl_card_info_malloc
+#define snd_ctl_card_info_free psnd_ctl_card_info_free
+#define snd_ctl_card_info psnd_ctl_card_info
+#define snd_ctl_card_info_get_name psnd_ctl_card_info_get_name
+#define snd_ctl_card_info_get_id psnd_ctl_card_info_get_id
+#define snd_card_next psnd_card_next
+#define snd_config_update_free_global psnd_config_update_free_global
+#endif
+#endif
+
+
+struct HwParamsDeleter {
+    void operator()(snd_pcm_hw_params_t *ptr) { snd_pcm_hw_params_free(ptr); }
+};
+using HwParamsPtr = std::unique_ptr<snd_pcm_hw_params_t,HwParamsDeleter>;
+HwParamsPtr CreateHwParams()
+{
+    snd_pcm_hw_params_t *hp{};
+    snd_pcm_hw_params_malloc(&hp);
+    return HwParamsPtr{hp};
+}
+
+struct SwParamsDeleter {
+    void operator()(snd_pcm_sw_params_t *ptr) { snd_pcm_sw_params_free(ptr); }
+};
+using SwParamsPtr = std::unique_ptr<snd_pcm_sw_params_t,SwParamsDeleter>;
+SwParamsPtr CreateSwParams()
+{
+    snd_pcm_sw_params_t *sp{};
+    snd_pcm_sw_params_malloc(&sp);
+    return SwParamsPtr{sp};
+}
+
+
+struct DevMap {
+    std::string name;
+    std::string device_name;
+
+    template<typename T, typename U>
+    DevMap(T&& name_, U&& devname)
+        : name{std::forward<T>(name_)}, device_name{std::forward<U>(devname)}
+    { }
+};
+
+al::vector<DevMap> PlaybackDevices;
+al::vector<DevMap> CaptureDevices;
+
+
+const char *prefix_name(snd_pcm_stream_t stream)
+{
+    assert(stream == SND_PCM_STREAM_PLAYBACK || stream == SND_PCM_STREAM_CAPTURE);
+    return (stream==SND_PCM_STREAM_PLAYBACK) ? "device-prefix" : "capture-prefix";
+}
+
+al::vector<DevMap> probe_devices(snd_pcm_stream_t stream)
+{
+    al::vector<DevMap> devlist;
+
+    snd_ctl_card_info_t *info;
+    snd_ctl_card_info_malloc(&info);
+    snd_pcm_info_t *pcminfo;
+    snd_pcm_info_malloc(&pcminfo);
+
+    auto defname = ConfigValueStr(nullptr, "alsa",
+        (stream == SND_PCM_STREAM_PLAYBACK) ? "device" : "capture");
+    devlist.emplace_back(alsaDevice, defname ? defname->c_str() : "default");
+
+    if(auto customdevs = ConfigValueStr(nullptr, "alsa",
+        (stream == SND_PCM_STREAM_PLAYBACK) ? "custom-devices" : "custom-captures"))
+    {
+        size_t nextpos{customdevs->find_first_not_of(';')};
+        size_t curpos;
+        while((curpos=nextpos) < customdevs->length())
+        {
+            nextpos = customdevs->find_first_of(';', curpos+1);
+
+            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(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{
+        ConfigValueStr(nullptr, "alsa", prefix_name(stream)).value_or("plughw:")};
+
+    int card{-1};
+    int err{snd_card_next(&card)};
+    for(;err >= 0 && card >= 0;err = snd_card_next(&card))
+    {
+        std::string name{"hw:" + std::to_string(card)};
+
+        snd_ctl_t *handle;
+        if((err=snd_ctl_open(&handle, name.c_str(), 0)) < 0)
+        {
+            ERR("control open (hw:%d): %s\n", card, snd_strerror(err));
+            continue;
+        }
+        if((err=snd_ctl_card_info(handle, info)) < 0)
+        {
+            ERR("control hardware info (hw:%d): %s\n", card, snd_strerror(err));
+            snd_ctl_close(handle);
+            continue;
+        }
+
+        const char *cardname{snd_ctl_card_info_get_name(info)};
+        const char *cardid{snd_ctl_card_info_get_id(info)};
+        name = prefix_name(stream);
+        name += '-';
+        name += cardid;
+        const std::string card_prefix{
+            ConfigValueStr(nullptr, "alsa", name.c_str()).value_or(main_prefix)};
+
+        int dev{-1};
+        while(true)
+        {
+            if(snd_ctl_pcm_next_device(handle, &dev) < 0)
+                ERR("snd_ctl_pcm_next_device failed\n");
+            if(dev < 0) break;
+
+            snd_pcm_info_set_device(pcminfo, static_cast<uint>(dev));
+            snd_pcm_info_set_subdevice(pcminfo, 0);
+            snd_pcm_info_set_stream(pcminfo, stream);
+            if((err=snd_ctl_pcm_info(handle, pcminfo)) < 0)
+            {
+                if(err != -ENOENT)
+                    ERR("control digital audio info (hw:%d): %s\n", card, snd_strerror(err));
+                continue;
+            }
+
+            /* "prefix-cardid-dev" */
+            name = prefix_name(stream);
+            name += '-';
+            name += cardid;
+            name += '-';
+            name += std::to_string(dev);
+            const std::string device_prefix{
+                ConfigValueStr(nullptr, "alsa", name.c_str()).value_or(card_prefix)};
+
+            /* "CardName, PcmName (CARD=cardid,DEV=dev)" */
+            name = cardname;
+            name += ", ";
+            name += snd_pcm_info_get_name(pcminfo);
+            name += " (CARD=";
+            name += cardid;
+            name += ",DEV=";
+            name += std::to_string(dev);
+            name += ')';
+
+            /* "devprefixCARD=cardid,DEV=dev" */
+            std::string device{device_prefix};
+            device += "CARD=";
+            device += cardid;
+            device += ",DEV=";
+            device += std::to_string(dev);
+            
+            devlist.emplace_back(std::move(name), std::move(device));
+            const auto &entry = devlist.back();
+            TRACE("Got device \"%s\", \"%s\"\n", entry.name.c_str(), entry.device_name.c_str());
+        }
+        snd_ctl_close(handle);
+    }
+    if(err < 0)
+        ERR("snd_card_next failed: %s\n", snd_strerror(err));
+
+    snd_pcm_info_free(pcminfo);
+    snd_ctl_card_info_free(info);
+
+    return devlist;
+}
+
+
+int verify_state(snd_pcm_t *handle)
+{
+    snd_pcm_state_t state{snd_pcm_state(handle)};
+
+    int err;
+    switch(state)
+    {
+        case SND_PCM_STATE_OPEN:
+        case SND_PCM_STATE_SETUP:
+        case SND_PCM_STATE_PREPARED:
+        case SND_PCM_STATE_RUNNING:
+        case SND_PCM_STATE_DRAINING:
+        case SND_PCM_STATE_PAUSED:
+            /* All Okay */
+            break;
+
+        case SND_PCM_STATE_XRUN:
+            if((err=snd_pcm_recover(handle, -EPIPE, 1)) < 0)
+                return err;
+            break;
+        case SND_PCM_STATE_SUSPENDED:
+            if((err=snd_pcm_recover(handle, -ESTRPIPE, 1)) < 0)
+                return err;
+            break;
+        case SND_PCM_STATE_DISCONNECTED:
+            return -ENODEV;
+    }
+
+    return state;
+}
+
+
+struct AlsaPlayback final : public BackendBase {
+    AlsaPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~AlsaPlayback() override;
+
+    int mixerProc();
+    int mixerNoMMapProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    ClockLatency getClockLatency() override;
+
+    snd_pcm_t *mPcmHandle{nullptr};
+
+    std::mutex mMutex;
+
+    uint mFrameStep{};
+    al::vector<al::byte> mBuffer;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(AlsaPlayback)
+};
+
+AlsaPlayback::~AlsaPlayback()
+{
+    if(mPcmHandle)
+        snd_pcm_close(mPcmHandle);
+    mPcmHandle = nullptr;
+}
+
+
+int AlsaPlayback::mixerProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    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))
+    {
+        int state{verify_state(mPcmHandle)};
+        if(state < 0)
+        {
+            ERR("Invalid state detected: %s\n", snd_strerror(state));
+            mDevice->handleDisconnect("Bad state: %s", snd_strerror(state));
+            break;
+        }
+
+        snd_pcm_sframes_t avails{snd_pcm_avail_update(mPcmHandle)};
+        if(avails < 0)
+        {
+            ERR("available update failed: %s\n", snd_strerror(static_cast<int>(avails)));
+            continue;
+        }
+        snd_pcm_uframes_t avail{static_cast<snd_pcm_uframes_t>(avails)};
+
+        if(avail > buffer_size)
+        {
+            WARN("available samples exceeds the buffer size\n");
+            snd_pcm_reset(mPcmHandle);
+            continue;
+        }
+
+        // make sure there's frames to process
+        if(avail < update_size)
+        {
+            if(state != SND_PCM_STATE_RUNNING)
+            {
+                int err{snd_pcm_start(mPcmHandle)};
+                if(err < 0)
+                {
+                    ERR("start failed: %s\n", snd_strerror(err));
+                    continue;
+                }
+            }
+            if(snd_pcm_wait(mPcmHandle, 1000) == 0)
+                ERR("Wait timeout... buffer size too low?\n");
+            continue;
+        }
+        avail -= avail%update_size;
+
+        // it is possible that contiguous areas are smaller, thus we use a loop
+        std::lock_guard<std::mutex> _{mMutex};
+        while(avail > 0)
+        {
+            snd_pcm_uframes_t frames{avail};
+
+            const snd_pcm_channel_area_t *areas{};
+            snd_pcm_uframes_t offset{};
+            int err{snd_pcm_mmap_begin(mPcmHandle, &areas, &offset, &frames)};
+            if(err < 0)
+            {
+                ERR("mmap begin error: %s\n", snd_strerror(err));
+                break;
+            }
+
+            char *WritePtr{static_cast<char*>(areas->addr) + (offset * areas->step / 8)};
+            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)
+            {
+                ERR("mmap commit error: %s\n",
+                    snd_strerror(commitres >= 0 ? -EPIPE : static_cast<int>(commitres)));
+                break;
+            }
+
+            avail -= frames;
+        }
+    }
+
+    return 0;
+}
+
+int AlsaPlayback::mixerNoMMapProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    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))
+    {
+        int state{verify_state(mPcmHandle)};
+        if(state < 0)
+        {
+            ERR("Invalid state detected: %s\n", snd_strerror(state));
+            mDevice->handleDisconnect("Bad state: %s", snd_strerror(state));
+            break;
+        }
+
+        snd_pcm_sframes_t avail{snd_pcm_avail_update(mPcmHandle)};
+        if(avail < 0)
+        {
+            ERR("available update failed: %s\n", snd_strerror(static_cast<int>(avail)));
+            continue;
+        }
+
+        if(static_cast<snd_pcm_uframes_t>(avail) > buffer_size)
+        {
+            WARN("available samples exceeds the buffer size\n");
+            snd_pcm_reset(mPcmHandle);
+            continue;
+        }
+
+        if(static_cast<snd_pcm_uframes_t>(avail) < update_size)
+        {
+            if(state != SND_PCM_STATE_RUNNING)
+            {
+                int err{snd_pcm_start(mPcmHandle)};
+                if(err < 0)
+                {
+                    ERR("start failed: %s\n", snd_strerror(err));
+                    continue;
+                }
+            }
+            if(snd_pcm_wait(mPcmHandle, 1000) == 0)
+                ERR("Wait timeout... buffer size too low?\n");
+            continue;
+        }
+
+        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), mFrameStep);
+        while(avail > 0)
+        {
+            snd_pcm_sframes_t ret{snd_pcm_writei(mPcmHandle, WritePtr,
+                static_cast<snd_pcm_uframes_t>(avail))};
+            switch(ret)
+            {
+            case -EAGAIN:
+                continue;
+#if ESTRPIPE != EPIPE
+            case -ESTRPIPE:
+#endif
+            case -EPIPE:
+            case -EINTR:
+                ret = snd_pcm_recover(mPcmHandle, static_cast<int>(ret), 1);
+                if(ret < 0)
+                    avail = 0;
+                break;
+            default:
+                if(ret >= 0)
+                {
+                    WritePtr += snd_pcm_frames_to_bytes(mPcmHandle, ret);
+                    avail -= ret;
+                }
+                break;
+            }
+            if(ret < 0)
+            {
+                ret = snd_pcm_prepare(mPcmHandle);
+                if(ret < 0) break;
+            }
+        }
+    }
+
+    return 0;
+}
+
+
+void AlsaPlayback::open(const char *name)
+{
+    std::string 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; });
+        if(iter == PlaybackDevices.cend())
+            throw al::backend_exception{al::backend_error::NoDevice,
+                "Device name \"%s\" not found", name};
+        driver = iter->device_name;
+    }
+    else
+    {
+        name = alsaDevice;
+        if(auto driveropt = ConfigValueStr(nullptr, "alsa", "device"))
+            driver = std::move(driveropt).value();
+    }
+    TRACE("Opening device \"%s\"\n", driver.c_str());
+
+    snd_pcm_t *pcmHandle{};
+    int err{snd_pcm_open(&pcmHandle, driver.c_str(), 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.c_str()};
+    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();
+
+    mDevice->DeviceName = name;
+}
+
+bool AlsaPlayback::reset()
+{
+    snd_pcm_format_t format{SND_PCM_FORMAT_UNKNOWN};
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        format = SND_PCM_FORMAT_S8;
+        break;
+    case DevFmtUByte:
+        format = SND_PCM_FORMAT_U8;
+        break;
+    case DevFmtShort:
+        format = SND_PCM_FORMAT_S16;
+        break;
+    case DevFmtUShort:
+        format = SND_PCM_FORMAT_U16;
+        break;
+    case DevFmtInt:
+        format = SND_PCM_FORMAT_S32;
+        break;
+    case DevFmtUInt:
+        format = SND_PCM_FORMAT_U32;
+        break;
+    case DevFmtFloat:
+        format = SND_PCM_FORMAT_FLOAT;
+        break;
+    }
+
+    bool allowmmap{!!GetConfigValueBool(mDevice->DeviceName.c_str(), "alsa", "mmap", true)};
+    uint periodLen{static_cast<uint>(mDevice->UpdateSize * 1000000_u64 / mDevice->Frequency)};
+    uint bufferLen{static_cast<uint>(mDevice->BufferSize * 1000000_u64 / mDevice->Frequency)};
+    uint rate{mDevice->Frequency};
+
+    int err{};
+    HwParamsPtr hp{CreateHwParams()};
+#define CHECK(x) do {                                                         \
+    if((err=(x)) < 0)                                                         \
+        throw al::backend_exception{al::backend_error::DeviceError, #x " failed: %s", \
+            snd_strerror(err)};                                               \
+} while(0)
+    CHECK(snd_pcm_hw_params_any(mPcmHandle, hp.get()));
+    /* set interleaved access */
+    if(!allowmmap
+        || snd_pcm_hw_params_set_access(mPcmHandle, hp.get(), SND_PCM_ACCESS_MMAP_INTERLEAVED) < 0)
+    {
+        /* No mmap */
+        CHECK(snd_pcm_hw_params_set_access(mPcmHandle, hp.get(), SND_PCM_ACCESS_RW_INTERLEAVED));
+    }
+    /* test and set format (implicitly sets sample bits) */
+    if(snd_pcm_hw_params_test_format(mPcmHandle, hp.get(), format) < 0)
+    {
+        static const struct {
+            snd_pcm_format_t format;
+            DevFmtType fmttype;
+        } formatlist[] = {
+            { SND_PCM_FORMAT_FLOAT, DevFmtFloat  },
+            { SND_PCM_FORMAT_S32,   DevFmtInt    },
+            { SND_PCM_FORMAT_U32,   DevFmtUInt   },
+            { SND_PCM_FORMAT_S16,   DevFmtShort  },
+            { SND_PCM_FORMAT_U16,   DevFmtUShort },
+            { SND_PCM_FORMAT_S8,    DevFmtByte   },
+            { SND_PCM_FORMAT_U8,    DevFmtUByte  },
+        };
+
+        for(const auto &fmt : formatlist)
+        {
+            format = fmt.format;
+            if(snd_pcm_hw_params_test_format(mPcmHandle, hp.get(), format) >= 0)
+            {
+                mDevice->FmtType = fmt.fmttype;
+                break;
+            }
+        }
+    }
+    CHECK(snd_pcm_hw_params_set_format(mPcmHandle, hp.get(), format));
+    /* set channels (implicitly sets frame bits) */
+    if(snd_pcm_hw_params_set_channels(mPcmHandle, hp.get(), mDevice->channelsFromFmt()) < 0)
+    {
+        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;
+    }
+    /* set rate (implicitly constrains period/buffer parameters) */
+    if(!GetConfigValueBool(mDevice->DeviceName.c_str(), "alsa", "allow-resampler", false)
+        || !mDevice->Flags.test(FrequencyRequest))
+    {
+        if(snd_pcm_hw_params_set_rate_resample(mPcmHandle, hp.get(), 0) < 0)
+            WARN("Failed to disable ALSA resampler\n");
+    }
+    else if(snd_pcm_hw_params_set_rate_resample(mPcmHandle, hp.get(), 1) < 0)
+        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)
+        ERR("snd_pcm_hw_params_set_period_time_near failed: %s\n", snd_strerror(err));
+    /* set buffer time (implicitly sets buffer size/bytes/time and period size/bytes) */
+    if((err=snd_pcm_hw_params_set_buffer_time_near(mPcmHandle, hp.get(), &bufferLen, nullptr)) < 0)
+        ERR("snd_pcm_hw_params_set_buffer_time_near failed: %s\n", snd_strerror(err));
+    /* install and prepare hardware configuration */
+    CHECK(snd_pcm_hw_params(mPcmHandle, hp.get()));
+
+    /* retrieve configuration info */
+    snd_pcm_uframes_t periodSizeInFrames{};
+    snd_pcm_uframes_t bufferSizeInFrames{};
+    snd_pcm_access_t access{};
+
+    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()};
+    CHECK(snd_pcm_sw_params_current(mPcmHandle, sp.get()));
+    CHECK(snd_pcm_sw_params_set_avail_min(mPcmHandle, sp.get(), periodSizeInFrames));
+    CHECK(snd_pcm_sw_params_set_stop_threshold(mPcmHandle, sp.get(), bufferSizeInFrames));
+    CHECK(snd_pcm_sw_params(mPcmHandle, sp.get()));
+#undef CHECK
+    sp = nullptr;
+
+    mDevice->BufferSize = static_cast<uint>(bufferSizeInFrames);
+    mDevice->UpdateSize = static_cast<uint>(periodSizeInFrames);
+    mDevice->Frequency = rate;
+
+    setDefaultChannelOrder();
+
+    return true;
+}
+
+void AlsaPlayback::start()
+{
+    int err{};
+    snd_pcm_access_t access{};
+    HwParamsPtr hp{CreateHwParams()};
+#define CHECK(x) do {                                                         \
+    if((err=(x)) < 0)                                                         \
+        throw al::backend_exception{al::backend_error::DeviceError, #x " failed: %s", \
+            snd_strerror(err)};                                               \
+} while(0)
+    CHECK(snd_pcm_hw_params_current(mPcmHandle, hp.get()));
+    /* retrieve configuration info */
+    CHECK(snd_pcm_hw_params_get_access(hp.get(), &access));
+    hp = nullptr;
+
+    int (AlsaPlayback::*thread_func)(){};
+    if(access == SND_PCM_ACCESS_RW_INTERLEAVED)
+    {
+        auto datalen = snd_pcm_frames_to_bytes(mPcmHandle, mDevice->UpdateSize);
+        mBuffer.resize(static_cast<size_t>(datalen));
+        thread_func = &AlsaPlayback::mixerNoMMapProc;
+    }
+    else
+    {
+        CHECK(snd_pcm_prepare(mPcmHandle));
+        thread_func = &AlsaPlayback::mixerProc;
+    }
+#undef CHECK
+
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(thread_func), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void AlsaPlayback::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    mBuffer.clear();
+    int err{snd_pcm_drop(mPcmHandle)};
+    if(err < 0)
+        ERR("snd_pcm_drop failed: %s\n", snd_strerror(err));
+}
+
+ClockLatency AlsaPlayback::getClockLatency()
+{
+    ClockLatency ret;
+
+    std::lock_guard<std::mutex> _{mMutex};
+    ret.ClockTime = GetDeviceClockTime(mDevice);
+    snd_pcm_sframes_t delay{};
+    int err{snd_pcm_delay(mPcmHandle, &delay)};
+    if(err < 0)
+    {
+        ERR("Failed to get pcm delay: %s\n", snd_strerror(err));
+        delay = 0;
+    }
+    ret.Latency  = std::chrono::seconds{std::max<snd_pcm_sframes_t>(0, delay)};
+    ret.Latency /= mDevice->Frequency;
+
+    return ret;
+}
+
+
+struct AlsaCapture final : public BackendBase {
+    AlsaCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~AlsaCapture() override;
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+    ClockLatency getClockLatency() override;
+
+    snd_pcm_t *mPcmHandle{nullptr};
+
+    al::vector<al::byte> mBuffer;
+
+    bool mDoCapture{false};
+    RingBufferPtr mRing{nullptr};
+
+    snd_pcm_sframes_t mLastAvail{0};
+
+    DEF_NEWDEL(AlsaCapture)
+};
+
+AlsaCapture::~AlsaCapture()
+{
+    if(mPcmHandle)
+        snd_pcm_close(mPcmHandle);
+    mPcmHandle = nullptr;
+}
+
+
+void AlsaCapture::open(const char *name)
+{
+    std::string 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; });
+        if(iter == CaptureDevices.cend())
+            throw al::backend_exception{al::backend_error::NoDevice,
+                "Device name \"%s\" not found", name};
+        driver = iter->device_name;
+    }
+    else
+    {
+        name = alsaDevice;
+        if(auto driveropt = ConfigValueStr(nullptr, "alsa", "capture"))
+            driver = std::move(driveropt).value();
+    }
+
+    TRACE("Opening device \"%s\"\n", driver.c_str());
+    int err{snd_pcm_open(&mPcmHandle, driver.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)};
+    if(err < 0)
+        throw al::backend_exception{al::backend_error::NoDevice,
+            "Could not open ALSA device \"%s\"", driver.c_str()};
+
+    /* Free alsa's global config tree. Otherwise valgrind reports a ton of leaks. */
+    snd_config_update_free_global();
+
+    snd_pcm_format_t format{SND_PCM_FORMAT_UNKNOWN};
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        format = SND_PCM_FORMAT_S8;
+        break;
+    case DevFmtUByte:
+        format = SND_PCM_FORMAT_U8;
+        break;
+    case DevFmtShort:
+        format = SND_PCM_FORMAT_S16;
+        break;
+    case DevFmtUShort:
+        format = SND_PCM_FORMAT_U16;
+        break;
+    case DevFmtInt:
+        format = SND_PCM_FORMAT_S32;
+        break;
+    case DevFmtUInt:
+        format = SND_PCM_FORMAT_U32;
+        break;
+    case DevFmtFloat:
+        format = SND_PCM_FORMAT_FLOAT;
+        break;
+    }
+
+    snd_pcm_uframes_t bufferSizeInFrames{maxu(mDevice->BufferSize, 100*mDevice->Frequency/1000)};
+    snd_pcm_uframes_t periodSizeInFrames{minu(mDevice->BufferSize, 25*mDevice->Frequency/1000)};
+
+    bool needring{false};
+    HwParamsPtr hp{CreateHwParams()};
+#define CHECK(x) do {                                                         \
+    if((err=(x)) < 0)                                                         \
+        throw al::backend_exception{al::backend_error::DeviceError, #x " failed: %s", \
+            snd_strerror(err)};                                               \
+} while(0)
+    CHECK(snd_pcm_hw_params_any(mPcmHandle, hp.get()));
+    /* set interleaved access */
+    CHECK(snd_pcm_hw_params_set_access(mPcmHandle, hp.get(), SND_PCM_ACCESS_RW_INTERLEAVED));
+    /* set format (implicitly sets sample bits) */
+    CHECK(snd_pcm_hw_params_set_format(mPcmHandle, hp.get(), format));
+    /* set channels (implicitly sets frame bits) */
+    CHECK(snd_pcm_hw_params_set_channels(mPcmHandle, hp.get(), mDevice->channelsFromFmt()));
+    /* set rate (implicitly constrains period/buffer parameters) */
+    CHECK(snd_pcm_hw_params_set_rate(mPcmHandle, hp.get(), mDevice->Frequency, 0));
+    /* set buffer size in frame units (implicitly sets period size/bytes/time and buffer time/bytes) */
+    if(snd_pcm_hw_params_set_buffer_size_min(mPcmHandle, hp.get(), &bufferSizeInFrames) < 0)
+    {
+        TRACE("Buffer too large, using intermediate ring buffer\n");
+        needring = true;
+        CHECK(snd_pcm_hw_params_set_buffer_size_near(mPcmHandle, hp.get(), &bufferSizeInFrames));
+    }
+    /* set buffer size in frame units (implicitly sets period size/bytes/time and buffer time/bytes) */
+    CHECK(snd_pcm_hw_params_set_period_size_near(mPcmHandle, hp.get(), &periodSizeInFrames, nullptr));
+    /* install and prepare hardware configuration */
+    CHECK(snd_pcm_hw_params(mPcmHandle, hp.get()));
+    /* retrieve configuration info */
+    CHECK(snd_pcm_hw_params_get_period_size(hp.get(), &periodSizeInFrames, nullptr));
+#undef CHECK
+    hp = nullptr;
+
+    if(needring)
+        mRing = RingBuffer::Create(mDevice->BufferSize, mDevice->frameSizeFromFmt(), false);
+
+    mDevice->DeviceName = name;
+}
+
+
+void AlsaCapture::start()
+{
+    int err{snd_pcm_prepare(mPcmHandle)};
+    if(err < 0)
+        throw al::backend_exception{al::backend_error::DeviceError, "snd_pcm_prepare failed: %s",
+            snd_strerror(err)};
+
+    err = snd_pcm_start(mPcmHandle);
+    if(err < 0)
+        throw al::backend_exception{al::backend_error::DeviceError, "snd_pcm_start failed: %s",
+            snd_strerror(err)};
+
+    mDoCapture = true;
+}
+
+void AlsaCapture::stop()
+{
+    /* OpenAL requires access to unread audio after stopping, but ALSA's
+     * snd_pcm_drain is unreliable and snd_pcm_drop drops it. Capture what's
+     * available now so it'll be available later after the drop.
+     */
+    uint avail{availableSamples()};
+    if(!mRing && avail > 0)
+    {
+        /* The ring buffer implicitly captures when checking availability.
+         * Direct access needs to explicitly capture it into temp storage.
+         */
+        auto temp = al::vector<al::byte>(
+            static_cast<size_t>(snd_pcm_frames_to_bytes(mPcmHandle, avail)));
+        captureSamples(temp.data(), avail);
+        mBuffer = std::move(temp);
+    }
+    int err{snd_pcm_drop(mPcmHandle)};
+    if(err < 0)
+        ERR("drop failed: %s\n", snd_strerror(err));
+    mDoCapture = false;
+}
+
+void AlsaCapture::captureSamples(al::byte *buffer, uint samples)
+{
+    if(mRing)
+    {
+        mRing->read(buffer, samples);
+        return;
+    }
+
+    mLastAvail -= samples;
+    while(mDevice->Connected.load(std::memory_order_acquire) && samples > 0)
+    {
+        snd_pcm_sframes_t amt{0};
+
+        if(!mBuffer.empty())
+        {
+            /* First get any data stored from the last stop */
+            amt = snd_pcm_bytes_to_frames(mPcmHandle, static_cast<ssize_t>(mBuffer.size()));
+            if(static_cast<snd_pcm_uframes_t>(amt) > samples) amt = samples;
+
+            amt = snd_pcm_frames_to_bytes(mPcmHandle, amt);
+            std::copy_n(mBuffer.begin(), amt, buffer);
+
+            mBuffer.erase(mBuffer.begin(), mBuffer.begin()+amt);
+            amt = snd_pcm_bytes_to_frames(mPcmHandle, amt);
+        }
+        else if(mDoCapture)
+            amt = snd_pcm_readi(mPcmHandle, buffer, samples);
+        if(amt < 0)
+        {
+            ERR("read error: %s\n", snd_strerror(static_cast<int>(amt)));
+
+            if(amt == -EAGAIN)
+                continue;
+            if((amt=snd_pcm_recover(mPcmHandle, static_cast<int>(amt), 1)) >= 0)
+            {
+                amt = snd_pcm_start(mPcmHandle);
+                if(amt >= 0)
+                    amt = snd_pcm_avail_update(mPcmHandle);
+            }
+            if(amt < 0)
+            {
+                const char *err{snd_strerror(static_cast<int>(amt))};
+                ERR("restore error: %s\n", err);
+                mDevice->handleDisconnect("Capture recovery failure: %s", err);
+                break;
+            }
+            /* If the amount available is less than what's asked, we lost it
+             * during recovery. So just give silence instead. */
+            if(static_cast<snd_pcm_uframes_t>(amt) < samples)
+                break;
+            continue;
+        }
+
+        buffer = buffer + amt;
+        samples -= static_cast<uint>(amt);
+    }
+    if(samples > 0)
+        std::fill_n(buffer, snd_pcm_frames_to_bytes(mPcmHandle, samples),
+            al::byte((mDevice->FmtType == DevFmtUByte) ? 0x80 : 0));
+}
+
+uint AlsaCapture::availableSamples()
+{
+    snd_pcm_sframes_t avail{0};
+    if(mDevice->Connected.load(std::memory_order_acquire) && mDoCapture)
+        avail = snd_pcm_avail_update(mPcmHandle);
+    if(avail < 0)
+    {
+        ERR("avail update failed: %s\n", snd_strerror(static_cast<int>(avail)));
+
+        if((avail=snd_pcm_recover(mPcmHandle, static_cast<int>(avail), 1)) >= 0)
+        {
+            if(mDoCapture)
+                avail = snd_pcm_start(mPcmHandle);
+            if(avail >= 0)
+                avail = snd_pcm_avail_update(mPcmHandle);
+        }
+        if(avail < 0)
+        {
+            const char *err{snd_strerror(static_cast<int>(avail))};
+            ERR("restore error: %s\n", err);
+            mDevice->handleDisconnect("Capture recovery failure: %s", err);
+        }
+    }
+
+    if(!mRing)
+    {
+        if(avail < 0) avail = 0;
+        avail += snd_pcm_bytes_to_frames(mPcmHandle, static_cast<ssize_t>(mBuffer.size()));
+        if(avail > mLastAvail) mLastAvail = avail;
+        return static_cast<uint>(mLastAvail);
+    }
+
+    while(avail > 0)
+    {
+        auto vec = mRing->getWriteVector();
+        if(vec.first.len == 0) break;
+
+        snd_pcm_sframes_t amt{std::min(static_cast<snd_pcm_sframes_t>(vec.first.len), avail)};
+        amt = snd_pcm_readi(mPcmHandle, vec.first.buf, static_cast<snd_pcm_uframes_t>(amt));
+        if(amt < 0)
+        {
+            ERR("read error: %s\n", snd_strerror(static_cast<int>(amt)));
+
+            if(amt == -EAGAIN)
+                continue;
+            if((amt=snd_pcm_recover(mPcmHandle, static_cast<int>(amt), 1)) >= 0)
+            {
+                if(mDoCapture)
+                    amt = snd_pcm_start(mPcmHandle);
+                if(amt >= 0)
+                    amt = snd_pcm_avail_update(mPcmHandle);
+            }
+            if(amt < 0)
+            {
+                const char *err{snd_strerror(static_cast<int>(amt))};
+                ERR("restore error: %s\n", err);
+                mDevice->handleDisconnect("Capture recovery failure: %s", err);
+                break;
+            }
+            avail = amt;
+            continue;
+        }
+
+        mRing->writeAdvance(static_cast<snd_pcm_uframes_t>(amt));
+        avail -= amt;
+    }
+
+    return static_cast<uint>(mRing->readSpace());
+}
+
+ClockLatency AlsaCapture::getClockLatency()
+{
+    ClockLatency ret;
+
+    ret.ClockTime = GetDeviceClockTime(mDevice);
+    snd_pcm_sframes_t delay{};
+    int err{snd_pcm_delay(mPcmHandle, &delay)};
+    if(err < 0)
+    {
+        ERR("Failed to get pcm delay: %s\n", snd_strerror(err));
+        delay = 0;
+    }
+    ret.Latency  = std::chrono::seconds{std::max<snd_pcm_sframes_t>(0, delay)};
+    ret.Latency /= mDevice->Frequency;
+
+    return ret;
+}
+
+} // namespace
+
+
+bool AlsaBackendFactory::init()
+{
+    bool error{false};
+
+#ifdef HAVE_DYNLOAD
+    if(!alsa_handle)
+    {
+        std::string missing_funcs;
+
+        alsa_handle = LoadLib("libasound.so.2");
+        if(!alsa_handle)
+        {
+            WARN("Failed to load %s\n", "libasound.so.2");
+            return false;
+        }
+
+        error = false;
+#define LOAD_FUNC(f) do {                                                     \
+    p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(alsa_handle, #f));      \
+    if(p##f == nullptr) {                                                     \
+        error = true;                                                         \
+        missing_funcs += "\n" #f;                                             \
+    }                                                                         \
+} while(0)
+        ALSA_FUNCS(LOAD_FUNC);
+#undef LOAD_FUNC
+
+        if(error)
+        {
+            WARN("Missing expected functions:%s\n", missing_funcs.c_str());
+            CloseLib(alsa_handle);
+            alsa_handle = nullptr;
+        }
+    }
+#endif
+
+    return !error;
+}
+
+bool AlsaBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+std::string AlsaBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+
+    auto add_device = [&outnames](const DevMap &entry) -> void
+    {
+        /* +1 to also append the null char (to ensure a null-separated list and
+         * double-null terminated list).
+         */
+        outnames.append(entry.name.c_str(), entry.name.length()+1);
+    };
+    switch(type)
+    {
+    case BackendType::Playback:
+        PlaybackDevices = probe_devices(SND_PCM_STREAM_PLAYBACK);
+        std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+        break;
+
+    case BackendType::Capture:
+        CaptureDevices = probe_devices(SND_PCM_STREAM_CAPTURE);
+        std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+        break;
+    }
+
+    return outnames;
+}
+
+BackendPtr AlsaBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new AlsaPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new AlsaCapture{device}};
+    return nullptr;
+}
+
+BackendFactory &AlsaBackendFactory::getFactory()
+{
+    static AlsaBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/alsa.h b/alc/backends/alsa.h
new file mode 100644 (file)
index 0000000..b256dcf
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_ALSA_H
+#define BACKENDS_ALSA_H
+
+#include "base.h"
+
+struct AlsaBackendFactory 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_ALSA_H */
diff --git a/alc/backends/base.cpp b/alc/backends/base.cpp
new file mode 100644 (file)
index 0000000..e5ad849
--- /dev/null
@@ -0,0 +1,202 @@
+
+#include "config.h"
+
+#include "base.h"
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <mmreg.h>
+
+#include "albit.h"
+#include "core/logging.h"
+#include "aloptional.h"
+#endif
+
+#include "atomic.h"
+#include "core/devformat.h"
+
+
+namespace al {
+
+backend_exception::backend_exception(backend_error code, const char *msg, ...) : mErrorCode{code}
+{
+    std::va_list args;
+    va_start(args, msg);
+    setMessage(msg, args);
+    va_end(args);
+}
+backend_exception::~backend_exception() = default;
+
+} // namespace al
+
+
+bool BackendBase::reset()
+{ throw al::backend_exception{al::backend_error::DeviceError, "Invalid BackendBase call"}; }
+
+void BackendBase::captureSamples(al::byte*, uint)
+{ }
+
+uint BackendBase::availableSamples()
+{ return 0; }
+
+ClockLatency BackendBase::getClockLatency()
+{
+    ClockLatency ret;
+
+    uint refcount;
+    do {
+        refcount = mDevice->waitForMix();
+        ret.ClockTime = GetDeviceClockTime(mDevice);
+        std::atomic_thread_fence(std::memory_order_acquire);
+    } while(refcount != ReadRef(mDevice->MixCount));
+
+    /* NOTE: The device will generally have about all but one periods filled at
+     * any given time during playback. Without a more accurate measurement from
+     * the output, this is an okay approximation.
+     */
+    ret.Latency = std::max(std::chrono::seconds{mDevice->BufferSize-mDevice->UpdateSize},
+        std::chrono::seconds::zero());
+    ret.Latency /= mDevice->Frequency;
+
+    return ret;
+}
+
+void BackendBase::setDefaultWFXChannelOrder()
+{
+    mDevice->RealOut.ChannelIndex.fill(InvalidChannelIndex);
+
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+        mDevice->RealOut.ChannelIndex[FrontCenter] = 0;
+        break;
+    case DevFmtStereo:
+        mDevice->RealOut.ChannelIndex[FrontLeft]  = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight] = 1;
+        break;
+    case DevFmtQuad:
+        mDevice->RealOut.ChannelIndex[FrontLeft]  = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight] = 1;
+        mDevice->RealOut.ChannelIndex[BackLeft]   = 2;
+        mDevice->RealOut.ChannelIndex[BackRight]  = 3;
+        break;
+    case DevFmtX51:
+        mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
+        mDevice->RealOut.ChannelIndex[FrontCenter] = 2;
+        mDevice->RealOut.ChannelIndex[LFE]         = 3;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 4;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 5;
+        break;
+    case DevFmtX61:
+        mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
+        mDevice->RealOut.ChannelIndex[FrontCenter] = 2;
+        mDevice->RealOut.ChannelIndex[LFE]         = 3;
+        mDevice->RealOut.ChannelIndex[BackCenter]  = 4;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 5;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 6;
+        break;
+    case DevFmtX71:
+        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;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 6;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 7;
+        break;
+    case DevFmtX714:
+        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;
+        mDevice->RealOut.ChannelIndex[SideLeft]      = 6;
+        mDevice->RealOut.ChannelIndex[SideRight]     = 7;
+        mDevice->RealOut.ChannelIndex[TopFrontLeft]  = 8;
+        mDevice->RealOut.ChannelIndex[TopFrontRight] = 9;
+        mDevice->RealOut.ChannelIndex[TopBackLeft]   = 10;
+        mDevice->RealOut.ChannelIndex[TopBackRight]  = 11;
+        break;
+    case DevFmtX3D71:
+        mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
+        mDevice->RealOut.ChannelIndex[FrontCenter] = 2;
+        mDevice->RealOut.ChannelIndex[LFE]         = 3;
+        mDevice->RealOut.ChannelIndex[Aux0]        = 4;
+        mDevice->RealOut.ChannelIndex[Aux1]        = 5;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 6;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 7;
+        break;
+    case DevFmtAmbi3D:
+        break;
+    }
+}
+
+void BackendBase::setDefaultChannelOrder()
+{
+    mDevice->RealOut.ChannelIndex.fill(InvalidChannelIndex);
+
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtX51:
+        mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 2;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 3;
+        mDevice->RealOut.ChannelIndex[FrontCenter] = 4;
+        mDevice->RealOut.ChannelIndex[LFE]         = 5;
+        return;
+    case DevFmtX71:
+        mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
+        mDevice->RealOut.ChannelIndex[BackLeft]    = 2;
+        mDevice->RealOut.ChannelIndex[BackRight]   = 3;
+        mDevice->RealOut.ChannelIndex[FrontCenter] = 4;
+        mDevice->RealOut.ChannelIndex[LFE]         = 5;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 6;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 7;
+        return;
+    case DevFmtX714:
+        mDevice->RealOut.ChannelIndex[FrontLeft]     = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight]    = 1;
+        mDevice->RealOut.ChannelIndex[BackLeft]      = 2;
+        mDevice->RealOut.ChannelIndex[BackRight]     = 3;
+        mDevice->RealOut.ChannelIndex[FrontCenter]   = 4;
+        mDevice->RealOut.ChannelIndex[LFE]           = 5;
+        mDevice->RealOut.ChannelIndex[SideLeft]      = 6;
+        mDevice->RealOut.ChannelIndex[SideRight]     = 7;
+        mDevice->RealOut.ChannelIndex[TopFrontLeft]  = 8;
+        mDevice->RealOut.ChannelIndex[TopFrontRight] = 9;
+        mDevice->RealOut.ChannelIndex[TopBackLeft]   = 10;
+        mDevice->RealOut.ChannelIndex[TopBackRight]  = 11;
+        break;
+    case DevFmtX3D71:
+        mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
+        mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
+        mDevice->RealOut.ChannelIndex[Aux0]        = 2;
+        mDevice->RealOut.ChannelIndex[Aux1]        = 3;
+        mDevice->RealOut.ChannelIndex[FrontCenter] = 4;
+        mDevice->RealOut.ChannelIndex[LFE]         = 5;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 6;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 7;
+        return;
+
+    /* Same as WFX order */
+    case DevFmtMono:
+    case DevFmtStereo:
+    case DevFmtQuad:
+    case DevFmtX61:
+    case DevFmtAmbi3D:
+        setDefaultWFXChannelOrder();
+        break;
+    }
+}
diff --git a/alc/backends/base.h b/alc/backends/base.h
new file mode 100644 (file)
index 0000000..b6b3d92
--- /dev/null
@@ -0,0 +1,114 @@
+#ifndef ALC_BACKENDS_BASE_H
+#define ALC_BACKENDS_BASE_H
+
+#include <chrono>
+#include <cstdarg>
+#include <memory>
+#include <ratio>
+#include <string>
+
+#include "albyte.h"
+#include "core/device.h"
+#include "core/except.h"
+
+
+using uint = unsigned int;
+
+struct ClockLatency {
+    std::chrono::nanoseconds ClockTime;
+    std::chrono::nanoseconds Latency;
+};
+
+struct BackendBase {
+    virtual void open(const char *name) = 0;
+
+    virtual bool reset();
+    virtual void start() = 0;
+    virtual void stop() = 0;
+
+    virtual void captureSamples(al::byte *buffer, uint samples);
+    virtual uint availableSamples();
+
+    virtual ClockLatency getClockLatency();
+
+    DeviceBase *const mDevice;
+
+    BackendBase(DeviceBase *device) noexcept : mDevice{device} { }
+    virtual ~BackendBase() = default;
+
+protected:
+    /** Sets the default channel order used by most non-WaveFormatEx-based APIs. */
+    void setDefaultChannelOrder();
+    /** Sets the default channel order used by WaveFormatEx. */
+    void setDefaultWFXChannelOrder();
+};
+using BackendPtr = std::unique_ptr<BackendBase>;
+
+enum class BackendType {
+    Playback,
+    Capture
+};
+
+
+/* Helper to get the current clock time from the device's ClockBase, and
+ * SamplesDone converted from the sample rate.
+ */
+inline std::chrono::nanoseconds GetDeviceClockTime(DeviceBase *device)
+{
+    using std::chrono::seconds;
+    using std::chrono::nanoseconds;
+
+    auto ns = nanoseconds{seconds{device->SamplesDone}} / device->Frequency;
+    return device->ClockBase + ns;
+}
+
+/* Helper to get the device latency from the backend, including any fixed
+ * latency from post-processing.
+ */
+inline ClockLatency GetClockLatency(DeviceBase *device, BackendBase *backend)
+{
+    ClockLatency ret{backend->getClockLatency()};
+    ret.Latency += device->FixedLatency;
+    return ret;
+}
+
+
+struct BackendFactory {
+    virtual bool init() = 0;
+
+    virtual bool querySupport(BackendType type) = 0;
+
+    virtual std::string probe(BackendType type) = 0;
+
+    virtual BackendPtr createBackend(DeviceBase *device, BackendType type) = 0;
+
+protected:
+    virtual ~BackendFactory() = default;
+};
+
+namespace al {
+
+enum class backend_error {
+    NoDevice,
+    DeviceError,
+    OutOfMemory
+};
+
+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, ...);
+    ~backend_exception() override;
+
+    backend_error errorCode() const noexcept { return mErrorCode; }
+};
+
+} // namespace al
+
+#endif /* ALC_BACKENDS_BASE_H */
diff --git a/alc/backends/coreaudio.cpp b/alc/backends/coreaudio.cpp
new file mode 100644 (file)
index 0000000..8b0e75f
--- /dev/null
@@ -0,0 +1,932 @@
+/**
+ * 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 "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 "alnumeric.h"
+#include "core/converter.h"
+#include "core/device.h"
+#include "core/logging.h"
+#include "ringbuffer.h"
+
+#include <AudioUnit/AudioUnit.h>
+#include <AudioToolbox/AudioToolbox.h>
+
+
+namespace {
+
+#if TARGET_OS_IOS || TARGET_OS_TV
+#define CAN_ENUMERATE 0
+#else
+#define CAN_ENUMERATE 1
+#endif
+
+constexpr auto OutputElement = 0;
+constexpr auto InputElement = 1;
+
+#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(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~CoreAudioPlayback() override;
+
+    OSStatus MixerProc(AudioUnitRenderActionFlags *ioActionFlags,
+        const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
+        AudioBufferList *ioData) noexcept;
+    static OSStatus MixerProcC(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags,
+        const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
+        AudioBufferList *ioData) noexcept
+    {
+        return static_cast<CoreAudioPlayback*>(inRefCon)->MixerProc(ioActionFlags, inTimeStamp,
+            inBusNumber, inNumberFrames, ioData);
+    }
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    AudioUnit mAudioUnit{};
+
+    uint mFrameSize{0u};
+    AudioStreamBasicDescription mFormat{}; // This is the OpenAL format as a CoreAudio ASBD
+
+    DEF_NEWDEL(CoreAudioPlayback)
+};
+
+CoreAudioPlayback::~CoreAudioPlayback()
+{
+    AudioUnitUninitialize(mAudioUnit);
+    AudioComponentInstanceDispose(mAudioUnit);
+}
+
+
+OSStatus CoreAudioPlayback::MixerProc(AudioUnitRenderActionFlags*, const AudioTimeStamp*, UInt32,
+    UInt32, AudioBufferList *ioData) noexcept
+{
+    for(size_t i{0};i < ioData->mNumberBuffers;++i)
+    {
+        auto &buffer = ioData->mBuffers[i];
+        mDevice->renderSamples(buffer.mData, buffer.mDataByteSize/mFrameSize,
+            buffer.mNumberChannels);
+    }
+    return noErr;
+}
+
+
+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 CAN_ENUMERATE
+    desc.componentSubType = (audioDevice == kAudioDeviceUnknown) ?
+        kAudioUnitSubType_DefaultOutput : kAudioUnitSubType_HALOutput;
+#else
+    desc.componentSubType = kAudioUnitSubType_RemoteIO;
+#endif
+    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
+    desc.componentFlags = 0;
+    desc.componentFlagsMask = 0;
+
+    AudioComponent comp{AudioComponentFindNext(NULL, &desc)};
+    if(comp == nullptr)
+        throw al::backend_exception{al::backend_error::NoDevice, "Could not find audio component"};
+
+    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};
+
+#if CAN_ENUMERATE
+    if(audioDevice != kAudioDeviceUnknown)
+        AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_CurrentDevice,
+            kAudioUnitScope_Global, OutputElement, &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, OutputElement, &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()
+{
+    OSStatus err{AudioUnitUninitialize(mAudioUnit)};
+    if(err != noErr)
+        ERR("-- AudioUnitUninitialize failed.\n");
+
+    /* retrieve default output unit's properties (output side) */
+    AudioStreamBasicDescription streamFormat{};
+    UInt32 size{sizeof(streamFormat)};
+    err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output,
+        OutputElement, &streamFormat, &size);
+    if(err != noErr || size != sizeof(streamFormat))
+    {
+        ERR("AudioUnitGetProperty failed\n");
+        return false;
+    }
+
+#if 0
+    TRACE("Output streamFormat of default output unit -\n");
+    TRACE("  streamFormat.mFramesPerPacket = %d\n", streamFormat.mFramesPerPacket);
+    TRACE("  streamFormat.mChannelsPerFrame = %d\n", streamFormat.mChannelsPerFrame);
+    TRACE("  streamFormat.mBitsPerChannel = %d\n", streamFormat.mBitsPerChannel);
+    TRACE("  streamFormat.mBytesPerPacket = %d\n", streamFormat.mBytesPerPacket);
+    TRACE("  streamFormat.mBytesPerFrame = %d\n", streamFormat.mBytesPerFrame);
+    TRACE("  streamFormat.mSampleRate = %5.0f\n", streamFormat.mSampleRate);
+#endif
+
+    /* 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>(mDevice->BufferSize*streamFormat.mSampleRate/
+            mDevice->Frequency + 0.5);
+        mDevice->Frequency = static_cast<uint>(streamFormat.mSampleRate);
+    }
+
+    /* FIXME: How to tell what channels are what in the output device, and how
+     * to specify what we're giving? e.g. 6.0 vs 5.1
+     */
+    streamFormat.mChannelsPerFrame = mDevice->channelsFromFmt();
+
+    streamFormat.mFramesPerPacket = 1;
+    streamFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kLinearPCMFormatFlagIsPacked;
+    streamFormat.mFormatID = kAudioFormatLinearPCM;
+    switch(mDevice->FmtType)
+    {
+        case DevFmtUByte:
+            mDevice->FmtType = DevFmtByte;
+            /* fall-through */
+        case DevFmtByte:
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
+            streamFormat.mBitsPerChannel = 8;
+            break;
+        case DevFmtUShort:
+            mDevice->FmtType = DevFmtShort;
+            /* fall-through */
+        case DevFmtShort:
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
+            streamFormat.mBitsPerChannel = 16;
+            break;
+        case DevFmtUInt:
+            mDevice->FmtType = DevFmtInt;
+            /* fall-through */
+        case DevFmtInt:
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
+            streamFormat.mBitsPerChannel = 32;
+            break;
+        case DevFmtFloat:
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsFloat;
+            streamFormat.mBitsPerChannel = 32;
+            break;
+    }
+    streamFormat.mBytesPerFrame = streamFormat.mChannelsPerFrame*streamFormat.mBitsPerChannel/8;
+    streamFormat.mBytesPerPacket = streamFormat.mBytesPerFrame*streamFormat.mFramesPerPacket;
+
+    err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
+        OutputElement, &streamFormat, sizeof(streamFormat));
+    if(err != noErr)
+    {
+        ERR("AudioUnitSetProperty failed\n");
+        return false;
+    }
+
+    setDefaultWFXChannelOrder();
+
+    /* setup callback */
+    mFrameSize = mDevice->frameSizeFromFmt();
+    AURenderCallbackStruct input{};
+    input.inputProc = CoreAudioPlayback::MixerProcC;
+    input.inputProcRefCon = this;
+
+    err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_SetRenderCallback,
+        kAudioUnitScope_Input, OutputElement, &input, sizeof(AURenderCallbackStruct));
+    if(err != noErr)
+    {
+        ERR("AudioUnitSetProperty failed\n");
+        return false;
+    }
+
+    /* init the default audio unit... */
+    err = AudioUnitInitialize(mAudioUnit);
+    if(err != noErr)
+    {
+        ERR("AudioUnitInitialize failed\n");
+        return false;
+    }
+
+    return true;
+}
+
+void CoreAudioPlayback::start()
+{
+    const OSStatus err{AudioOutputUnitStart(mAudioUnit)};
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "AudioOutputUnitStart failed: %d", err};
+}
+
+void CoreAudioPlayback::stop()
+{
+    OSStatus err{AudioOutputUnitStop(mAudioUnit)};
+    if(err != noErr)
+        ERR("AudioOutputUnitStop failed\n");
+}
+
+
+struct CoreAudioCapture final : public BackendBase {
+    CoreAudioCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~CoreAudioCapture() override;
+
+    OSStatus RecordProc(AudioUnitRenderActionFlags *ioActionFlags,
+        const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber,
+        UInt32 inNumberFrames, AudioBufferList *ioData) noexcept;
+    static OSStatus RecordProcC(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags,
+        const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
+        AudioBufferList *ioData) noexcept
+    {
+        return static_cast<CoreAudioCapture*>(inRefCon)->RecordProc(ioActionFlags, inTimeStamp,
+            inBusNumber, inNumberFrames, ioData);
+    }
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    AudioUnit mAudioUnit{0};
+
+    uint mFrameSize{0u};
+    AudioStreamBasicDescription mFormat{};  // This is the OpenAL format as a CoreAudio ASBD
+
+    SampleConverterPtr mConverter;
+
+    al::vector<char> mCaptureData;
+
+    RingBufferPtr mRing{nullptr};
+
+    DEF_NEWDEL(CoreAudioCapture)
+};
+
+CoreAudioCapture::~CoreAudioCapture()
+{
+    if(mAudioUnit)
+        AudioComponentInstanceDispose(mAudioUnit);
+    mAudioUnit = 0;
+}
+
+
+OSStatus CoreAudioCapture::RecordProc(AudioUnitRenderActionFlags *ioActionFlags,
+    const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
+    AudioBufferList*) noexcept
+{
+    union {
+        al::byte _[maxz(sizeof(AudioBufferList), offsetof(AudioBufferList, mBuffers[1]))];
+        AudioBufferList list;
+    } audiobuf{};
+
+    audiobuf.list.mNumberBuffers = 1;
+    audiobuf.list.mBuffers[0].mNumberChannels = mFormat.mChannelsPerFrame;
+    audiobuf.list.mBuffers[0].mData = mCaptureData.data();
+    audiobuf.list.mBuffers[0].mDataByteSize = static_cast<UInt32>(mCaptureData.size());
+
+    OSStatus err{AudioUnitRender(mAudioUnit, ioActionFlags, inTimeStamp, inBusNumber,
+        inNumberFrames, &audiobuf.list)};
+    if(err != noErr)
+    {
+        ERR("AudioUnitRender capture error: %d\n", err);
+        return err;
+    }
+
+    mRing->write(mCaptureData.data(), inNumberFrames);
+    return noErr;
+}
+
+
+void CoreAudioCapture::open(const char *name)
+{
+#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 CAN_ENUMERATE
+    desc.componentSubType = (audioDevice == kAudioDeviceUnknown) ?
+        kAudioUnitSubType_DefaultOutput : kAudioUnitSubType_HALOutput;
+#else
+    desc.componentSubType = kAudioUnitSubType_RemoteIO;
+#endif
+    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
+    desc.componentFlags = 0;
+    desc.componentFlagsMask = 0;
+
+    // Search for component with given description
+    AudioComponent comp{AudioComponentFindNext(NULL, &desc)};
+    if(comp == NULL)
+        throw al::backend_exception{al::backend_error::NoDevice, "Could not find audio component"};
+
+    // Open the component
+    OSStatus err{AudioComponentInstanceNew(comp, &mAudioUnit)};
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::NoDevice,
+            "Could not create component instance: %u", err};
+
+    // Turn off AudioUnit output
+    UInt32 enableIO{0};
+    err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_EnableIO,
+        kAudioUnitScope_Output, OutputElement, &enableIO, sizeof(enableIO));
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Could not disable audio unit output property: %u", err};
+
+    // Turn on AudioUnit input
+    enableIO = 1;
+    err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_EnableIO,
+        kAudioUnitScope_Input, InputElement, &enableIO, sizeof(enableIO));
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Could not enable audio unit input property: %u", err};
+
+#if CAN_ENUMERATE
+    if(audioDevice != kAudioDeviceUnknown)
+        AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_CurrentDevice,
+            kAudioUnitScope_Global, InputElement, &audioDevice, sizeof(AudioDeviceID));
+#endif
+
+    // set capture callback
+    AURenderCallbackStruct input{};
+    input.inputProc = CoreAudioCapture::RecordProcC;
+    input.inputProcRefCon = this;
+
+    err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_SetInputCallback,
+        kAudioUnitScope_Global, InputElement, &input, sizeof(AURenderCallbackStruct));
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Could not set capture callback: %u", err};
+
+    // Disable buffer allocation for capture
+    UInt32 flag{0};
+    err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_ShouldAllocateBuffer,
+        kAudioUnitScope_Output, InputElement, &flag, sizeof(flag));
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Could not disable buffer allocation property: %u", err};
+
+    // Initialize the device
+    err = AudioUnitInitialize(mAudioUnit);
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Could not initialize audio unit: %u", err};
+
+    // Get the hardware format
+    AudioStreamBasicDescription hardwareFormat{};
+    UInt32 propertySize{sizeof(hardwareFormat)};
+    err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
+        InputElement, &hardwareFormat, &propertySize);
+    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:
+        requestedFormat.mBitsPerChannel = 8;
+        requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
+        break;
+    case DevFmtUByte:
+        requestedFormat.mBitsPerChannel = 8;
+        requestedFormat.mFormatFlags = kAudioFormatFlagIsPacked;
+        break;
+    case DevFmtShort:
+        requestedFormat.mBitsPerChannel = 16;
+        requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger
+            | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+        break;
+    case DevFmtUShort:
+        requestedFormat.mBitsPerChannel = 16;
+        requestedFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+        break;
+    case DevFmtInt:
+        requestedFormat.mBitsPerChannel = 32;
+        requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger
+            | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+        break;
+    case DevFmtUInt:
+        requestedFormat.mBitsPerChannel = 32;
+        requestedFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+        break;
+    case DevFmtFloat:
+        requestedFormat.mBitsPerChannel = 32;
+        requestedFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagsNativeEndian
+            | kAudioFormatFlagIsPacked;
+        break;
+    }
+
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+        requestedFormat.mChannelsPerFrame = 1;
+        break;
+    case DevFmtStereo:
+        requestedFormat.mChannelsPerFrame = 2;
+        break;
+
+    case DevFmtQuad:
+    case DevFmtX51:
+    case DevFmtX61:
+    case DevFmtX71:
+    case DevFmtX714:
+    case DevFmtX3D71:
+    case DevFmtAmbi3D:
+        throw al::backend_exception{al::backend_error::DeviceError, "%s not supported",
+            DevFmtChannelsString(mDevice->FmtChans)};
+    }
+
+    requestedFormat.mBytesPerFrame = requestedFormat.mChannelsPerFrame * requestedFormat.mBitsPerChannel / 8;
+    requestedFormat.mBytesPerPacket = requestedFormat.mBytesPerFrame;
+    requestedFormat.mSampleRate = mDevice->Frequency;
+    requestedFormat.mFormatID = kAudioFormatLinearPCM;
+    requestedFormat.mReserved = 0;
+    requestedFormat.mFramesPerPacket = 1;
+
+    // save requested format description for later use
+    mFormat = requestedFormat;
+    mFrameSize = mDevice->frameSizeFromFmt();
+
+    // Use intermediate format for sample rate conversion (outputFormat)
+    // Set sample rate to the same as hardware for resampling later
+    AudioStreamBasicDescription outputFormat{requestedFormat};
+    outputFormat.mSampleRate = hardwareFormat.mSampleRate;
+
+    // The output format should be the requested format, but using the hardware sample rate
+    // This is because the AudioUnit will automatically scale other properties, except for sample rate
+    err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output,
+        InputElement, &outputFormat, sizeof(outputFormat));
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Could not set input format: %u", err};
+
+    /* Calculate the minimum AudioUnit output format frame count for the pre-
+     * conversion ring buffer. Ensure at least 100ms for the total buffer.
+     */
+    double srateScale{outputFormat.mSampleRate / mDevice->Frequency};
+    auto FrameCount64 = maxu64(static_cast<uint64_t>(std::ceil(mDevice->BufferSize*srateScale)),
+        static_cast<UInt32>(outputFormat.mSampleRate)/10);
+    FrameCount64 += MaxResamplerPadding;
+    if(FrameCount64 > std::numeric_limits<int32_t>::max())
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Calculated frame count is too large: %" PRIu64, FrameCount64};
+
+    UInt32 outputFrameCount{};
+    propertySize = sizeof(outputFrameCount);
+    err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice,
+        kAudioUnitScope_Global, OutputElement, &outputFrameCount, &propertySize);
+    if(err != noErr || propertySize != sizeof(outputFrameCount))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Could not get input frame count: %u", err};
+
+    mCaptureData.resize(outputFrameCount * mFrameSize);
+
+    outputFrameCount = static_cast<UInt32>(maxu64(outputFrameCount, FrameCount64));
+    mRing = RingBuffer::Create(outputFrameCount, mFrameSize, false);
+
+    /* Set up sample converter if needed */
+    if(outputFormat.mSampleRate != mDevice->Frequency)
+        mConverter = SampleConverter::Create(mDevice->FmtType, mDevice->FmtType,
+            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, InputElement, &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
+}
+
+
+void CoreAudioCapture::start()
+{
+    OSStatus err{AudioOutputUnitStart(mAudioUnit)};
+    if(err != noErr)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "AudioOutputUnitStart failed: %d", err};
+}
+
+void CoreAudioCapture::stop()
+{
+    OSStatus err{AudioOutputUnitStop(mAudioUnit)};
+    if(err != noErr)
+        ERR("AudioOutputUnitStop failed\n");
+}
+
+void CoreAudioCapture::captureSamples(al::byte *buffer, uint samples)
+{
+    if(!mConverter)
+    {
+        mRing->read(buffer, samples);
+        return;
+    }
+
+    auto rec_vec = mRing->getReadVector();
+    const void *src0{rec_vec.first.buf};
+    auto src0len = static_cast<uint>(rec_vec.first.len);
+    uint got{mConverter->convert(&src0, &src0len, buffer, samples)};
+    size_t total_read{rec_vec.first.len - src0len};
+    if(got < samples && !src0len && rec_vec.second.len > 0)
+    {
+        const void *src1{rec_vec.second.buf};
+        auto src1len = static_cast<uint>(rec_vec.second.len);
+        got += mConverter->convert(&src1, &src1len, buffer + got*mFrameSize, samples-got);
+        total_read += rec_vec.second.len - src1len;
+    }
+
+    mRing->readAdvance(total_read);
+}
+
+uint CoreAudioCapture::availableSamples()
+{
+    if(!mConverter) return static_cast<uint>(mRing->readSpace());
+    return mConverter->availableOut(static_cast<uint>(mRing->readSpace()));
+}
+
+} // namespace
+
+BackendFactory &CoreAudioBackendFactory::getFactory()
+{
+    static CoreAudioBackendFactory factory{};
+    return factory;
+}
+
+bool CoreAudioBackendFactory::init() { return true; }
+
+bool CoreAudioBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+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:
+    case BackendType::Capture:
+        /* Includes null char. */
+        outnames.append(ca_device, sizeof(ca_device));
+        break;
+    }
+#endif
+    return outnames;
+}
+
+BackendPtr CoreAudioBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new CoreAudioPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new CoreAudioCapture{device}};
+    return nullptr;
+}
diff --git a/alc/backends/coreaudio.h b/alc/backends/coreaudio.h
new file mode 100644 (file)
index 0000000..1252edd
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_COREAUDIO_H
+#define BACKENDS_COREAUDIO_H
+
+#include "base.h"
+
+struct CoreAudioBackendFactory 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_COREAUDIO_H */
diff --git a/alc/backends/dsound.cpp b/alc/backends/dsound.cpp
new file mode 100644 (file)
index 0000000..f549c0f
--- /dev/null
@@ -0,0 +1,850 @@
+/**
+ * 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 "dsound.h"
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <memory.h>
+
+#include <cguid.h>
+#include <mmreg.h>
+#ifndef _WAVEFORMATEXTENSIBLE_
+#include <ks.h>
+#include <ksmedia.h>
+#endif
+
+#include <atomic>
+#include <cassert>
+#include <thread>
+#include <string>
+#include <vector>
+#include <algorithm>
+#include <functional>
+
+#include "alnumeric.h"
+#include "comptr.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "dynload.h"
+#include "ringbuffer.h"
+#include "strutils.h"
+#include "threads.h"
+
+/* MinGW-w64 needs this for some unknown reason now. */
+using LPCWAVEFORMATEX = const WAVEFORMATEX*;
+#include <dsound.h>
+
+
+#ifndef DSSPEAKER_5POINT1
+#   define DSSPEAKER_5POINT1          0x00000006
+#endif
+#ifndef DSSPEAKER_5POINT1_BACK
+#   define DSSPEAKER_5POINT1_BACK     0x00000006
+#endif
+#ifndef DSSPEAKER_7POINT1
+#   define DSSPEAKER_7POINT1          0x00000007
+#endif
+#ifndef DSSPEAKER_7POINT1_SURROUND
+#   define DSSPEAKER_7POINT1_SURROUND 0x00000008
+#endif
+#ifndef DSSPEAKER_5POINT1_SURROUND
+#   define DSSPEAKER_5POINT1_SURROUND 0x00000009
+#endif
+
+
+/* Some headers seem to define these as macros for __uuidof, which is annoying
+ * since some headers don't declare them at all. Hopefully the ifdef is enough
+ * to tell if they need to be declared.
+ */
+#ifndef KSDATAFORMAT_SUBTYPE_PCM
+DEFINE_GUID(KSDATAFORMAT_SUBTYPE_PCM, 0x00000001, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+#endif
+#ifndef KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
+DEFINE_GUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x00000003, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+#endif
+
+namespace {
+
+#define DEVNAME_HEAD "OpenAL Soft on "
+
+
+#ifdef HAVE_DYNLOAD
+void *ds_handle;
+HRESULT (WINAPI *pDirectSoundCreate)(const GUID *pcGuidDevice, IDirectSound **ppDS, IUnknown *pUnkOuter);
+HRESULT (WINAPI *pDirectSoundEnumerateW)(LPDSENUMCALLBACKW pDSEnumCallback, void *pContext);
+HRESULT (WINAPI *pDirectSoundCaptureCreate)(const GUID *pcGuidDevice, IDirectSoundCapture **ppDSC, IUnknown *pUnkOuter);
+HRESULT (WINAPI *pDirectSoundCaptureEnumerateW)(LPDSENUMCALLBACKW pDSEnumCallback, void *pContext);
+
+#ifndef IN_IDE_PARSER
+#define DirectSoundCreate            pDirectSoundCreate
+#define DirectSoundEnumerateW        pDirectSoundEnumerateW
+#define DirectSoundCaptureCreate     pDirectSoundCaptureCreate
+#define DirectSoundCaptureEnumerateW pDirectSoundCaptureEnumerateW
+#endif
+#endif
+
+
+#define MONO SPEAKER_FRONT_CENTER
+#define STEREO (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT)
+#define QUAD (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT)
+#define X5DOT1 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT)
+#define X5DOT1REAR (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT)
+#define X6DOT1 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_CENTER|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT)
+#define X7DOT1 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT)
+#define X7DOT1DOT4 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT|SPEAKER_TOP_FRONT_LEFT|SPEAKER_TOP_FRONT_RIGHT|SPEAKER_TOP_BACK_LEFT|SPEAKER_TOP_BACK_RIGHT)
+
+#define MAX_UPDATES 128
+
+struct DevMap {
+    std::string name;
+    GUID guid;
+
+    template<typename T0, typename T1>
+    DevMap(T0&& name_, T1&& guid_)
+      : name{std::forward<T0>(name_)}, guid{std::forward<T1>(guid_)}
+    { }
+};
+
+al::vector<DevMap> PlaybackDevices;
+al::vector<DevMap> CaptureDevices;
+
+bool checkName(const al::vector<DevMap> &list, const std::string &name)
+{
+    auto match_name = [&name](const DevMap &entry) -> bool
+    { return entry.name == name; };
+    return std::find_if(list.cbegin(), list.cend(), match_name) != list.cend();
+}
+
+BOOL CALLBACK DSoundEnumDevices(GUID *guid, const WCHAR *desc, const WCHAR*, void *data) noexcept
+{
+    if(!guid)
+        return TRUE;
+
+    auto& devices = *static_cast<al::vector<DevMap>*>(data);
+    const std::string basename{DEVNAME_HEAD + wstr_to_utf8(desc)};
+
+    int count{1};
+    std::string newname{basename};
+    while(checkName(devices, newname))
+    {
+        newname = basename;
+        newname += " #";
+        newname += std::to_string(++count);
+    }
+    devices.emplace_back(std::move(newname), *guid);
+    const DevMap &newentry = devices.back();
+
+    OLECHAR *guidstr{nullptr};
+    HRESULT hr{StringFromCLSID(*guid, &guidstr)};
+    if(SUCCEEDED(hr))
+    {
+        TRACE("Got device \"%s\", GUID \"%ls\"\n", newentry.name.c_str(), guidstr);
+        CoTaskMemFree(guidstr);
+    }
+
+    return TRUE;
+}
+
+
+struct DSoundPlayback final : public BackendBase {
+    DSoundPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~DSoundPlayback() override;
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    ComPtr<IDirectSound>       mDS;
+    ComPtr<IDirectSoundBuffer> mPrimaryBuffer;
+    ComPtr<IDirectSoundBuffer> mBuffer;
+    ComPtr<IDirectSoundNotify> mNotifies;
+    HANDLE mNotifyEvent{nullptr};
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(DSoundPlayback)
+};
+
+DSoundPlayback::~DSoundPlayback()
+{
+    mNotifies = nullptr;
+    mBuffer = nullptr;
+    mPrimaryBuffer = nullptr;
+    mDS = nullptr;
+
+    if(mNotifyEvent)
+        CloseHandle(mNotifyEvent);
+    mNotifyEvent = nullptr;
+}
+
+
+FORCE_ALIGN int DSoundPlayback::mixerProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    DSBCAPS DSBCaps{};
+    DSBCaps.dwSize = sizeof(DSBCaps);
+    HRESULT err{mBuffer->GetCaps(&DSBCaps)};
+    if(FAILED(err))
+    {
+        ERR("Failed to get buffer caps: 0x%lx\n", err);
+        mDevice->handleDisconnect("Failure retrieving playback buffer info: 0x%lx", err);
+        return 1;
+    }
+
+    const size_t FrameStep{mDevice->channelsFromFmt()};
+    uint FrameSize{mDevice->frameSizeFromFmt()};
+    DWORD FragSize{mDevice->UpdateSize * FrameSize};
+
+    bool Playing{false};
+    DWORD LastCursor{0u};
+    mBuffer->GetCurrentPosition(&LastCursor, nullptr);
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        // Get current play cursor
+        DWORD PlayCursor;
+        mBuffer->GetCurrentPosition(&PlayCursor, nullptr);
+        DWORD avail = (PlayCursor-LastCursor+DSBCaps.dwBufferBytes) % DSBCaps.dwBufferBytes;
+
+        if(avail < FragSize)
+        {
+            if(!Playing)
+            {
+                err = mBuffer->Play(0, 0, DSBPLAY_LOOPING);
+                if(FAILED(err))
+                {
+                    ERR("Failed to play buffer: 0x%lx\n", err);
+                    mDevice->handleDisconnect("Failure starting playback: 0x%lx", err);
+                    return 1;
+                }
+                Playing = true;
+            }
+
+            avail = WaitForSingleObjectEx(mNotifyEvent, 2000, FALSE);
+            if(avail != WAIT_OBJECT_0)
+                ERR("WaitForSingleObjectEx error: 0x%lx\n", avail);
+            continue;
+        }
+        avail -= avail%FragSize;
+
+        // Lock output buffer
+        void *WritePtr1, *WritePtr2;
+        DWORD WriteCnt1{0u},  WriteCnt2{0u};
+        err = mBuffer->Lock(LastCursor, avail, &WritePtr1, &WriteCnt1, &WritePtr2, &WriteCnt2, 0);
+
+        // If the buffer is lost, restore it and lock
+        if(err == DSERR_BUFFERLOST)
+        {
+            WARN("Buffer lost, restoring...\n");
+            err = mBuffer->Restore();
+            if(SUCCEEDED(err))
+            {
+                Playing = false;
+                LastCursor = 0;
+                err = mBuffer->Lock(0, DSBCaps.dwBufferBytes, &WritePtr1, &WriteCnt1,
+                                    &WritePtr2, &WriteCnt2, 0);
+            }
+        }
+
+        if(SUCCEEDED(err))
+        {
+            mDevice->renderSamples(WritePtr1, WriteCnt1/FrameSize, FrameStep);
+            if(WriteCnt2 > 0)
+                mDevice->renderSamples(WritePtr2, WriteCnt2/FrameSize, FrameStep);
+
+            mBuffer->Unlock(WritePtr1, WriteCnt1, WritePtr2, WriteCnt2);
+        }
+        else
+        {
+            ERR("Buffer lock error: %#lx\n", err);
+            mDevice->handleDisconnect("Failed to lock output buffer: 0x%lx", err);
+            return 1;
+        }
+
+        // Update old write cursor location
+        LastCursor += WriteCnt1+WriteCnt2;
+        LastCursor %= DSBCaps.dwBufferBytes;
+    }
+
+    return 0;
+}
+
+void DSoundPlayback::open(const char *name)
+{
+    HRESULT hr;
+    if(PlaybackDevices.empty())
+    {
+        /* Initialize COM to prevent name truncation */
+        HRESULT hrcom{CoInitialize(nullptr)};
+        hr = DirectSoundEnumerateW(DSoundEnumDevices, &PlaybackDevices);
+        if(FAILED(hr))
+            ERR("Error enumerating DirectSound devices (0x%lx)!\n", hr);
+        if(SUCCEEDED(hrcom))
+            CoUninitialize();
+    }
+
+    const GUID *guid{nullptr};
+    if(!name && !PlaybackDevices.empty())
+    {
+        name = PlaybackDevices[0].name.c_str();
+        guid = &PlaybackDevices[0].guid;
+    }
+    else
+    {
+        auto iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+            [name](const DevMap &entry) -> bool { return entry.name == name; });
+        if(iter == PlaybackDevices.cend())
+        {
+            GUID id{};
+            hr = CLSIDFromString(utf8_to_wstr(name).c_str(), &id);
+            if(SUCCEEDED(hr))
+                iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+                    [&id](const DevMap &entry) -> bool { return entry.guid == id; });
+            if(iter == PlaybackDevices.cend())
+                throw al::backend_exception{al::backend_error::NoDevice,
+                    "Device name \"%s\" not found", name};
+        }
+        guid = &iter->guid;
+    }
+
+    hr = DS_OK;
+    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, ds.getPtr(), nullptr);
+    if(SUCCEEDED(hr))
+        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()
+{
+    mNotifies = nullptr;
+    mBuffer = nullptr;
+    mPrimaryBuffer = nullptr;
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        mDevice->FmtType = DevFmtUByte;
+        break;
+    case DevFmtFloat:
+        if(mDevice->Flags.test(SampleTypeRequest))
+            break;
+        /* fall-through */
+    case DevFmtUShort:
+        mDevice->FmtType = DevFmtShort;
+        break;
+    case DevFmtUInt:
+        mDevice->FmtType = DevFmtInt;
+        break;
+    case DevFmtUByte:
+    case DevFmtShort:
+    case DevFmtInt:
+        break;
+    }
+
+    WAVEFORMATEXTENSIBLE OutputType{};
+    DWORD speakers{};
+    HRESULT hr{mDS->GetSpeakerConfig(&speakers)};
+    if(FAILED(hr))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to get speaker config: 0x%08lx", hr};
+
+    speakers = DSSPEAKER_CONFIG(speakers);
+    if(!mDevice->Flags.test(ChannelsRequest))
+    {
+        if(speakers == DSSPEAKER_MONO)
+            mDevice->FmtChans = DevFmtMono;
+        else if(speakers == DSSPEAKER_STEREO || speakers == DSSPEAKER_HEADPHONE)
+            mDevice->FmtChans = DevFmtStereo;
+        else if(speakers == DSSPEAKER_QUAD)
+            mDevice->FmtChans = DevFmtQuad;
+        else if(speakers == DSSPEAKER_5POINT1_SURROUND || speakers == DSSPEAKER_5POINT1_BACK)
+            mDevice->FmtChans = DevFmtX51;
+        else if(speakers == DSSPEAKER_7POINT1 || speakers == DSSPEAKER_7POINT1_SURROUND)
+            mDevice->FmtChans = DevFmtX71;
+        else
+            ERR("Unknown system speaker config: 0x%lx\n", speakers);
+    }
+    mDevice->Flags.set(DirectEar, (speakers == DSSPEAKER_HEADPHONE));
+    const bool isRear51{speakers == DSSPEAKER_5POINT1_BACK};
+
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono: OutputType.dwChannelMask = MONO; break;
+    case DevFmtAmbi3D: mDevice->FmtChans = DevFmtStereo;
+        /* fall-through */
+    case DevFmtStereo: OutputType.dwChannelMask = STEREO; break;
+    case DevFmtQuad: OutputType.dwChannelMask = QUAD; break;
+    case DevFmtX51: OutputType.dwChannelMask = isRear51 ? X5DOT1REAR : X5DOT1; break;
+    case DevFmtX61: OutputType.dwChannelMask = X6DOT1; break;
+    case DevFmtX71: OutputType.dwChannelMask = X7DOT1; break;
+    case DevFmtX714: OutputType.dwChannelMask = X7DOT1DOT4; break;
+    case DevFmtX3D71: OutputType.dwChannelMask = X7DOT1; break;
+    }
+
+retry_open:
+    hr = S_OK;
+    OutputType.Format.wFormatTag = WAVE_FORMAT_PCM;
+    OutputType.Format.nChannels = static_cast<WORD>(mDevice->channelsFromFmt());
+    OutputType.Format.wBitsPerSample = static_cast<WORD>(mDevice->bytesFromFmt() * 8);
+    OutputType.Format.nBlockAlign = static_cast<WORD>(OutputType.Format.nChannels *
+        OutputType.Format.wBitsPerSample / 8);
+    OutputType.Format.nSamplesPerSec = mDevice->Frequency;
+    OutputType.Format.nAvgBytesPerSec = OutputType.Format.nSamplesPerSec *
+        OutputType.Format.nBlockAlign;
+    OutputType.Format.cbSize = 0;
+
+    if(OutputType.Format.nChannels > 2 || mDevice->FmtType == DevFmtFloat)
+    {
+        OutputType.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+        OutputType.Samples.wValidBitsPerSample = OutputType.Format.wBitsPerSample;
+        OutputType.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+        if(mDevice->FmtType == DevFmtFloat)
+            OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+        else
+            OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+
+        mPrimaryBuffer = nullptr;
+    }
+    else
+    {
+        if(SUCCEEDED(hr) && !mPrimaryBuffer)
+        {
+            DSBUFFERDESC DSBDescription{};
+            DSBDescription.dwSize = sizeof(DSBDescription);
+            DSBDescription.dwFlags = DSBCAPS_PRIMARYBUFFER;
+            hr = mDS->CreateSoundBuffer(&DSBDescription, mPrimaryBuffer.getPtr(), nullptr);
+        }
+        if(SUCCEEDED(hr))
+            hr = mPrimaryBuffer->SetFormat(&OutputType.Format);
+    }
+
+    if(SUCCEEDED(hr))
+    {
+        uint num_updates{mDevice->BufferSize / mDevice->UpdateSize};
+        if(num_updates > MAX_UPDATES)
+            num_updates = MAX_UPDATES;
+        mDevice->BufferSize = mDevice->UpdateSize * num_updates;
+
+        DSBUFFERDESC DSBDescription{};
+        DSBDescription.dwSize = sizeof(DSBDescription);
+        DSBDescription.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GETCURRENTPOSITION2
+            | DSBCAPS_GLOBALFOCUS;
+        DSBDescription.dwBufferBytes = mDevice->BufferSize * OutputType.Format.nBlockAlign;
+        DSBDescription.lpwfxFormat = &OutputType.Format;
+
+        hr = mDS->CreateSoundBuffer(&DSBDescription, mBuffer.getPtr(), nullptr);
+        if(FAILED(hr) && mDevice->FmtType == DevFmtFloat)
+        {
+            mDevice->FmtType = DevFmtShort;
+            goto retry_open;
+        }
+    }
+
+    if(SUCCEEDED(hr))
+    {
+        void *ptr;
+        hr = mBuffer->QueryInterface(IID_IDirectSoundNotify, &ptr);
+        if(SUCCEEDED(hr))
+        {
+            mNotifies = ComPtr<IDirectSoundNotify>{static_cast<IDirectSoundNotify*>(ptr)};
+
+            uint num_updates{mDevice->BufferSize / mDevice->UpdateSize};
+            assert(num_updates <= MAX_UPDATES);
+
+            std::array<DSBPOSITIONNOTIFY,MAX_UPDATES> nots;
+            for(uint i{0};i < num_updates;++i)
+            {
+                nots[i].dwOffset = i * mDevice->UpdateSize * OutputType.Format.nBlockAlign;
+                nots[i].hEventNotify = mNotifyEvent;
+            }
+            if(mNotifies->SetNotificationPositions(num_updates, nots.data()) != DS_OK)
+                hr = E_FAIL;
+        }
+    }
+
+    if(FAILED(hr))
+    {
+        mNotifies = nullptr;
+        mBuffer = nullptr;
+        mPrimaryBuffer = nullptr;
+        return false;
+    }
+
+    ResetEvent(mNotifyEvent);
+    setDefaultWFXChannelOrder();
+
+    return true;
+}
+
+void DSoundPlayback::start()
+{
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&DSoundPlayback::mixerProc), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void DSoundPlayback::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    mBuffer->Stop();
+}
+
+
+struct DSoundCapture final : public BackendBase {
+    DSoundCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~DSoundCapture() override;
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    ComPtr<IDirectSoundCapture> mDSC;
+    ComPtr<IDirectSoundCaptureBuffer> mDSCbuffer;
+    DWORD mBufferBytes{0u};
+    DWORD mCursor{0u};
+
+    RingBufferPtr mRing;
+
+    DEF_NEWDEL(DSoundCapture)
+};
+
+DSoundCapture::~DSoundCapture()
+{
+    if(mDSCbuffer)
+    {
+        mDSCbuffer->Stop();
+        mDSCbuffer = nullptr;
+    }
+    mDSC = nullptr;
+}
+
+
+void DSoundCapture::open(const char *name)
+{
+    HRESULT hr;
+    if(CaptureDevices.empty())
+    {
+        /* Initialize COM to prevent name truncation */
+        HRESULT hrcom{CoInitialize(nullptr)};
+        hr = DirectSoundCaptureEnumerateW(DSoundEnumDevices, &CaptureDevices);
+        if(FAILED(hr))
+            ERR("Error enumerating DirectSound devices (0x%lx)!\n", hr);
+        if(SUCCEEDED(hrcom))
+            CoUninitialize();
+    }
+
+    const GUID *guid{nullptr};
+    if(!name && !CaptureDevices.empty())
+    {
+        name = CaptureDevices[0].name.c_str();
+        guid = &CaptureDevices[0].guid;
+    }
+    else
+    {
+        auto iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+            [name](const DevMap &entry) -> bool { return entry.name == name; });
+        if(iter == CaptureDevices.cend())
+        {
+            GUID id{};
+            hr = CLSIDFromString(utf8_to_wstr(name).c_str(), &id);
+            if(SUCCEEDED(hr))
+                iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+                    [&id](const DevMap &entry) -> bool { return entry.guid == id; });
+            if(iter == CaptureDevices.cend())
+                throw al::backend_exception{al::backend_error::NoDevice,
+                    "Device name \"%s\" not found", name};
+        }
+        guid = &iter->guid;
+    }
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+    case DevFmtUShort:
+    case DevFmtUInt:
+        WARN("%s capture samples not supported\n", DevFmtTypeString(mDevice->FmtType));
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "%s capture samples not supported", DevFmtTypeString(mDevice->FmtType)};
+
+    case DevFmtUByte:
+    case DevFmtShort:
+    case DevFmtInt:
+    case DevFmtFloat:
+        break;
+    }
+
+    WAVEFORMATEXTENSIBLE InputType{};
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono: InputType.dwChannelMask = MONO; break;
+    case DevFmtStereo: InputType.dwChannelMask = STEREO; break;
+    case DevFmtQuad: InputType.dwChannelMask = QUAD; break;
+    case DevFmtX51: InputType.dwChannelMask = X5DOT1; break;
+    case DevFmtX61: InputType.dwChannelMask = X6DOT1; break;
+    case DevFmtX71: InputType.dwChannelMask = X7DOT1; break;
+    case DevFmtX714: InputType.dwChannelMask = X7DOT1DOT4; break;
+    case DevFmtX3D71:
+    case DevFmtAmbi3D:
+        WARN("%s capture not supported\n", DevFmtChannelsString(mDevice->FmtChans));
+        throw al::backend_exception{al::backend_error::DeviceError, "%s capture not supported",
+            DevFmtChannelsString(mDevice->FmtChans)};
+    }
+
+    InputType.Format.wFormatTag = WAVE_FORMAT_PCM;
+    InputType.Format.nChannels = static_cast<WORD>(mDevice->channelsFromFmt());
+    InputType.Format.wBitsPerSample = static_cast<WORD>(mDevice->bytesFromFmt() * 8);
+    InputType.Format.nBlockAlign = static_cast<WORD>(InputType.Format.nChannels *
+        InputType.Format.wBitsPerSample / 8);
+    InputType.Format.nSamplesPerSec = mDevice->Frequency;
+    InputType.Format.nAvgBytesPerSec = InputType.Format.nSamplesPerSec *
+        InputType.Format.nBlockAlign;
+    InputType.Format.cbSize = 0;
+    InputType.Samples.wValidBitsPerSample = InputType.Format.wBitsPerSample;
+    if(mDevice->FmtType == DevFmtFloat)
+        InputType.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+    else
+        InputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+
+    if(InputType.Format.nChannels > 2 || mDevice->FmtType == DevFmtFloat)
+    {
+        InputType.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+        InputType.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+    }
+
+    uint samples{mDevice->BufferSize};
+    samples = maxu(samples, 100 * mDevice->Frequency / 1000);
+
+    DSCBUFFERDESC DSCBDescription{};
+    DSCBDescription.dwSize = sizeof(DSCBDescription);
+    DSCBDescription.dwFlags = 0;
+    DSCBDescription.dwBufferBytes = samples * InputType.Format.nBlockAlign;
+    DSCBDescription.lpwfxFormat = &InputType.Format;
+
+    //DirectSoundCapture Init code
+    hr = DirectSoundCaptureCreate(guid, mDSC.getPtr(), nullptr);
+    if(SUCCEEDED(hr))
+        mDSC->CreateCaptureBuffer(&DSCBDescription, mDSCbuffer.getPtr(), nullptr);
+    if(SUCCEEDED(hr))
+         mRing = RingBuffer::Create(mDevice->BufferSize, InputType.Format.nBlockAlign, false);
+
+    if(FAILED(hr))
+    {
+        mRing = nullptr;
+        mDSCbuffer = nullptr;
+        mDSC = nullptr;
+
+        throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx",
+            hr};
+    }
+
+    mBufferBytes = DSCBDescription.dwBufferBytes;
+    setDefaultWFXChannelOrder();
+
+    mDevice->DeviceName = name;
+}
+
+void DSoundCapture::start()
+{
+    const HRESULT hr{mDSCbuffer->Start(DSCBSTART_LOOPING)};
+    if(FAILED(hr))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failure starting capture: 0x%lx", hr};
+}
+
+void DSoundCapture::stop()
+{
+    HRESULT hr{mDSCbuffer->Stop()};
+    if(FAILED(hr))
+    {
+        ERR("stop failed: 0x%08lx\n", hr);
+        mDevice->handleDisconnect("Failure stopping capture: 0x%lx", hr);
+    }
+}
+
+void DSoundCapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, samples); }
+
+uint DSoundCapture::availableSamples()
+{
+    if(!mDevice->Connected.load(std::memory_order_acquire))
+        return static_cast<uint>(mRing->readSpace());
+
+    const uint FrameSize{mDevice->frameSizeFromFmt()};
+    const DWORD BufferBytes{mBufferBytes};
+    const DWORD LastCursor{mCursor};
+
+    DWORD ReadCursor{};
+    void *ReadPtr1{}, *ReadPtr2{};
+    DWORD ReadCnt1{},  ReadCnt2{};
+    HRESULT hr{mDSCbuffer->GetCurrentPosition(nullptr, &ReadCursor)};
+    if(SUCCEEDED(hr))
+    {
+        const DWORD NumBytes{(BufferBytes+ReadCursor-LastCursor) % BufferBytes};
+        if(!NumBytes) return static_cast<uint>(mRing->readSpace());
+        hr = mDSCbuffer->Lock(LastCursor, NumBytes, &ReadPtr1, &ReadCnt1, &ReadPtr2, &ReadCnt2, 0);
+    }
+    if(SUCCEEDED(hr))
+    {
+        mRing->write(ReadPtr1, ReadCnt1/FrameSize);
+        if(ReadPtr2 != nullptr && ReadCnt2 > 0)
+            mRing->write(ReadPtr2, ReadCnt2/FrameSize);
+        hr = mDSCbuffer->Unlock(ReadPtr1, ReadCnt1, ReadPtr2, ReadCnt2);
+        mCursor = ReadCursor;
+    }
+
+    if(FAILED(hr))
+    {
+        ERR("update failed: 0x%08lx\n", hr);
+        mDevice->handleDisconnect("Failure retrieving capture data: 0x%lx", hr);
+    }
+
+    return static_cast<uint>(mRing->readSpace());
+}
+
+} // namespace
+
+
+BackendFactory &DSoundBackendFactory::getFactory()
+{
+    static DSoundBackendFactory factory{};
+    return factory;
+}
+
+bool DSoundBackendFactory::init()
+{
+#ifdef HAVE_DYNLOAD
+    if(!ds_handle)
+    {
+        ds_handle = LoadLib("dsound.dll");
+        if(!ds_handle)
+        {
+            ERR("Failed to load dsound.dll\n");
+            return false;
+        }
+
+#define LOAD_FUNC(f) do {                                                     \
+    p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(ds_handle, #f));        \
+    if(!p##f)                                                                 \
+    {                                                                         \
+        CloseLib(ds_handle);                                                  \
+        ds_handle = nullptr;                                                  \
+        return false;                                                         \
+    }                                                                         \
+} while(0)
+        LOAD_FUNC(DirectSoundCreate);
+        LOAD_FUNC(DirectSoundEnumerateW);
+        LOAD_FUNC(DirectSoundCaptureCreate);
+        LOAD_FUNC(DirectSoundCaptureEnumerateW);
+#undef LOAD_FUNC
+    }
+#endif
+    return true;
+}
+
+bool DSoundBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+std::string DSoundBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    auto add_device = [&outnames](const DevMap &entry) -> void
+    {
+        /* +1 to also append the null char (to ensure a null-separated list and
+         * double-null terminated list).
+         */
+        outnames.append(entry.name.c_str(), entry.name.length()+1);
+    };
+
+    /* Initialize COM to prevent name truncation */
+    HRESULT hr;
+    HRESULT hrcom{CoInitialize(nullptr)};
+    switch(type)
+    {
+    case BackendType::Playback:
+        PlaybackDevices.clear();
+        hr = DirectSoundEnumerateW(DSoundEnumDevices, &PlaybackDevices);
+        if(FAILED(hr))
+            ERR("Error enumerating DirectSound playback devices (0x%lx)!\n", hr);
+        std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+        break;
+
+    case BackendType::Capture:
+        CaptureDevices.clear();
+        hr = DirectSoundCaptureEnumerateW(DSoundEnumDevices, &CaptureDevices);
+        if(FAILED(hr))
+            ERR("Error enumerating DirectSound capture devices (0x%lx)!\n", hr);
+        std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+        break;
+    }
+    if(SUCCEEDED(hrcom))
+        CoUninitialize();
+
+    return outnames;
+}
+
+BackendPtr DSoundBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new DSoundPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new DSoundCapture{device}};
+    return nullptr;
+}
diff --git a/alc/backends/dsound.h b/alc/backends/dsound.h
new file mode 100644 (file)
index 0000000..787f227
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_DSOUND_H
+#define BACKENDS_DSOUND_H
+
+#include "base.h"
+
+struct DSoundBackendFactory 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_DSOUND_H */
diff --git a/alc/backends/jack.cpp b/alc/backends/jack.cpp
new file mode 100644 (file)
index 0000000..791002c
--- /dev/null
@@ -0,0 +1,744 @@
+/**
+ * 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 "jack.h"
+
+#include <cstdlib>
+#include <cstdio>
+#include <cstring>
+#include <memory.h>
+
+#include <array>
+#include <thread>
+#include <functional>
+
+#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"
+#include "threads.h"
+
+#include <jack/jack.h>
+#include <jack/ringbuffer.h>
+
+
+namespace {
+
+#ifdef HAVE_DYNLOAD
+#define JACK_FUNCS(MAGIC)          \
+    MAGIC(jack_client_open);       \
+    MAGIC(jack_client_close);      \
+    MAGIC(jack_client_name_size);  \
+    MAGIC(jack_get_client_name);   \
+    MAGIC(jack_connect);           \
+    MAGIC(jack_activate);          \
+    MAGIC(jack_deactivate);        \
+    MAGIC(jack_port_register);     \
+    MAGIC(jack_port_unregister);   \
+    MAGIC(jack_port_get_buffer);   \
+    MAGIC(jack_port_name);         \
+    MAGIC(jack_get_ports);         \
+    MAGIC(jack_free);              \
+    MAGIC(jack_get_sample_rate);   \
+    MAGIC(jack_set_error_function); \
+    MAGIC(jack_set_process_callback); \
+    MAGIC(jack_set_buffer_size_callback); \
+    MAGIC(jack_set_buffer_size);   \
+    MAGIC(jack_get_buffer_size);
+
+void *jack_handle;
+#define MAKE_FUNC(f) decltype(f) * p##f
+JACK_FUNCS(MAKE_FUNC)
+decltype(jack_error_callback) * pjack_error_callback;
+#undef MAKE_FUNC
+
+#ifndef IN_IDE_PARSER
+#define jack_client_open pjack_client_open
+#define jack_client_close pjack_client_close
+#define jack_client_name_size pjack_client_name_size
+#define jack_get_client_name pjack_get_client_name
+#define jack_connect pjack_connect
+#define jack_activate pjack_activate
+#define jack_deactivate pjack_deactivate
+#define jack_port_register pjack_port_register
+#define jack_port_unregister pjack_port_unregister
+#define jack_port_get_buffer pjack_port_get_buffer
+#define jack_port_name pjack_port_name
+#define jack_get_ports pjack_get_ports
+#define jack_free pjack_free
+#define jack_get_sample_rate pjack_get_sample_rate
+#define jack_set_error_function pjack_set_error_function
+#define jack_set_process_callback pjack_set_process_callback
+#define jack_set_buffer_size_callback pjack_set_buffer_size_callback
+#define jack_set_buffer_size pjack_set_buffer_size
+#define jack_get_buffer_size pjack_get_buffer_size
+#define jack_error_callback (*pjack_error_callback)
+#endif
+#endif
+
+
+constexpr char JackDefaultAudioType[] = JACK_DEFAULT_AUDIO_TYPE;
+
+jack_options_t ClientOptions = JackNullOption;
+
+bool jack_load()
+{
+    bool error{false};
+
+#ifdef HAVE_DYNLOAD
+    if(!jack_handle)
+    {
+        std::string missing_funcs;
+
+#ifdef _WIN32
+#define JACKLIB "libjack.dll"
+#else
+#define JACKLIB "libjack.so.0"
+#endif
+        jack_handle = LoadLib(JACKLIB);
+        if(!jack_handle)
+        {
+            WARN("Failed to load %s\n", JACKLIB);
+            return false;
+        }
+
+        error = false;
+#define LOAD_FUNC(f) do {                                                     \
+    p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(jack_handle, #f));      \
+    if(p##f == nullptr) {                                                     \
+        error = true;                                                         \
+        missing_funcs += "\n" #f;                                             \
+    }                                                                         \
+} while(0)
+        JACK_FUNCS(LOAD_FUNC);
+#undef LOAD_FUNC
+        /* Optional symbols. These don't exist in all versions of JACK. */
+#define LOAD_SYM(f) p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(jack_handle, #f))
+        LOAD_SYM(jack_error_callback);
+#undef LOAD_SYM
+
+        if(error)
+        {
+            WARN("Missing expected functions:%s\n", missing_funcs.c_str());
+            CloseLib(jack_handle);
+            jack_handle = nullptr;
+        }
+    }
+#endif
+
+    return !error;
+}
+
+
+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;
+
+    template<typename T, typename U>
+    DeviceEntry(T&& name, U&& pattern)
+        : mName{std::forward<T>(name)}, mPattern{std::forward<U>(pattern)}
+    { }
+};
+
+al::vector<DeviceEntry> PlaybackList;
+
+
+void EnumerateDevices(jack_client_t *client, al::vector<DeviceEntry> &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;
+
+            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 name{portdev.data(), portdev.size()};
+            list.emplace_back(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("JACK", "");
+        }
+    }
+
+    if(auto listopt = ConfigValueStr(nullptr, "jack", "custom-devices"))
+    {
+        for(size_t strpos{0};strpos < listopt->size();)
+        {
+            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(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;
+        }
+    }
+
+    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)
+        {
+            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);
+            }
+        }
+    }
+}
+
+
+struct JackPlayback final : public BackendBase {
+    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); }
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+    ClockLatency getClockLatency() override;
+
+    std::string mPortPattern;
+
+    jack_client_t *mClient{nullptr};
+    std::array<jack_port_t*,MAX_OUTPUT_CHANNELS> mPort{};
+
+    std::mutex mMutex;
+
+    std::atomic<bool> mPlaying{false};
+    bool mRTMixing{false};
+    RingBufferPtr mRing;
+    al::semaphore mSem;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(JackPlayback)
+};
+
+JackPlayback::~JackPlayback()
+{
+    if(!mClient)
+        return;
+
+    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(mPlaying.load(std::memory_order_acquire)) LIKELY
+        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;
+    size_t numchans{0};
+    for(auto port : mPort)
+    {
+        if(!port) break;
+        out[numchans++] = static_cast<float*>(jack_port_get_buffer(port, numframes));
+    }
+
+    jack_nframes_t total{0};
+    if(mPlaying.load(std::memory_order_acquire)) LIKELY
+    {
+        auto data = mRing->getReadVector();
+        jack_nframes_t todo{minu(numframes, static_cast<uint>(data.first.len))};
+        auto write_first = [&data,numchans,todo](float *outbuf) -> float*
+        {
+            const float *RESTRICT in = reinterpret_cast<float*>(data.first.buf);
+            auto deinterlace_input = [&in,numchans]() noexcept -> float
+            {
+                float ret{*in};
+                in += numchans;
+                return ret;
+            };
+            std::generate_n(outbuf, todo, deinterlace_input);
+            data.first.buf += sizeof(float);
+            return outbuf + todo;
+        };
+        std::transform(out.begin(), out.begin()+numchans, out.begin(), write_first);
+        total += todo;
+
+        todo = minu(numframes-total, static_cast<uint>(data.second.len));
+        if(todo > 0)
+        {
+            auto write_second = [&data,numchans,todo](float *outbuf) -> float*
+            {
+                const float *RESTRICT in = reinterpret_cast<float*>(data.second.buf);
+                auto deinterlace_input = [&in,numchans]() noexcept -> float
+                {
+                    float ret{*in};
+                    in += numchans;
+                    return ret;
+                };
+                std::generate_n(outbuf, todo, deinterlace_input);
+                data.second.buf += sizeof(float);
+                return outbuf + todo;
+            };
+            std::transform(out.begin(), out.begin()+numchans, out.begin(), write_second);
+            total += todo;
+        }
+
+        mRing->readAdvance(total);
+        mSem.post();
+    }
+
+    if(numframes > total)
+    {
+        const jack_nframes_t todo{numframes - total};
+        auto clear_buf = [todo](float *outbuf) -> void { std::fill_n(outbuf, todo, 0.0f); };
+        std::for_each(out.begin(), out.begin()+numchans, clear_buf);
+    }
+
+    return 0;
+}
+
+int JackPlayback::mixerProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    const size_t frame_step{mDevice->channelsFromFmt()};
+
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        if(mRing->writeSpace() < mDevice->UpdateSize)
+        {
+            mSem.wait();
+            continue;
+        }
+
+        auto data = mRing->getWriteVector();
+        size_t todo{data.first.len + data.second.len};
+        todo -= todo%mDevice->UpdateSize;
+
+        const auto len1 = static_cast<uint>(minz(data.first.len, todo));
+        const auto len2 = static_cast<uint>(minz(data.second.len, todo-len1));
+
+        std::lock_guard<std::mutex> _{mMutex};
+        mDevice->renderSamples(data.first.buf, len1, frame_step);
+        if(len2 > 0)
+            mDevice->renderSamples(data.second.buf, len2, frame_step);
+        mRing->writeAdvance(todo);
+    }
+
+    return 0;
+}
+
+
+void JackPlayback::open(const char *name)
+{
+    if(!mClient)
+    {
+        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?name:""};
+        mPortPattern = iter->mPattern;
+    }
+
+    mRTMixing = GetConfigValueBool(name, "jack", "rt-mix", true);
+    jack_set_process_callback(mClient,
+        mRTMixing ? &JackPlayback::processRtC : &JackPlayback::processC, this);
+
+    mDevice->DeviceName = name;
+}
+
+bool JackPlayback::reset()
+{
+    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
+     * ready for when requested.
+     */
+    mDevice->Frequency = jack_get_sample_rate(mClient);
+    mDevice->UpdateSize = jack_get_buffer_size(mClient);
+    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 = 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("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)
+            mDevice->FmtChans = DevFmtMono;
+        else
+        {
+            ports_end = mPort.begin()+2;
+            while(bad_port != ports_end)
+            {
+                jack_port_unregister(mClient, *(--bad_port));
+                *bad_port = nullptr;
+            }
+            mDevice->FmtChans = DevFmtStereo;
+        }
+    }
+
+    setDefaultChannelOrder();
+
+    return true;
+}
+
+void JackPlayback::start()
+{
+    if(jack_activate(mClient))
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to activate client"};
+
+    const char *devname{mDevice->DeviceName.c_str()};
+    if(ConfigValueBool(devname, "jack", "connect-ports").value_or(true))
+    {
+        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 playback ports found"};
+        }
+
+        for(size_t i{0};i < al::size(mPort) && mPort[i];++i)
+        {
+            if(!pnames[i])
+            {
+                ERR("No physical playback port for \"%s\"\n", jack_port_name(mPort[i]));
+                break;
+            }
+            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
+     * (it won't change again after jack_activate), then allocate the ring
+     * buffer with the appropriate size.
+     */
+    mDevice->Frequency = jack_get_sample_rate(mClient);
+    mDevice->UpdateSize = jack_get_buffer_size(mClient);
+    mDevice->BufferSize = mDevice->UpdateSize * 2;
+
+    mRing = nullptr;
+    if(mRTMixing)
+        mPlaying.store(true, std::memory_order_release);
+    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(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);
+    }
+}
+
+
+ClockLatency JackPlayback::getClockLatency()
+{
+    ClockLatency ret;
+
+    std::lock_guard<std::mutex> _{mMutex};
+    ret.ClockTime = GetDeviceClockTime(mDevice);
+    ret.Latency  = std::chrono::seconds{mRing ? mRing->readSpace() : mDevice->UpdateSize};
+    ret.Latency /= mDevice->Frequency;
+
+    return ret;
+}
+
+
+void jack_msg_handler(const char *message)
+{
+    WARN("%s\n", message);
+}
+
+} // namespace
+
+bool JackBackendFactory::init()
+{
+    if(!jack_load())
+        return false;
+
+    if(!GetConfigValueBool(nullptr, "jack", "spawn-server", false))
+        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(client_name, ClientOptions, &status, nullptr)};
+    jack_set_error_function(old_error_cb);
+    if(!client)
+    {
+        WARN("jack_client_open() failed, 0x%02x\n", status);
+        if((status&JackServerFailed) && !(ClientOptions&JackNoStartServer))
+            ERR("Unable to connect to JACK server\n");
+        return false;
+    }
+
+    jack_client_close(client);
+    return true;
+}
+
+bool JackBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback); }
+
+std::string JackBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    auto append_name = [&outnames](const DeviceEntry &entry) -> void
+    {
+        /* 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:
+        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:
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr JackBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new JackPlayback{device}};
+    return nullptr;
+}
+
+BackendFactory &JackBackendFactory::getFactory()
+{
+    static JackBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/jack.h b/alc/backends/jack.h
new file mode 100644 (file)
index 0000000..b83f24d
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_JACK_H
+#define BACKENDS_JACK_H
+
+#include "base.h"
+
+struct JackBackendFactory 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_JACK_H */
diff --git a/alc/backends/loopback.cpp b/alc/backends/loopback.cpp
new file mode 100644 (file)
index 0000000..bf4ab24
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2011 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 "loopback.h"
+
+#include "core/device.h"
+
+
+namespace {
+
+struct LoopbackBackend final : public BackendBase {
+    LoopbackBackend(DeviceBase *device) noexcept : BackendBase{device} { }
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    DEF_NEWDEL(LoopbackBackend)
+};
+
+
+void LoopbackBackend::open(const char *name)
+{
+    mDevice->DeviceName = name;
+}
+
+bool LoopbackBackend::reset()
+{
+    setDefaultWFXChannelOrder();
+    return true;
+}
+
+void LoopbackBackend::start()
+{ }
+
+void LoopbackBackend::stop()
+{ }
+
+} // namespace
+
+
+bool LoopbackBackendFactory::init()
+{ return true; }
+
+bool LoopbackBackendFactory::querySupport(BackendType)
+{ return true; }
+
+std::string LoopbackBackendFactory::probe(BackendType)
+{ return std::string{}; }
+
+BackendPtr LoopbackBackendFactory::createBackend(DeviceBase *device, BackendType)
+{ return BackendPtr{new LoopbackBackend{device}}; }
+
+BackendFactory &LoopbackBackendFactory::getFactory()
+{
+    static LoopbackBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/loopback.h b/alc/backends/loopback.h
new file mode 100644 (file)
index 0000000..cb42b3c
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_LOOPBACK_H
+#define BACKENDS_LOOPBACK_H
+
+#include "base.h"
+
+struct LoopbackBackendFactory 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_LOOPBACK_H */
diff --git a/alc/backends/null.cpp b/alc/backends/null.cpp
new file mode 100644 (file)
index 0000000..5a8fc25
--- /dev/null
@@ -0,0 +1,179 @@
+/**
+ * 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 "null.h"
+
+#include <exception>
+#include <atomic>
+#include <chrono>
+#include <cstdint>
+#include <cstring>
+#include <functional>
+#include <thread>
+
+#include "core/device.h"
+#include "almalloc.h"
+#include "core/helpers.h"
+#include "threads.h"
+
+
+namespace {
+
+using std::chrono::seconds;
+using std::chrono::milliseconds;
+using std::chrono::nanoseconds;
+
+constexpr char nullDevice[] = "No Output";
+
+
+struct NullBackend final : public BackendBase {
+    NullBackend(DeviceBase *device) noexcept : BackendBase{device} { }
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(NullBackend)
+};
+
+int NullBackend::mixerProc()
+{
+    const milliseconds restTime{mDevice->UpdateSize*1000/mDevice->Frequency / 2};
+
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    int64_t done{0};
+    auto start = std::chrono::steady_clock::now();
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        auto now = std::chrono::steady_clock::now();
+
+        /* This converts from nanoseconds to nanosamples, then to samples. */
+        int64_t avail{std::chrono::duration_cast<seconds>((now-start) * mDevice->Frequency).count()};
+        if(avail-done < mDevice->UpdateSize)
+        {
+            std::this_thread::sleep_for(restTime);
+            continue;
+        }
+        while(avail-done >= mDevice->UpdateSize)
+        {
+            mDevice->renderSamples(nullptr, mDevice->UpdateSize, 0u);
+            done += mDevice->UpdateSize;
+        }
+
+        /* For every completed second, increment the start time and reduce the
+         * samples done. This prevents the difference between the start time
+         * and current time from growing too large, while maintaining the
+         * correct number of samples to render.
+         */
+        if(done >= mDevice->Frequency)
+        {
+            seconds s{done/mDevice->Frequency};
+            start += s;
+            done -= mDevice->Frequency*s.count();
+        }
+    }
+
+    return 0;
+}
+
+
+void NullBackend::open(const char *name)
+{
+    if(!name)
+        name = nullDevice;
+    else if(strcmp(name, nullDevice) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    mDevice->DeviceName = name;
+}
+
+bool NullBackend::reset()
+{
+    setDefaultWFXChannelOrder();
+    return true;
+}
+
+void NullBackend::start()
+{
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&NullBackend::mixerProc), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void NullBackend::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+}
+
+} // namespace
+
+
+bool NullBackendFactory::init()
+{ return true; }
+
+bool NullBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback); }
+
+std::string NullBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    switch(type)
+    {
+    case BackendType::Playback:
+        /* Includes null char. */
+        outnames.append(nullDevice, sizeof(nullDevice));
+        break;
+    case BackendType::Capture:
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr NullBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new NullBackend{device}};
+    return nullptr;
+}
+
+BackendFactory &NullBackendFactory::getFactory()
+{
+    static NullBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/null.h b/alc/backends/null.h
new file mode 100644 (file)
index 0000000..7048cad
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_NULL_H
+#define BACKENDS_NULL_H
+
+#include "base.h"
+
+struct NullBackendFactory 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_NULL_H */
diff --git a/alc/backends/oboe.cpp b/alc/backends/oboe.cpp
new file mode 100644 (file)
index 0000000..461f5a6
--- /dev/null
@@ -0,0 +1,360 @@
+
+#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 "ringbuffer.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.setSampleRateConversionQuality(oboe::SampleRateConversionQuality::High);
+        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:
+#if OBOE_VERSION_MAJOR > 1 || (OBOE_VERSION_MAJOR == 1 && OBOE_VERSION_MINOR >= 6)
+            format = oboe::AudioFormat::I32;
+            break;
+#endif
+        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;
+#if OBOE_VERSION_MAJOR > 1 || (OBOE_VERSION_MAJOR == 1 && OBOE_VERSION_MINOR >= 6)
+    case oboe::AudioFormat::I32:
+        mDevice->FmtType = DevFmtInt;
+        break;
+    case oboe::AudioFormat::I24:
+#endif
+    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, public oboe::AudioStreamCallback {
+    OboeCapture(DeviceBase *device) : BackendBase{device} { }
+
+    oboe::ManagedStream mStream;
+
+    RingBufferPtr mRing{nullptr};
+
+    oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData,
+        int32_t numFrames) override;
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+};
+
+oboe::DataCallbackResult OboeCapture::onAudioReady(oboe::AudioStream*, void *audioData,
+    int32_t numFrames)
+{
+    mRing->write(audioData, static_cast<uint32_t>(numFrames));
+    return oboe::DataCallbackResult::Continue;
+}
+
+
+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)
+        ->setSampleRate(static_cast<int32_t>(mDevice->Frequency))
+        ->setCallback(this);
+    /* 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 DevFmtX714:
+    case DevFmtX3D71:
+    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
+     * convert.
+     */
+    switch(mDevice->FmtType)
+    {
+    case DevFmtShort:
+        builder.setFormat(oboe::AudioFormat::I16);
+        break;
+    case DevFmtFloat:
+        builder.setFormat(oboe::AudioFormat::Float);
+        break;
+    case DevFmtInt:
+#if OBOE_VERSION_MAJOR > 1 || (OBOE_VERSION_MAJOR == 1 && OBOE_VERSION_MINOR >= 6)
+        builder.setFormat(oboe::AudioFormat::I32);
+        break;
+#endif
+    case DevFmtByte:
+    case DevFmtUByte:
+    case DevFmtUShort:
+    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)};
+
+    TRACE("Got stream with properties:\n%s", oboe::convertToText(mStream.get()));
+
+    /* Ensure a minimum ringbuffer size of 100ms. */
+    mRing = RingBuffer::Create(maxu(mDevice->BufferSize, mDevice->Frequency/10),
+        static_cast<uint32_t>(mStream->getBytesPerFrame()), false);
+
+    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()
+{
+    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()
+{ return static_cast<uint>(mRing->readSpace()); }
+
+void OboeCapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, 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;
+}
diff --git a/alc/backends/oboe.h b/alc/backends/oboe.h
new file mode 100644 (file)
index 0000000..a39c244
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_OBOE_H
+#define BACKENDS_OBOE_H
+
+#include "base.h"
+
+struct OboeBackendFactory 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_OBOE_H */
diff --git a/alc/backends/opensl.cpp b/alc/backends/opensl.cpp
new file mode 100644 (file)
index 0000000..f5b98fb
--- /dev/null
@@ -0,0 +1,1005 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* This is an OpenAL backend for Android using the native audio APIs based on
+ * OpenSL ES 1.0.1. It is based on source code for the native-audio sample app
+ * bundled with NDK.
+ */
+
+#include "config.h"
+
+#include "opensl.h"
+
+#include <stdlib.h>
+#include <jni.h>
+
+#include <new>
+#include <array>
+#include <cstring>
+#include <thread>
+#include <functional>
+
+#include "albit.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "opthelpers.h"
+#include "ringbuffer.h"
+#include "threads.h"
+
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+#include <SLES/OpenSLES_AndroidConfiguration.h>
+
+
+namespace {
+
+/* Helper macros */
+#define EXTRACT_VCALL_ARGS(...)  __VA_ARGS__))
+#define VCALL(obj, func)  ((*(obj))->func((obj), EXTRACT_VCALL_ARGS
+#define VCALL0(obj, func)  ((*(obj))->func((obj) EXTRACT_VCALL_ARGS
+
+
+constexpr char opensl_device[] = "OpenSL";
+
+
+constexpr SLuint32 GetChannelMask(DevFmtChannels chans) noexcept
+{
+    switch(chans)
+    {
+    case DevFmtMono: return SL_SPEAKER_FRONT_CENTER;
+    case DevFmtStereo: return SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+    case DevFmtQuad: return SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT |
+        SL_SPEAKER_BACK_LEFT | SL_SPEAKER_BACK_RIGHT;
+    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 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;
+    case DevFmtX71:
+    case DevFmtX3D71: 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 | SL_SPEAKER_SIDE_LEFT | SL_SPEAKER_SIDE_RIGHT;
+    case DevFmtX714: 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 | SL_SPEAKER_SIDE_LEFT | SL_SPEAKER_SIDE_RIGHT |
+        SL_SPEAKER_TOP_FRONT_LEFT | SL_SPEAKER_TOP_FRONT_RIGHT | SL_SPEAKER_TOP_BACK_LEFT |
+        SL_SPEAKER_TOP_BACK_RIGHT;
+    case DevFmtAmbi3D:
+        break;
+    }
+    return 0;
+}
+
+#ifdef SL_ANDROID_DATAFORMAT_PCM_EX
+constexpr SLuint32 GetTypeRepresentation(DevFmtType type) noexcept
+{
+    switch(type)
+    {
+    case DevFmtUByte:
+    case DevFmtUShort:
+    case DevFmtUInt:
+        return SL_ANDROID_PCM_REPRESENTATION_UNSIGNED_INT;
+    case DevFmtByte:
+    case DevFmtShort:
+    case DevFmtInt:
+        return SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT;
+    case DevFmtFloat:
+        return SL_ANDROID_PCM_REPRESENTATION_FLOAT;
+    }
+    return 0;
+}
+#endif
+
+constexpr SLuint32 GetByteOrderEndianness() noexcept
+{
+    if(al::endian::native == al::endian::little)
+        return SL_BYTEORDER_LITTLEENDIAN;
+    return SL_BYTEORDER_BIGENDIAN;
+}
+
+constexpr const char *res_str(SLresult result) noexcept
+{
+    switch(result)
+    {
+    case SL_RESULT_SUCCESS: return "Success";
+    case SL_RESULT_PRECONDITIONS_VIOLATED: return "Preconditions violated";
+    case SL_RESULT_PARAMETER_INVALID: return "Parameter invalid";
+    case SL_RESULT_MEMORY_FAILURE: return "Memory failure";
+    case SL_RESULT_RESOURCE_ERROR: return "Resource error";
+    case SL_RESULT_RESOURCE_LOST: return "Resource lost";
+    case SL_RESULT_IO_ERROR: return "I/O error";
+    case SL_RESULT_BUFFER_INSUFFICIENT: return "Buffer insufficient";
+    case SL_RESULT_CONTENT_CORRUPTED: return "Content corrupted";
+    case SL_RESULT_CONTENT_UNSUPPORTED: return "Content unsupported";
+    case SL_RESULT_CONTENT_NOT_FOUND: return "Content not found";
+    case SL_RESULT_PERMISSION_DENIED: return "Permission denied";
+    case SL_RESULT_FEATURE_UNSUPPORTED: return "Feature unsupported";
+    case SL_RESULT_INTERNAL_ERROR: return "Internal error";
+    case SL_RESULT_UNKNOWN_ERROR: return "Unknown error";
+    case SL_RESULT_OPERATION_ABORTED: return "Operation aborted";
+    case SL_RESULT_CONTROL_LOST: return "Control lost";
+#ifdef SL_RESULT_READONLY
+    case SL_RESULT_READONLY: return "ReadOnly";
+#endif
+#ifdef SL_RESULT_ENGINEOPTION_UNSUPPORTED
+    case SL_RESULT_ENGINEOPTION_UNSUPPORTED: return "Engine option unsupported";
+#endif
+#ifdef SL_RESULT_SOURCE_SINK_INCOMPATIBLE
+    case SL_RESULT_SOURCE_SINK_INCOMPATIBLE: return "Source/Sink incompatible";
+#endif
+    }
+    return "Unknown error code";
+}
+
+inline void PrintErr(SLresult res, const char *str)
+{
+    if(res != SL_RESULT_SUCCESS) UNLIKELY
+        ERR("%s: %s\n", str, res_str(res));
+}
+
+
+struct OpenSLPlayback final : public BackendBase {
+    OpenSLPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~OpenSLPlayback() override;
+
+    void process(SLAndroidSimpleBufferQueueItf bq) noexcept;
+    static void processC(SLAndroidSimpleBufferQueueItf bq, void *context) noexcept
+    { static_cast<OpenSLPlayback*>(context)->process(bq); }
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+    ClockLatency getClockLatency() override;
+
+    /* engine interfaces */
+    SLObjectItf mEngineObj{nullptr};
+    SLEngineItf mEngine{nullptr};
+
+    /* output mix interfaces */
+    SLObjectItf mOutputMix{nullptr};
+
+    /* buffer queue player interfaces */
+    SLObjectItf mBufferQueueObj{nullptr};
+
+    RingBufferPtr mRing{nullptr};
+    al::semaphore mSem;
+
+    std::mutex mMutex;
+
+    uint mFrameSize{0};
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(OpenSLPlayback)
+};
+
+OpenSLPlayback::~OpenSLPlayback()
+{
+    if(mBufferQueueObj)
+        VCALL0(mBufferQueueObj,Destroy)();
+    mBufferQueueObj = nullptr;
+
+    if(mOutputMix)
+        VCALL0(mOutputMix,Destroy)();
+    mOutputMix = nullptr;
+
+    if(mEngineObj)
+        VCALL0(mEngineObj,Destroy)();
+    mEngineObj = nullptr;
+    mEngine = nullptr;
+}
+
+
+/* this callback handler is called every time a buffer finishes playing */
+void OpenSLPlayback::process(SLAndroidSimpleBufferQueueItf) noexcept
+{
+    /* A note on the ringbuffer usage: The buffer queue seems to hold on to the
+     * pointer passed to the Enqueue method, rather than copying the audio.
+     * Consequently, the ringbuffer contains the audio that is currently queued
+     * and waiting to play. This process() callback is called when a buffer is
+     * finished, so we simply move the read pointer up to indicate the space is
+     * available for writing again, and wake up the mixer thread to mix and
+     * queue more audio.
+     */
+    mRing->readAdvance(1);
+
+    mSem.post();
+}
+
+int OpenSLPlayback::mixerProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    SLPlayItf player;
+    SLAndroidSimpleBufferQueueItf bufferQueue;
+    SLresult result{VCALL(mBufferQueueObj,GetInterface)(SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+        &bufferQueue)};
+    PrintErr(result, "bufferQueue->GetInterface SL_IID_ANDROIDSIMPLEBUFFERQUEUE");
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mBufferQueueObj,GetInterface)(SL_IID_PLAY, &player);
+        PrintErr(result, "bufferQueue->GetInterface SL_IID_PLAY");
+    }
+
+    const size_t frame_step{mDevice->channelsFromFmt()};
+
+    if(SL_RESULT_SUCCESS != result)
+        mDevice->handleDisconnect("Failed to get playback buffer: 0x%08x", result);
+
+    while(SL_RESULT_SUCCESS == result && !mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        if(mRing->writeSpace() == 0)
+        {
+            SLuint32 state{0};
+
+            result = VCALL(player,GetPlayState)(&state);
+            PrintErr(result, "player->GetPlayState");
+            if(SL_RESULT_SUCCESS == result && state != SL_PLAYSTATE_PLAYING)
+            {
+                result = VCALL(player,SetPlayState)(SL_PLAYSTATE_PLAYING);
+                PrintErr(result, "player->SetPlayState");
+            }
+            if(SL_RESULT_SUCCESS != result)
+            {
+                mDevice->handleDisconnect("Failed to start playback: 0x%08x", result);
+                break;
+            }
+
+            if(mRing->writeSpace() == 0)
+            {
+                mSem.wait();
+                continue;
+            }
+        }
+
+        std::unique_lock<std::mutex> dlock{mMutex};
+        auto data = mRing->getWriteVector();
+        mDevice->renderSamples(data.first.buf,
+            static_cast<uint>(data.first.len)*mDevice->UpdateSize, frame_step);
+        if(data.second.len > 0)
+            mDevice->renderSamples(data.second.buf,
+                static_cast<uint>(data.second.len)*mDevice->UpdateSize, frame_step);
+
+        size_t todo{data.first.len + data.second.len};
+        mRing->writeAdvance(todo);
+        dlock.unlock();
+
+        for(size_t i{0};i < todo;i++)
+        {
+            if(!data.first.len)
+            {
+                data.first = data.second;
+                data.second.buf = nullptr;
+                data.second.len = 0;
+            }
+
+            result = VCALL(bufferQueue,Enqueue)(data.first.buf, mDevice->UpdateSize*mFrameSize);
+            PrintErr(result, "bufferQueue->Enqueue");
+            if(SL_RESULT_SUCCESS != result)
+            {
+                mDevice->handleDisconnect("Failed to queue audio: 0x%08x", result);
+                break;
+            }
+
+            data.first.len--;
+            data.first.buf += mDevice->UpdateSize*mFrameSize;
+        }
+    }
+
+    return 0;
+}
+
+
+void OpenSLPlayback::open(const char *name)
+{
+    if(!name)
+        name = opensl_device;
+    else if(strcmp(name, opensl_device) != 0)
+        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");
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mEngineObj,Realize)(SL_BOOLEAN_FALSE);
+        PrintErr(result, "engine->Realize");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mEngineObj,GetInterface)(SL_IID_ENGINE, &mEngine);
+        PrintErr(result, "engine->GetInterface");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mEngine,CreateOutputMix)(&mOutputMix, 0, nullptr, nullptr);
+        PrintErr(result, "engine->CreateOutputMix");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mOutputMix,Realize)(SL_BOOLEAN_FALSE);
+        PrintErr(result, "outputMix->Realize");
+    }
+
+    if(SL_RESULT_SUCCESS != result)
+    {
+        if(mOutputMix)
+            VCALL0(mOutputMix,Destroy)();
+        mOutputMix = nullptr;
+
+        if(mEngineObj)
+            VCALL0(mEngineObj,Destroy)();
+        mEngineObj = nullptr;
+        mEngine = nullptr;
+
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to initialize OpenSL device: 0x%08x", result};
+    }
+
+    mDevice->DeviceName = name;
+}
+
+bool OpenSLPlayback::reset()
+{
+    SLresult result;
+
+    if(mBufferQueueObj)
+        VCALL0(mBufferQueueObj,Destroy)();
+    mBufferQueueObj = nullptr;
+
+    mRing = nullptr;
+
+#if 0
+    if(!mDevice->Flags.get<FrequencyRequest>())
+    {
+        /* FIXME: Disabled until I figure out how to get the Context needed for
+         * the getSystemService call.
+         */
+        JNIEnv *env = Android_GetJNIEnv();
+        jobject jctx = Android_GetContext();
+
+        /* Get necessary stuff for using java.lang.Integer,
+         * android.content.Context, and android.media.AudioManager.
+         */
+        jclass int_cls = JCALL(env,FindClass)("java/lang/Integer");
+        jmethodID int_parseint = JCALL(env,GetStaticMethodID)(int_cls,
+            "parseInt", "(Ljava/lang/String;)I"
+        );
+        TRACE("Integer: %p, parseInt: %p\n", int_cls, int_parseint);
+
+        jclass ctx_cls = JCALL(env,FindClass)("android/content/Context");
+        jfieldID ctx_audsvc = JCALL(env,GetStaticFieldID)(ctx_cls,
+            "AUDIO_SERVICE", "Ljava/lang/String;"
+        );
+        jmethodID ctx_getSysSvc = JCALL(env,GetMethodID)(ctx_cls,
+            "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"
+        );
+        TRACE("Context: %p, AUDIO_SERVICE: %p, getSystemService: %p\n",
+              ctx_cls, ctx_audsvc, ctx_getSysSvc);
+
+        jclass audmgr_cls = JCALL(env,FindClass)("android/media/AudioManager");
+        jfieldID audmgr_prop_out_srate = JCALL(env,GetStaticFieldID)(audmgr_cls,
+            "PROPERTY_OUTPUT_SAMPLE_RATE", "Ljava/lang/String;"
+        );
+        jmethodID audmgr_getproperty = JCALL(env,GetMethodID)(audmgr_cls,
+            "getProperty", "(Ljava/lang/String;)Ljava/lang/String;"
+        );
+        TRACE("AudioManager: %p, PROPERTY_OUTPUT_SAMPLE_RATE: %p, getProperty: %p\n",
+              audmgr_cls, audmgr_prop_out_srate, audmgr_getproperty);
+
+        const char *strchars;
+        jstring strobj;
+
+        /* Now make the calls. */
+        //AudioManager audMgr = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
+        strobj = JCALL(env,GetStaticObjectField)(ctx_cls, ctx_audsvc);
+        jobject audMgr = JCALL(env,CallObjectMethod)(jctx, ctx_getSysSvc, strobj);
+        strchars = JCALL(env,GetStringUTFChars)(strobj, nullptr);
+        TRACE("Context.getSystemService(%s) = %p\n", strchars, audMgr);
+        JCALL(env,ReleaseStringUTFChars)(strobj, strchars);
+
+        //String srateStr = audMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+        strobj = JCALL(env,GetStaticObjectField)(audmgr_cls, audmgr_prop_out_srate);
+        jstring srateStr = JCALL(env,CallObjectMethod)(audMgr, audmgr_getproperty, strobj);
+        strchars = JCALL(env,GetStringUTFChars)(strobj, nullptr);
+        TRACE("audMgr.getProperty(%s) = %p\n", strchars, srateStr);
+        JCALL(env,ReleaseStringUTFChars)(strobj, strchars);
+
+        //int sampleRate = Integer.parseInt(srateStr);
+        sampleRate = JCALL(env,CallStaticIntMethod)(int_cls, int_parseint, srateStr);
+
+        strchars = JCALL(env,GetStringUTFChars)(srateStr, nullptr);
+        TRACE("Got system sample rate %uhz (%s)\n", sampleRate, strchars);
+        JCALL(env,ReleaseStringUTFChars)(srateStr, strchars);
+
+        if(!sampleRate) sampleRate = device->Frequency;
+        else sampleRate = maxu(sampleRate, MIN_OUTPUT_RATE);
+    }
+#endif
+
+    mDevice->FmtChans = DevFmtStereo;
+    mDevice->FmtType = DevFmtShort;
+
+    setDefaultWFXChannelOrder();
+    mFrameSize = mDevice->frameSizeFromFmt();
+
+
+    const std::array<SLInterfaceID,2> ids{{ SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION }};
+    const std::array<SLboolean,2> reqs{{ SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE }};
+
+    SLDataLocator_OutputMix loc_outmix{};
+    loc_outmix.locatorType = SL_DATALOCATOR_OUTPUTMIX;
+    loc_outmix.outputMix = mOutputMix;
+
+    SLDataSink audioSnk{};
+    audioSnk.pLocator = &loc_outmix;
+    audioSnk.pFormat = nullptr;
+
+    SLDataLocator_AndroidSimpleBufferQueue loc_bufq{};
+    loc_bufq.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
+    loc_bufq.numBuffers = mDevice->BufferSize / mDevice->UpdateSize;
+
+    SLDataSource audioSrc{};
+#ifdef SL_ANDROID_DATAFORMAT_PCM_EX
+    SLAndroidDataFormat_PCM_EX format_pcm_ex{};
+    format_pcm_ex.formatType = SL_ANDROID_DATAFORMAT_PCM_EX;
+    format_pcm_ex.numChannels = mDevice->channelsFromFmt();
+    format_pcm_ex.sampleRate = mDevice->Frequency * 1000;
+    format_pcm_ex.bitsPerSample = mDevice->bytesFromFmt() * 8;
+    format_pcm_ex.containerSize = format_pcm_ex.bitsPerSample;
+    format_pcm_ex.channelMask = GetChannelMask(mDevice->FmtChans);
+    format_pcm_ex.endianness = GetByteOrderEndianness();
+    format_pcm_ex.representation = GetTypeRepresentation(mDevice->FmtType);
+
+    audioSrc.pLocator = &loc_bufq;
+    audioSrc.pFormat = &format_pcm_ex;
+
+    result = VCALL(mEngine,CreateAudioPlayer)(&mBufferQueueObj, &audioSrc, &audioSnk, ids.size(),
+        ids.data(), reqs.data());
+    if(SL_RESULT_SUCCESS != result)
+#endif
+    {
+        /* Alter sample type according to what SLDataFormat_PCM can support. */
+        switch(mDevice->FmtType)
+        {
+        case DevFmtByte: mDevice->FmtType = DevFmtUByte; break;
+        case DevFmtUInt: mDevice->FmtType = DevFmtInt; break;
+        case DevFmtFloat:
+        case DevFmtUShort: mDevice->FmtType = DevFmtShort; break;
+        case DevFmtUByte:
+        case DevFmtShort:
+        case DevFmtInt:
+            break;
+        }
+
+        SLDataFormat_PCM format_pcm{};
+        format_pcm.formatType = SL_DATAFORMAT_PCM;
+        format_pcm.numChannels = mDevice->channelsFromFmt();
+        format_pcm.samplesPerSec = mDevice->Frequency * 1000;
+        format_pcm.bitsPerSample = mDevice->bytesFromFmt() * 8;
+        format_pcm.containerSize = format_pcm.bitsPerSample;
+        format_pcm.channelMask = GetChannelMask(mDevice->FmtChans);
+        format_pcm.endianness = GetByteOrderEndianness();
+
+        audioSrc.pLocator = &loc_bufq;
+        audioSrc.pFormat = &format_pcm;
+
+        result = VCALL(mEngine,CreateAudioPlayer)(&mBufferQueueObj, &audioSrc, &audioSnk, ids.size(),
+            ids.data(), reqs.data());
+        PrintErr(result, "engine->CreateAudioPlayer");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        /* Set the stream type to "media" (games, music, etc), if possible. */
+        SLAndroidConfigurationItf config;
+        result = VCALL(mBufferQueueObj,GetInterface)(SL_IID_ANDROIDCONFIGURATION, &config);
+        PrintErr(result, "bufferQueue->GetInterface SL_IID_ANDROIDCONFIGURATION");
+        if(SL_RESULT_SUCCESS == result)
+        {
+            SLint32 streamType = SL_ANDROID_STREAM_MEDIA;
+            result = VCALL(config,SetConfiguration)(SL_ANDROID_KEY_STREAM_TYPE, &streamType,
+                sizeof(streamType));
+            PrintErr(result, "config->SetConfiguration");
+        }
+
+        /* Clear any error since this was optional. */
+        result = SL_RESULT_SUCCESS;
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mBufferQueueObj,Realize)(SL_BOOLEAN_FALSE);
+        PrintErr(result, "bufferQueue->Realize");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        const uint num_updates{mDevice->BufferSize / mDevice->UpdateSize};
+        mRing = RingBuffer::Create(num_updates, mFrameSize*mDevice->UpdateSize, true);
+    }
+
+    if(SL_RESULT_SUCCESS != result)
+    {
+        if(mBufferQueueObj)
+            VCALL0(mBufferQueueObj,Destroy)();
+        mBufferQueueObj = nullptr;
+
+        return false;
+    }
+
+    return true;
+}
+
+void OpenSLPlayback::start()
+{
+    mRing->reset();
+
+    SLAndroidSimpleBufferQueueItf bufferQueue;
+    SLresult result{VCALL(mBufferQueueObj,GetInterface)(SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+        &bufferQueue)};
+    PrintErr(result, "bufferQueue->GetInterface");
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(bufferQueue,RegisterCallback)(&OpenSLPlayback::processC, this);
+        PrintErr(result, "bufferQueue->RegisterCallback");
+    }
+    if(SL_RESULT_SUCCESS != result)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to register callback: 0x%08x", result};
+
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread(std::mem_fn(&OpenSLPlayback::mixerProc), this);
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void OpenSLPlayback::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+
+    mSem.post();
+    mThread.join();
+
+    SLPlayItf player;
+    SLresult result{VCALL(mBufferQueueObj,GetInterface)(SL_IID_PLAY, &player)};
+    PrintErr(result, "bufferQueue->GetInterface");
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(player,SetPlayState)(SL_PLAYSTATE_STOPPED);
+        PrintErr(result, "player->SetPlayState");
+    }
+
+    SLAndroidSimpleBufferQueueItf bufferQueue;
+    result = VCALL(mBufferQueueObj,GetInterface)(SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &bufferQueue);
+    PrintErr(result, "bufferQueue->GetInterface");
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL0(bufferQueue,Clear)();
+        PrintErr(result, "bufferQueue->Clear");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(bufferQueue,RegisterCallback)(nullptr, nullptr);
+        PrintErr(result, "bufferQueue->RegisterCallback");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        SLAndroidSimpleBufferQueueState state;
+        do {
+            std::this_thread::yield();
+            result = VCALL(bufferQueue,GetState)(&state);
+        } while(SL_RESULT_SUCCESS == result && state.count > 0);
+        PrintErr(result, "bufferQueue->GetState");
+
+        mRing->reset();
+    }
+}
+
+ClockLatency OpenSLPlayback::getClockLatency()
+{
+    ClockLatency ret;
+
+    std::lock_guard<std::mutex> _{mMutex};
+    ret.ClockTime = GetDeviceClockTime(mDevice);
+    ret.Latency  = std::chrono::seconds{mRing->readSpace() * mDevice->UpdateSize};
+    ret.Latency /= mDevice->Frequency;
+
+    return ret;
+}
+
+
+struct OpenSLCapture final : public BackendBase {
+    OpenSLCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~OpenSLCapture() override;
+
+    void process(SLAndroidSimpleBufferQueueItf bq) noexcept;
+    static void processC(SLAndroidSimpleBufferQueueItf bq, void *context) noexcept
+    { static_cast<OpenSLCapture*>(context)->process(bq); }
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    /* engine interfaces */
+    SLObjectItf mEngineObj{nullptr};
+    SLEngineItf mEngine;
+
+    /* recording interfaces */
+    SLObjectItf mRecordObj{nullptr};
+
+    RingBufferPtr mRing{nullptr};
+    uint mSplOffset{0u};
+
+    uint mFrameSize{0};
+
+    DEF_NEWDEL(OpenSLCapture)
+};
+
+OpenSLCapture::~OpenSLCapture()
+{
+    if(mRecordObj)
+        VCALL0(mRecordObj,Destroy)();
+    mRecordObj = nullptr;
+
+    if(mEngineObj)
+        VCALL0(mEngineObj,Destroy)();
+    mEngineObj = nullptr;
+    mEngine = nullptr;
+}
+
+
+void OpenSLCapture::process(SLAndroidSimpleBufferQueueItf) noexcept
+{
+    /* A new chunk has been written into the ring buffer, advance it. */
+    mRing->writeAdvance(1);
+}
+
+
+void OpenSLCapture::open(const char* name)
+{
+    if(!name)
+        name = opensl_device;
+    else if(strcmp(name, opensl_device) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    SLresult result{slCreateEngine(&mEngineObj, 0, nullptr, 0, nullptr, nullptr)};
+    PrintErr(result, "slCreateEngine");
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mEngineObj,Realize)(SL_BOOLEAN_FALSE);
+        PrintErr(result, "engine->Realize");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mEngineObj,GetInterface)(SL_IID_ENGINE, &mEngine);
+        PrintErr(result, "engine->GetInterface");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        mFrameSize = mDevice->frameSizeFromFmt();
+        /* Ensure the total length is at least 100ms */
+        uint length{maxu(mDevice->BufferSize, mDevice->Frequency/10)};
+        /* Ensure the per-chunk length is at least 10ms, and no more than 50ms. */
+        uint update_len{clampu(mDevice->BufferSize/3, mDevice->Frequency/100,
+            mDevice->Frequency/100*5)};
+        uint num_updates{(length+update_len-1) / update_len};
+
+        mRing = RingBuffer::Create(num_updates, update_len*mFrameSize, false);
+
+        mDevice->UpdateSize = update_len;
+        mDevice->BufferSize = static_cast<uint>(mRing->writeSpace() * update_len);
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        const std::array<SLInterfaceID,2> ids{{ SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION }};
+        const std::array<SLboolean,2> reqs{{ SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE }};
+
+        SLDataLocator_IODevice loc_dev{};
+        loc_dev.locatorType = SL_DATALOCATOR_IODEVICE;
+        loc_dev.deviceType = SL_IODEVICE_AUDIOINPUT;
+        loc_dev.deviceID = SL_DEFAULTDEVICEID_AUDIOINPUT;
+        loc_dev.device = nullptr;
+
+        SLDataSource audioSrc{};
+        audioSrc.pLocator = &loc_dev;
+        audioSrc.pFormat = nullptr;
+
+        SLDataLocator_AndroidSimpleBufferQueue loc_bq{};
+        loc_bq.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
+        loc_bq.numBuffers = mDevice->BufferSize / mDevice->UpdateSize;
+
+        SLDataSink audioSnk{};
+#ifdef SL_ANDROID_DATAFORMAT_PCM_EX
+        SLAndroidDataFormat_PCM_EX format_pcm_ex{};
+        format_pcm_ex.formatType = SL_ANDROID_DATAFORMAT_PCM_EX;
+        format_pcm_ex.numChannels = mDevice->channelsFromFmt();
+        format_pcm_ex.sampleRate = mDevice->Frequency * 1000;
+        format_pcm_ex.bitsPerSample = mDevice->bytesFromFmt() * 8;
+        format_pcm_ex.containerSize = format_pcm_ex.bitsPerSample;
+        format_pcm_ex.channelMask = GetChannelMask(mDevice->FmtChans);
+        format_pcm_ex.endianness = GetByteOrderEndianness();
+        format_pcm_ex.representation = GetTypeRepresentation(mDevice->FmtType);
+
+        audioSnk.pLocator = &loc_bq;
+        audioSnk.pFormat = &format_pcm_ex;
+        result = VCALL(mEngine,CreateAudioRecorder)(&mRecordObj, &audioSrc, &audioSnk,
+            ids.size(), ids.data(), reqs.data());
+        if(SL_RESULT_SUCCESS != result)
+#endif
+        {
+            /* Fallback to SLDataFormat_PCM only if it supports the desired
+             * sample type.
+             */
+            if(mDevice->FmtType == DevFmtUByte || mDevice->FmtType == DevFmtShort
+                || mDevice->FmtType == DevFmtInt)
+            {
+                SLDataFormat_PCM format_pcm{};
+                format_pcm.formatType = SL_DATAFORMAT_PCM;
+                format_pcm.numChannels = mDevice->channelsFromFmt();
+                format_pcm.samplesPerSec = mDevice->Frequency * 1000;
+                format_pcm.bitsPerSample = mDevice->bytesFromFmt() * 8;
+                format_pcm.containerSize = format_pcm.bitsPerSample;
+                format_pcm.channelMask = GetChannelMask(mDevice->FmtChans);
+                format_pcm.endianness = GetByteOrderEndianness();
+
+                audioSnk.pLocator = &loc_bq;
+                audioSnk.pFormat = &format_pcm;
+                result = VCALL(mEngine,CreateAudioRecorder)(&mRecordObj, &audioSrc, &audioSnk,
+                    ids.size(), ids.data(), reqs.data());
+            }
+            PrintErr(result, "engine->CreateAudioRecorder");
+        }
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        /* Set the record preset to "generic", if possible. */
+        SLAndroidConfigurationItf config;
+        result = VCALL(mRecordObj,GetInterface)(SL_IID_ANDROIDCONFIGURATION, &config);
+        PrintErr(result, "recordObj->GetInterface SL_IID_ANDROIDCONFIGURATION");
+        if(SL_RESULT_SUCCESS == result)
+        {
+            SLuint32 preset = SL_ANDROID_RECORDING_PRESET_GENERIC;
+            result = VCALL(config,SetConfiguration)(SL_ANDROID_KEY_RECORDING_PRESET, &preset,
+                sizeof(preset));
+            PrintErr(result, "config->SetConfiguration");
+        }
+
+        /* Clear any error since this was optional. */
+        result = SL_RESULT_SUCCESS;
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mRecordObj,Realize)(SL_BOOLEAN_FALSE);
+        PrintErr(result, "recordObj->Realize");
+    }
+
+    SLAndroidSimpleBufferQueueItf bufferQueue;
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(mRecordObj,GetInterface)(SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &bufferQueue);
+        PrintErr(result, "recordObj->GetInterface");
+    }
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(bufferQueue,RegisterCallback)(&OpenSLCapture::processC, this);
+        PrintErr(result, "bufferQueue->RegisterCallback");
+    }
+    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);
+            PrintErr(result, "bufferQueue->Enqueue");
+        }
+        for(size_t i{0u};i < data.second.len && SL_RESULT_SUCCESS == result;i++)
+        {
+            result = VCALL(bufferQueue,Enqueue)(data.second.buf + chunk_size*i, chunk_size);
+            PrintErr(result, "bufferQueue->Enqueue");
+        }
+    }
+
+    if(SL_RESULT_SUCCESS != result)
+    {
+        if(mRecordObj)
+            VCALL0(mRecordObj,Destroy)();
+        mRecordObj = nullptr;
+
+        if(mEngineObj)
+            VCALL0(mEngineObj,Destroy)();
+        mEngineObj = nullptr;
+        mEngine = nullptr;
+
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to initialize OpenSL device: 0x%08x", result};
+    }
+
+    mDevice->DeviceName = name;
+}
+
+void OpenSLCapture::start()
+{
+    SLRecordItf record;
+    SLresult result{VCALL(mRecordObj,GetInterface)(SL_IID_RECORD, &record)};
+    PrintErr(result, "recordObj->GetInterface");
+
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(record,SetRecordState)(SL_RECORDSTATE_RECORDING);
+        PrintErr(result, "record->SetRecordState");
+    }
+    if(SL_RESULT_SUCCESS != result)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start capture: 0x%08x", result};
+}
+
+void OpenSLCapture::stop()
+{
+    SLRecordItf record;
+    SLresult result{VCALL(mRecordObj,GetInterface)(SL_IID_RECORD, &record)};
+    PrintErr(result, "recordObj->GetInterface");
+
+    if(SL_RESULT_SUCCESS == result)
+    {
+        result = VCALL(record,SetRecordState)(SL_RECORDSTATE_PAUSED);
+        PrintErr(result, "record->SetRecordState");
+    }
+}
+
+void OpenSLCapture::captureSamples(al::byte *buffer, uint samples)
+{
+    const uint update_size{mDevice->UpdateSize};
+    const uint chunk_size{update_size * mFrameSize};
+
+    /* Read the desired samples from the ring buffer then advance its read
+     * pointer.
+     */
+    size_t adv_count{0};
+    auto rdata = mRing->getReadVector();
+    for(uint i{0};i < samples;)
+    {
+        const uint rem{minu(samples - i, update_size - mSplOffset)};
+        std::copy_n(rdata.first.buf + mSplOffset*size_t{mFrameSize}, rem*size_t{mFrameSize},
+            buffer + i*size_t{mFrameSize});
+
+        mSplOffset += rem;
+        if(mSplOffset == update_size)
+        {
+            /* Finished a chunk, reset the offset and advance the read pointer. */
+            mSplOffset = 0;
+
+            ++adv_count;
+            rdata.first.len -= 1;
+            if(!rdata.first.len)
+                rdata.first = rdata.second;
+            else
+                rdata.first.buf += chunk_size;
+        }
+
+        i += rem;
+    }
+
+    SLAndroidSimpleBufferQueueItf bufferQueue{};
+    if(mDevice->Connected.load(std::memory_order_acquire)) LIKELY
+    {
+        const SLresult result{VCALL(mRecordObj,GetInterface)(SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+            &bufferQueue)};
+        PrintErr(result, "recordObj->GetInterface");
+        if(SL_RESULT_SUCCESS != result) UNLIKELY
+        {
+            mDevice->handleDisconnect("Failed to get capture buffer queue: 0x%08x", result);
+            bufferQueue = nullptr;
+        }
+    }
+    if(!bufferQueue || adv_count == 0)
+        return;
+
+    /* For each buffer chunk that was fully read, queue another writable buffer
+     * chunk to keep the OpenSL queue full. This is rather convulated, as a
+     * result of the ring buffer holding more elements than are writable at a
+     * given time. The end of the write vector increments when the read pointer
+     * advances, which will "expose" a previously unwritable element. So for
+     * every element that we've finished reading, we queue that many elements
+     * from the end of the write vector.
+     */
+    mRing->readAdvance(adv_count);
+
+    SLresult result{SL_RESULT_SUCCESS};
+    auto wdata = mRing->getWriteVector();
+    if(adv_count > wdata.second.len) LIKELY
+    {
+        auto len1 = std::min(wdata.first.len, adv_count-wdata.second.len);
+        auto buf1 = wdata.first.buf + chunk_size*(wdata.first.len-len1);
+        for(size_t i{0u};i < len1 && SL_RESULT_SUCCESS == result;i++)
+        {
+            result = VCALL(bufferQueue,Enqueue)(buf1 + chunk_size*i, chunk_size);
+            PrintErr(result, "bufferQueue->Enqueue");
+        }
+    }
+    if(wdata.second.len > 0)
+    {
+        auto len2 = std::min(wdata.second.len, adv_count);
+        auto buf2 = wdata.second.buf + chunk_size*(wdata.second.len-len2);
+        for(size_t i{0u};i < len2 && SL_RESULT_SUCCESS == result;i++)
+        {
+            result = VCALL(bufferQueue,Enqueue)(buf2 + chunk_size*i, chunk_size);
+            PrintErr(result, "bufferQueue->Enqueue");
+        }
+    }
+}
+
+uint OpenSLCapture::availableSamples()
+{ return static_cast<uint>(mRing->readSpace()*mDevice->UpdateSize - mSplOffset); }
+
+} // namespace
+
+bool OSLBackendFactory::init() { return true; }
+
+bool OSLBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+std::string OSLBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    switch(type)
+    {
+    case BackendType::Playback:
+    case BackendType::Capture:
+        /* Includes null char. */
+        outnames.append(opensl_device, sizeof(opensl_device));
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr OSLBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new OpenSLPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new OpenSLCapture{device}};
+    return nullptr;
+}
+
+BackendFactory &OSLBackendFactory::getFactory()
+{
+    static OSLBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/opensl.h b/alc/backends/opensl.h
new file mode 100644 (file)
index 0000000..b816244
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_OSL_H
+#define BACKENDS_OSL_H
+
+#include "base.h"
+
+struct OSLBackendFactory 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_OSL_H */
diff --git a/alc/backends/oss.cpp b/alc/backends/oss.cpp
new file mode 100644 (file)
index 0000000..6d4fa26
--- /dev/null
@@ -0,0 +1,690 @@
+/**
+ * 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 "oss.h"
+
+#include <fcntl.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <atomic>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <exception>
+#include <functional>
+#include <memory>
+#include <new>
+#include <string>
+#include <thread>
+#include <utility>
+
+#include "albyte.h"
+#include "alc/alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "vector.h"
+
+#include <sys/soundcard.h>
+
+/*
+ * The OSS documentation talks about SOUND_MIXER_READ, but the header
+ * only contains MIXER_READ. Play safe. Same for WRITE.
+ */
+#ifndef SOUND_MIXER_READ
+#define SOUND_MIXER_READ MIXER_READ
+#endif
+#ifndef SOUND_MIXER_WRITE
+#define SOUND_MIXER_WRITE MIXER_WRITE
+#endif
+
+#if defined(SOUND_VERSION) && (SOUND_VERSION < 0x040000)
+#define ALC_OSS_COMPAT
+#endif
+#ifndef SNDCTL_AUDIOINFO
+#define ALC_OSS_COMPAT
+#endif
+
+/*
+ * FreeBSD strongly discourages the use of specific devices,
+ * such as those returned in oss_audioinfo.devnode
+ */
+#ifdef __FreeBSD__
+#define ALC_OSS_DEVNODE_TRUC
+#endif
+
+namespace {
+
+constexpr char DefaultName[] = "OSS Default";
+std::string DefaultPlayback{"/dev/dsp"};
+std::string DefaultCapture{"/dev/dsp"};
+
+struct DevMap {
+    std::string name;
+    std::string device_name;
+};
+
+al::vector<DevMap> PlaybackDevices;
+al::vector<DevMap> CaptureDevices;
+
+
+#ifdef ALC_OSS_COMPAT
+
+#define DSP_CAP_OUTPUT 0x00020000
+#define DSP_CAP_INPUT 0x00010000
+void ALCossListPopulate(al::vector<DevMap> &devlist, int type)
+{
+    devlist.emplace_back(DevMap{DefaultName, (type==DSP_CAP_INPUT) ? DefaultCapture : DefaultPlayback});
+}
+
+#else
+
+void ALCossListAppend(al::vector<DevMap> &list, al::span<const char> handle, al::span<const char> path)
+{
+#ifdef ALC_OSS_DEVNODE_TRUC
+    for(size_t i{0};i < path.size();++i)
+    {
+        if(path[i] == '.' && handle.size() + i >= path.size())
+        {
+            const size_t hoffset{handle.size() + i - path.size()};
+            if(strncmp(path.data() + i, handle.data() + hoffset, path.size() - i) == 0)
+                handle = handle.first(hoffset);
+            path = path.first(i);
+        }
+    }
+#endif
+    if(handle.empty())
+        handle = path;
+
+    std::string basename{handle.data(), handle.size()};
+    std::string devname{path.data(), path.size()};
+
+    auto match_devname = [&devname](const DevMap &entry) -> bool
+    { return entry.device_name == devname; };
+    if(std::find_if(list.cbegin(), list.cend(), match_devname) != list.cend())
+        return;
+
+    auto checkName = [&list](const std::string &name) -> bool
+    {
+        auto match_name = [&name](const DevMap &entry) -> bool { return entry.name == name; };
+        return std::find_if(list.cbegin(), list.cend(), match_name) != list.cend();
+    };
+    int count{1};
+    std::string newname{basename};
+    while(checkName(newname))
+    {
+        newname = basename;
+        newname += " #";
+        newname += std::to_string(++count);
+    }
+
+    list.emplace_back(DevMap{std::move(newname), std::move(devname)});
+    const DevMap &entry = list.back();
+
+    TRACE("Got device \"%s\", \"%s\"\n", entry.name.c_str(), entry.device_name.c_str());
+}
+
+void ALCossListPopulate(al::vector<DevMap> &devlist, int type_flag)
+{
+    int fd{open("/dev/mixer", O_RDONLY)};
+    if(fd < 0)
+    {
+        TRACE("Could not open /dev/mixer: %s\n", strerror(errno));
+        goto done;
+    }
+
+    oss_sysinfo si;
+    if(ioctl(fd, SNDCTL_SYSINFO, &si) == -1)
+    {
+        TRACE("SNDCTL_SYSINFO failed: %s\n", strerror(errno));
+        goto done;
+    }
+
+    for(int i{0};i < si.numaudios;i++)
+    {
+        oss_audioinfo ai;
+        ai.dev = i;
+        if(ioctl(fd, SNDCTL_AUDIOINFO, &ai) == -1)
+        {
+            ERR("SNDCTL_AUDIOINFO (%d) failed: %s\n", i, strerror(errno));
+            continue;
+        }
+        if(!(ai.caps&type_flag) || ai.devnode[0] == '\0')
+            continue;
+
+        al::span<const char> handle;
+        if(ai.handle[0] != '\0')
+            handle = {ai.handle, strnlen(ai.handle, sizeof(ai.handle))};
+        else
+            handle = {ai.name, strnlen(ai.name, sizeof(ai.name))};
+        al::span<const char> devnode{ai.devnode, strnlen(ai.devnode, sizeof(ai.devnode))};
+
+        ALCossListAppend(devlist, handle, devnode);
+    }
+
+done:
+    if(fd >= 0)
+        close(fd);
+    fd = -1;
+
+    const char *defdev{((type_flag==DSP_CAP_INPUT) ? DefaultCapture : DefaultPlayback).c_str()};
+    auto iter = std::find_if(devlist.cbegin(), devlist.cend(),
+        [defdev](const DevMap &entry) -> bool
+        { return entry.device_name == defdev; }
+    );
+    if(iter == devlist.cend())
+        devlist.insert(devlist.begin(), DevMap{DefaultName, defdev});
+    else
+    {
+        DevMap entry{std::move(*iter)};
+        devlist.erase(iter);
+        devlist.insert(devlist.begin(), std::move(entry));
+    }
+    devlist.shrink_to_fit();
+}
+
+#endif
+
+uint log2i(uint x)
+{
+    uint y{0};
+    while(x > 1)
+    {
+        x >>= 1;
+        y++;
+    }
+    return y;
+}
+
+
+struct OSSPlayback final : public BackendBase {
+    OSSPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~OSSPlayback() override;
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    int mFd{-1};
+
+    al::vector<al::byte> mMixData;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(OSSPlayback)
+};
+
+OSSPlayback::~OSSPlayback()
+{
+    if(mFd != -1)
+        ::close(mFd);
+    mFd = -1;
+}
+
+
+int OSSPlayback::mixerProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    const size_t frame_step{mDevice->channelsFromFmt()};
+    const size_t frame_size{mDevice->frameSizeFromFmt()};
+
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        pollfd pollitem{};
+        pollitem.fd = mFd;
+        pollitem.events = POLLOUT;
+
+        int pret{poll(&pollitem, 1, 1000)};
+        if(pret < 0)
+        {
+            if(errno == EINTR || errno == EAGAIN)
+                continue;
+            ERR("poll failed: %s\n", strerror(errno));
+            mDevice->handleDisconnect("Failed waiting for playback buffer: %s", strerror(errno));
+            break;
+        }
+        else if(pret == 0)
+        {
+            WARN("poll timeout\n");
+            continue;
+        }
+
+        al::byte *write_ptr{mMixData.data()};
+        size_t to_write{mMixData.size()};
+        mDevice->renderSamples(write_ptr, static_cast<uint>(to_write/frame_size), frame_step);
+        while(to_write > 0 && !mKillNow.load(std::memory_order_acquire))
+        {
+            ssize_t wrote{write(mFd, write_ptr, to_write)};
+            if(wrote < 0)
+            {
+                if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
+                    continue;
+                ERR("write failed: %s\n", strerror(errno));
+                mDevice->handleDisconnect("Failed writing playback samples: %s", strerror(errno));
+                break;
+            }
+
+            to_write -= static_cast<size_t>(wrote);
+            write_ptr += wrote;
+        }
+    }
+
+    return 0;
+}
+
+
+void OSSPlayback::open(const char *name)
+{
+    const char *devname{DefaultPlayback.c_str()};
+    if(!name)
+        name = DefaultName;
+    else
+    {
+        if(PlaybackDevices.empty())
+            ALCossListPopulate(PlaybackDevices, DSP_CAP_OUTPUT);
+
+        auto iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+            [&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};
+        devname = iter->device_name.c_str();
+    }
+
+    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;
+}
+
+bool OSSPlayback::reset()
+{
+    int ossFormat{};
+    switch(mDevice->FmtType)
+    {
+        case DevFmtByte:
+            ossFormat = AFMT_S8;
+            break;
+        case DevFmtUByte:
+            ossFormat = AFMT_U8;
+            break;
+        case DevFmtUShort:
+        case DevFmtInt:
+        case DevFmtUInt:
+        case DevFmtFloat:
+            mDevice->FmtType = DevFmtShort;
+            /* fall-through */
+        case DevFmtShort:
+            ossFormat = AFMT_S16_NE;
+            break;
+    }
+
+    uint periods{mDevice->BufferSize / mDevice->UpdateSize};
+    uint numChannels{mDevice->channelsFromFmt()};
+    uint ossSpeed{mDevice->Frequency};
+    uint frameSize{numChannels * mDevice->bytesFromFmt()};
+    /* According to the OSS spec, 16 bytes (log2(16)) is the minimum. */
+    uint log2FragmentSize{maxu(log2i(mDevice->UpdateSize*frameSize), 4)};
+    uint numFragmentsLogSize{(periods << 16) | log2FragmentSize};
+
+    audio_buf_info info{};
+    const char *err;
+#define CHECKERR(func) if((func) < 0) {                                       \
+    err = #func;                                                              \
+    goto err;                                                                 \
+}
+    /* Don't fail if SETFRAGMENT fails. We can handle just about anything
+     * that's reported back via GETOSPACE */
+    ioctl(mFd, SNDCTL_DSP_SETFRAGMENT, &numFragmentsLogSize);
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_SETFMT, &ossFormat));
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_CHANNELS, &numChannels));
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_SPEED, &ossSpeed));
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_GETOSPACE, &info));
+    if(0)
+    {
+    err:
+        ERR("%s failed: %s\n", err, strerror(errno));
+        return false;
+    }
+#undef CHECKERR
+
+    if(mDevice->channelsFromFmt() != numChannels)
+    {
+        ERR("Failed to set %s, got %d channels instead\n", DevFmtChannelsString(mDevice->FmtChans),
+            numChannels);
+        return false;
+    }
+
+    if(!((ossFormat == AFMT_S8 && mDevice->FmtType == DevFmtByte) ||
+         (ossFormat == AFMT_U8 && mDevice->FmtType == DevFmtUByte) ||
+         (ossFormat == AFMT_S16_NE && mDevice->FmtType == DevFmtShort)))
+    {
+        ERR("Failed to set %s samples, got OSS format %#x\n", DevFmtTypeString(mDevice->FmtType),
+            ossFormat);
+        return false;
+    }
+
+    mDevice->Frequency = ossSpeed;
+    mDevice->UpdateSize = static_cast<uint>(info.fragsize) / frameSize;
+    mDevice->BufferSize = static_cast<uint>(info.fragments) * mDevice->UpdateSize;
+
+    setDefaultChannelOrder();
+
+    mMixData.resize(mDevice->UpdateSize * mDevice->frameSizeFromFmt());
+
+    return true;
+}
+
+void OSSPlayback::start()
+{
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&OSSPlayback::mixerProc), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void OSSPlayback::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    if(ioctl(mFd, SNDCTL_DSP_RESET) != 0)
+        ERR("Error resetting device: %s\n", strerror(errno));
+}
+
+
+struct OSScapture final : public BackendBase {
+    OSScapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~OSScapture() override;
+
+    int recordProc();
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    int mFd{-1};
+
+    RingBufferPtr mRing{nullptr};
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(OSScapture)
+};
+
+OSScapture::~OSScapture()
+{
+    if(mFd != -1)
+        close(mFd);
+    mFd = -1;
+}
+
+
+int OSScapture::recordProc()
+{
+    SetRTPriority();
+    althrd_setname(RECORD_THREAD_NAME);
+
+    const size_t frame_size{mDevice->frameSizeFromFmt()};
+    while(!mKillNow.load(std::memory_order_acquire))
+    {
+        pollfd pollitem{};
+        pollitem.fd = mFd;
+        pollitem.events = POLLIN;
+
+        int sret{poll(&pollitem, 1, 1000)};
+        if(sret < 0)
+        {
+            if(errno == EINTR || errno == EAGAIN)
+                continue;
+            ERR("poll failed: %s\n", strerror(errno));
+            mDevice->handleDisconnect("Failed to check capture samples: %s", strerror(errno));
+            break;
+        }
+        else if(sret == 0)
+        {
+            WARN("poll timeout\n");
+            continue;
+        }
+
+        auto vec = mRing->getWriteVector();
+        if(vec.first.len > 0)
+        {
+            ssize_t amt{read(mFd, vec.first.buf, vec.first.len*frame_size)};
+            if(amt < 0)
+            {
+                ERR("read failed: %s\n", strerror(errno));
+                mDevice->handleDisconnect("Failed reading capture samples: %s", strerror(errno));
+                break;
+            }
+            mRing->writeAdvance(static_cast<size_t>(amt)/frame_size);
+        }
+    }
+
+    return 0;
+}
+
+
+void OSScapture::open(const char *name)
+{
+    const char *devname{DefaultCapture.c_str()};
+    if(!name)
+        name = DefaultName;
+    else
+    {
+        if(CaptureDevices.empty())
+            ALCossListPopulate(CaptureDevices, DSP_CAP_INPUT);
+
+        auto iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+            [&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};
+        devname = iter->device_name.c_str();
+    }
+
+    mFd = ::open(devname, O_RDONLY);
+    if(mFd == -1)
+        throw al::backend_exception{al::backend_error::NoDevice, "Could not open %s: %s", devname,
+            strerror(errno)};
+
+    int ossFormat{};
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        ossFormat = AFMT_S8;
+        break;
+    case DevFmtUByte:
+        ossFormat = AFMT_U8;
+        break;
+    case DevFmtShort:
+        ossFormat = AFMT_S16_NE;
+        break;
+    case DevFmtUShort:
+    case DevFmtInt:
+    case DevFmtUInt:
+    case DevFmtFloat:
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "%s capture samples not supported", DevFmtTypeString(mDevice->FmtType)};
+    }
+
+    uint periods{4};
+    uint numChannels{mDevice->channelsFromFmt()};
+    uint frameSize{numChannels * mDevice->bytesFromFmt()};
+    uint ossSpeed{mDevice->Frequency};
+    /* according to the OSS spec, 16 bytes are the minimum */
+    uint log2FragmentSize{maxu(log2i(mDevice->BufferSize * frameSize / periods), 4)};
+    uint numFragmentsLogSize{(periods << 16) | log2FragmentSize};
+
+    audio_buf_info info{};
+#define CHECKERR(func) if((func) < 0) {                                       \
+    throw al::backend_exception{al::backend_error::DeviceError, #func " failed: %s", \
+        strerror(errno)};                                                     \
+}
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_SETFRAGMENT, &numFragmentsLogSize));
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_SETFMT, &ossFormat));
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_CHANNELS, &numChannels));
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_SPEED, &ossSpeed));
+    CHECKERR(ioctl(mFd, SNDCTL_DSP_GETISPACE, &info));
+#undef CHECKERR
+
+    if(mDevice->channelsFromFmt() != numChannels)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to set %s, got %d channels instead", DevFmtChannelsString(mDevice->FmtChans),
+            numChannels};
+
+    if(!((ossFormat == AFMT_S8 && mDevice->FmtType == DevFmtByte)
+        || (ossFormat == AFMT_U8 && mDevice->FmtType == DevFmtUByte)
+        || (ossFormat == AFMT_S16_NE && mDevice->FmtType == DevFmtShort)))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to set %s samples, got OSS format %#x", DevFmtTypeString(mDevice->FmtType),
+            ossFormat};
+
+    mRing = RingBuffer::Create(mDevice->BufferSize, frameSize, false);
+
+    mDevice->DeviceName = name;
+}
+
+void OSScapture::start()
+{
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&OSScapture::recordProc), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start recording thread: %s", e.what()};
+    }
+}
+
+void OSScapture::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    if(ioctl(mFd, SNDCTL_DSP_RESET) != 0)
+        ERR("Error resetting device: %s\n", strerror(errno));
+}
+
+void OSScapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, samples); }
+
+uint OSScapture::availableSamples()
+{ return static_cast<uint>(mRing->readSpace()); }
+
+} // namespace
+
+
+BackendFactory &OSSBackendFactory::getFactory()
+{
+    static OSSBackendFactory factory{};
+    return factory;
+}
+
+bool OSSBackendFactory::init()
+{
+    if(auto devopt = ConfigValueStr(nullptr, "oss", "device"))
+        DefaultPlayback = std::move(*devopt);
+    if(auto capopt = ConfigValueStr(nullptr, "oss", "capture"))
+        DefaultCapture = std::move(*capopt);
+
+    return true;
+}
+
+bool OSSBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+std::string OSSBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+
+    auto add_device = [&outnames](const DevMap &entry) -> void
+    {
+        struct stat buf;
+        if(stat(entry.device_name.c_str(), &buf) == 0)
+        {
+            /* Includes null char. */
+            outnames.append(entry.name.c_str(), entry.name.length()+1);
+        }
+    };
+
+    switch(type)
+    {
+    case BackendType::Playback:
+        PlaybackDevices.clear();
+        ALCossListPopulate(PlaybackDevices, DSP_CAP_OUTPUT);
+        std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+        break;
+
+    case BackendType::Capture:
+        CaptureDevices.clear();
+        ALCossListPopulate(CaptureDevices, DSP_CAP_INPUT);
+        std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+        break;
+    }
+
+    return outnames;
+}
+
+BackendPtr OSSBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new OSSPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new OSScapture{device}};
+    return nullptr;
+}
diff --git a/alc/backends/oss.h b/alc/backends/oss.h
new file mode 100644 (file)
index 0000000..4f2c00b
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_OSS_H
+#define BACKENDS_OSS_H
+
+#include "base.h"
+
+struct OSSBackendFactory 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_OSS_H */
diff --git a/alc/backends/pipewire.cpp b/alc/backends/pipewire.cpp
new file mode 100644 (file)
index 0000000..c6569a7
--- /dev/null
@@ -0,0 +1,2166 @@
+/**
+ * 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 <thread>
+#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). GCC
+ * doesn't support ignoring -Weverything, so we have the list the individual
+ * warnings to ignore (and ignoring -Winline doesn't seem to work).
+ */
+_Pragma("GCC diagnostic push")
+_Pragma("GCC diagnostic ignored \"-Wpedantic\"")
+_Pragma("GCC diagnostic ignored \"-Wconversion\"")
+_Pragma("GCC diagnostic ignored \"-Wfloat-conversion\"")
+_Pragma("GCC diagnostic ignored \"-Wmissing-field-initializers\"")
+_Pragma("GCC diagnostic ignored \"-Wunused-parameter\"")
+_Pragma("GCC diagnostic ignored \"-Wold-style-cast\"")
+_Pragma("GCC diagnostic ignored \"-Wsign-compare\"")
+_Pragma("GCC diagnostic ignored \"-Winline\"")
+_Pragma("GCC diagnostic ignored \"-Wpragmas\"")
+_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 {
+
+/* Added in 0.3.33, but we currently only require 0.3.23. */
+#ifndef PW_KEY_NODE_RATE
+#define PW_KEY_NODE_RATE "node.rate"
+#endif
+
+using std::chrono::seconds;
+using std::chrono::milliseconds;
+using std::chrono::nanoseconds;
+using uint = unsigned int;
+
+constexpr char pwireDevice[] = "PipeWire Output";
+constexpr char pwireInput[] = "PipeWire Input";
+
+
+bool check_version(const char *version)
+{
+    /* There doesn't seem to be a function to get the version as an integer, so
+     * instead we have to parse the string, which hopefully won't break in the
+     * future.
+     */
+    int major{0}, minor{0}, revision{0};
+    int ret{sscanf(version, "%d.%d.%d", &major, &minor, &revision)};
+    if(ret == 3 && (major > PW_MAJOR || (major == PW_MAJOR && minor > PW_MINOR)
+        || (major == PW_MAJOR && minor == PW_MINOR && revision >= PW_MICRO)))
+        return true;
+    return false;
+}
+
+#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_get_library_version)                                             \
+    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_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)
+#if PW_CHECK_VERSION(0,3,50)
+#define PWIRE_FUNCS2(MAGIC)                                                   \
+    MAGIC(pw_stream_get_time_n)
+#else
+#define PWIRE_FUNCS2(MAGIC)                                                   \
+    MAGIC(pw_stream_get_time)
+#endif
+
+void *pwire_handle;
+#define MAKE_FUNC(f) decltype(f) * p##f;
+PWIRE_FUNCS(MAKE_FUNC)
+PWIRE_FUNCS2(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)
+    PWIRE_FUNCS2(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_get_library_version ppw_get_library_version
+#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_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
+#if PW_CHECK_VERSION(0,3,50)
+#define pw_stream_get_time_n ppw_stream_get_time_n
+#else
+inline auto pw_stream_get_time_n(pw_stream *stream, pw_time *ptime, size_t /*size*/)
+{ return ppw_stream_get_time(stream, ptime); }
+#endif
+#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 | al::to_underlying(rhs)); }
+
+constexpr pw_stream_flags& operator|=(pw_stream_flags &lhs, pw_stream_flags rhs) noexcept
+{ lhs = lhs | rhs; return lhs; }
+
+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(!mInitDone.load(std::memory_order_acquire)) UNLIKELY
+        {
+            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 {
+    uint32_t mId{};
+
+    uint64_t mSerial{};
+    std::string mName;
+    std::string mDevName;
+
+    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 al::to_address(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
+}, X714Map[]{
+    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,
+    SPA_AUDIO_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFR, SPA_AUDIO_CHANNEL_TRL, SPA_AUDIO_CHANNEL_TRR
+};
+
+/**
+ * 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 %" PRIu64 " sample rate: %d (range: %d -> %d)\n", mSerial, 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 %" PRIu64 " sample rate: %d (%s)\n", mSerial, 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 %" PRIu64 " sample rate: %d\n", mSerial, 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, X714Map))
+        mChannels = DevFmtX714;
+    else 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 %" PRIu64 " got %zu position%s for %s%s\n", mSerial, 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 %" PRIu64 " got %d channel%s for %s\n", mSerial, *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 AudioSourceVirtualClass[]{"Audio/Source/Virtual"};
+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(!media_class) UNLIKELY return;
+
+        NodeType ntype{};
+        if(al::strcasecmp(media_class, AudioSinkClass) == 0)
+            ntype = NodeType::Sink;
+        else if(al::strcasecmp(media_class, AudioSourceClass) == 0
+            || al::strcasecmp(media_class, AudioSourceVirtualClass) == 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;
+
+        uint64_t serial_id{info->id};
+#ifdef PW_KEY_OBJECT_SERIAL
+        if(const char *serial_str{spa_dict_lookup(info->props, PW_KEY_OBJECT_SERIAL)})
+        {
+            char *serial_end{};
+            serial_id = std::strtoull(serial_str, &serial_end, 0);
+            if(*serial_end != '\0' || errno == ERANGE)
+            {
+                ERR("Unexpected object serial: %s\n", serial_str);
+                serial_id = info->id;
+            }
+        }
+#endif
+
+        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 %" PRIu64 "\n", nodeName ? nodeName : "(nil)", serial_id);
+
+        DeviceNode &node = DeviceNode::Add(info->id);
+        node.mSerial = serial_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(!node) UNLIKELY 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, AudioSourceVirtualClass) == 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 DevFmtX714: map = X714Map; break;
+    case DevFmtX3D71: 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;
+
+    uint64_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(!pw_buf) UNLIKELY return;
+
+    const al::span<spa_data> datas{pw_buf->buffer->datas,
+        minu(mNumChannels, pw_buf->buffer->n_datas)};
+#if PW_CHECK_VERSION(0,3,49)
+    /* In 0.3.49, pw_buffer::requested specifies the number of samples needed
+     * by the resampler/graph for this audio update.
+     */
+    uint length{static_cast<uint>(pw_buf->requested)};
+#else
+    /* In 0.3.48 and earlier, spa_io_rate_match::size apparently has the number
+     * of samples per update.
+     */
+    uint length{mRateMatch ? mRateMatch->size : 0u};
+#endif
+    /* If no length is specified, use the device's update size as a fallback. */
+    if(!length) UNLIKELY length = mDevice->UpdateSize;
+
+    /* 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).
+     */
+    float **chanptr_end{mChannelPtrs.get()};
+    for(const auto &data : datas)
+    {
+        length = minu(length, data.maxsize/sizeof(float));
+        *chanptr_end = static_cast<float*>(data.data);
+        ++chanptr_end;
+    }
+
+    mDevice->renderSamples({mChannelPtrs.get(), chanptr_end}, length);
+
+    for(const auto &data : datas)
+    {
+        data.chunk->offset = 0;
+        data.chunk->stride = sizeof(float);
+        data.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};
+
+    uint64_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->mSerial;
+        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->mSerial;
+        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.mSerial; };
+        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};
+                const double numbufs{static_cast<double>(mDevice->BufferSize)/mDevice->UpdateSize};
+                mDevice->Frequency = match->mSampleRate;
+                mDevice->UpdateSize = static_cast<uint>(clampd(mDevice->UpdateSize*scale + 0.5,
+                    64.0, 8192.0));
+                mDevice->BufferSize = static_cast<uint>(numbufs*mDevice->UpdateSize + 0.5);
+            }
+            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"};
+
+    /* TODO: Which properties are actually needed here? Any others that could
+     * be useful?
+     */
+    auto&& binary = GetProcBinary();
+    const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"};
+    pw_properties *props{pw_properties_new(PW_KEY_NODE_NAME, appname,
+        PW_KEY_NODE_DESCRIPTION, appname,
+        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};
+
+    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);
+#ifdef PW_KEY_TARGET_OBJECT
+    pw_properties_setf(props, PW_KEY_TARGET_OBJECT, "%" PRIu64, mTargetId);
+#else
+    pw_properties_setf(props, PW_KEY_NODE_TARGET, "%" PRIu64, mTargetId);
+#endif
+
+    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);
+
+    pw_stream_flags flags{PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE
+        | PW_STREAM_FLAG_MAP_BUFFERS};
+    if(GetConfigValueBool(mDevice->DeviceName.c_str(), "pipewire", "rt-mix", true))
+        flags |= PW_STREAM_FLAG_RT_PROCESS;
+    if(int res{pw_stream_connect(mStream.get(), PW_DIRECTION_OUTPUT, PwIdAny, 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). */
+    plock.wait([stream=mStream.get()]()
+    {
+        const char *error{};
+        pw_stream_state 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->UpdateSize with the stream's quantum, and
+     * mDevice->BufferSize with the total known buffering delay from the head
+     * of this playback stream to the tail of the device output.
+     *
+     * This info is apparently not available until after the stream starts.
+     */
+    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).
+     */
+    plock.wait([stream=mStream.get()]()
+    {
+        const char *error{};
+        pw_stream_state state{pw_stream_get_state(stream, &error)};
+        if(state == PW_STREAM_STATE_ERROR)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "PipeWire stream error: %s", error ? error : "(unknown)"};
+        return state == PW_STREAM_STATE_STREAMING;
+    });
+
+    /* HACK: Try to work out the update size and total buffering size. There's
+     * no actual query for this, so we have to work it out from the stream time
+     * info, and assume it stays accurate with future updates. The stream time
+     * info may also not be available right away, so we have to wait until it
+     * is (up to about 2 seconds).
+     */
+    int wait_count{100};
+    do {
+        pw_time ptime{};
+        if(int res{pw_stream_get_time_n(mStream.get(), &ptime, sizeof(ptime))})
+        {
+            ERR("Failed to get PipeWire stream time (res: %d)\n", res);
+            break;
+        }
+
+        /* The rate match size is the update size for each buffer. */
+        const uint updatesize{mRateMatch ? mRateMatch->size : 0u};
+#if PW_CHECK_VERSION(0,3,50)
+        /* Assume ptime.avail_buffers+ptime.queued_buffers is the target buffer
+         * queue size.
+         */
+        if(ptime.rate.denom > 0 && (ptime.avail_buffers || ptime.queued_buffers) && updatesize > 0)
+        {
+            const uint totalbuffers{ptime.avail_buffers + ptime.queued_buffers};
+
+            /* Ensure the delay is in sample frames. */
+            const uint64_t delay{static_cast<uint64_t>(ptime.delay) * mDevice->Frequency *
+                ptime.rate.num / ptime.rate.denom};
+
+            mDevice->UpdateSize = updatesize;
+            mDevice->BufferSize = static_cast<uint>(ptime.buffered + delay +
+                totalbuffers*updatesize);
+            break;
+        }
+#else
+        /* Prior to 0.3.50, we can only measure the delay with the update size,
+         * assuming one buffer and no resample buffering.
+         */
+        if(ptime.rate.denom > 0 && updatesize > 0)
+        {
+            /* Ensure the delay is in sample frames. */
+            const uint64_t delay{static_cast<uint64_t>(ptime.delay) * mDevice->Frequency *
+                ptime.rate.num / ptime.rate.denom};
+
+            mDevice->UpdateSize = updatesize;
+            mDevice->BufferSize = static_cast<uint>(delay + updatesize);
+            break;
+        }
+#endif
+        if(!--wait_count)
+            break;
+
+        plock.unlock();
+        std::this_thread::sleep_for(milliseconds{20});
+        plock.lock();
+    } while(pw_stream_get_state(mStream.get(), nullptr) == PW_STREAM_STATE_STREAMING);
+}
+
+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_n(mStream.get(), &ptime, sizeof(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(ptime.rate.denom < 1) UNLIKELY
+    {
+        /* 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;
+
+    uint64_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(!pw_buf) UNLIKELY 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};
+
+    uint64_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->mSerial;
+        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->mSerial;
+        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.mSerial; };
+        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"};
+
+    auto&& binary = GetProcBinary();
+    const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"};
+    pw_properties *props{pw_properties_new(
+        PW_KEY_NODE_NAME, appname,
+        PW_KEY_NODE_DESCRIPTION, appname,
+        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};
+
+    /* 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);
+#ifdef PW_KEY_TARGET_OBJECT
+    pw_properties_setf(props, PW_KEY_TARGET_OBJECT, "%" PRIu64, mTargetId);
+#else
+    pw_properties_setf(props, PW_KEY_NODE_TARGET, "%" PRIu64, mTargetId);
+#endif
+
+    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, PwIdAny, 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). */
+    plock.wait([stream=mStream.get()]()
+    {
+        const char *error{};
+        pw_stream_state 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};
+
+    plock.wait([stream=mStream.get()]()
+    {
+        const char *error{};
+        pw_stream_state state{pw_stream_get_state(stream, &error)};
+        if(state == PW_STREAM_STATE_ERROR)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "PipeWire stream error: %s", error ? error : "(unknown)"};
+        return state == PW_STREAM_STATE_STREAMING;
+    });
+}
+
+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;
+
+    const char *version{pw_get_library_version()};
+    if(!check_version(version))
+    {
+        WARN("PipeWire version \"%s\" too old (%s or newer required)\n", version,
+            pw_get_headers_version());
+        return false;
+    }
+    TRACE("Found PipeWire version \"%s\" (%s or newer)\n", version, pw_get_headers_version());
+
+    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;
+}
diff --git a/alc/backends/pipewire.h b/alc/backends/pipewire.h
new file mode 100644 (file)
index 0000000..5f93023
--- /dev/null
@@ -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 */
diff --git a/alc/backends/portaudio.cpp b/alc/backends/portaudio.cpp
new file mode 100644 (file)
index 0000000..9c94587
--- /dev/null
@@ -0,0 +1,447 @@
+/**
+ * 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 "portaudio.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#include "alc/alconfig.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/logging.h"
+#include "dynload.h"
+#include "ringbuffer.h"
+
+#include <portaudio.h>
+
+
+namespace {
+
+constexpr char pa_device[] = "PortAudio Default";
+
+
+#ifdef HAVE_DYNLOAD
+void *pa_handle;
+#define MAKE_FUNC(x) decltype(x) * p##x
+MAKE_FUNC(Pa_Initialize);
+MAKE_FUNC(Pa_Terminate);
+MAKE_FUNC(Pa_GetErrorText);
+MAKE_FUNC(Pa_StartStream);
+MAKE_FUNC(Pa_StopStream);
+MAKE_FUNC(Pa_OpenStream);
+MAKE_FUNC(Pa_CloseStream);
+MAKE_FUNC(Pa_GetDefaultOutputDevice);
+MAKE_FUNC(Pa_GetDefaultInputDevice);
+MAKE_FUNC(Pa_GetStreamInfo);
+#undef MAKE_FUNC
+
+#ifndef IN_IDE_PARSER
+#define Pa_Initialize                  pPa_Initialize
+#define Pa_Terminate                   pPa_Terminate
+#define Pa_GetErrorText                pPa_GetErrorText
+#define Pa_StartStream                 pPa_StartStream
+#define Pa_StopStream                  pPa_StopStream
+#define Pa_OpenStream                  pPa_OpenStream
+#define Pa_CloseStream                 pPa_CloseStream
+#define Pa_GetDefaultOutputDevice      pPa_GetDefaultOutputDevice
+#define Pa_GetDefaultInputDevice       pPa_GetDefaultInputDevice
+#define Pa_GetStreamInfo               pPa_GetStreamInfo
+#endif
+#endif
+
+
+struct PortPlayback final : public BackendBase {
+    PortPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~PortPlayback() override;
+
+    int writeCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
+        const PaStreamCallbackTimeInfo *timeInfo, const PaStreamCallbackFlags statusFlags) noexcept;
+    static int writeCallbackC(const void *inputBuffer, void *outputBuffer,
+        unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo,
+        const PaStreamCallbackFlags statusFlags, void *userData) noexcept
+    {
+        return static_cast<PortPlayback*>(userData)->writeCallback(inputBuffer, outputBuffer,
+            framesPerBuffer, timeInfo, statusFlags);
+    }
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    PaStream *mStream{nullptr};
+    PaStreamParameters mParams{};
+    uint mUpdateSize{0u};
+
+    DEF_NEWDEL(PortPlayback)
+};
+
+PortPlayback::~PortPlayback()
+{
+    PaError err{mStream ? Pa_CloseStream(mStream) : paNoError};
+    if(err != paNoError)
+        ERR("Error closing stream: %s\n", Pa_GetErrorText(err));
+    mStream = nullptr;
+}
+
+
+int PortPlayback::writeCallback(const void*, void *outputBuffer, unsigned long framesPerBuffer,
+    const PaStreamCallbackTimeInfo*, const PaStreamCallbackFlags) noexcept
+{
+    mDevice->renderSamples(outputBuffer, static_cast<uint>(framesPerBuffer),
+        static_cast<uint>(mParams.channelCount));
+    return 0;
+}
+
+
+void PortPlayback::open(const char *name)
+{
+    if(!name)
+        name = pa_device;
+    else if(strcmp(name, pa_device) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    PaStreamParameters params{};
+    auto devidopt = ConfigValueInt(nullptr, "port", "device");
+    if(devidopt && *devidopt >= 0) params.device = *devidopt;
+    else params.device = Pa_GetDefaultOutputDevice();
+    params.suggestedLatency = mDevice->BufferSize / static_cast<double>(mDevice->Frequency);
+    params.hostApiSpecificStreamInfo = nullptr;
+
+    params.channelCount = ((mDevice->FmtChans == DevFmtMono) ? 1 : 2);
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        params.sampleFormat = paInt8;
+        break;
+    case DevFmtUByte:
+        params.sampleFormat = paUInt8;
+        break;
+    case DevFmtUShort:
+        /* fall-through */
+    case DevFmtShort:
+        params.sampleFormat = paInt16;
+        break;
+    case DevFmtUInt:
+        /* fall-through */
+    case DevFmtInt:
+        params.sampleFormat = paInt32;
+        break;
+    case DevFmtFloat:
+        params.sampleFormat = paFloat32;
+        break;
+    }
+
+retry_open:
+    PaStream *stream{};
+    PaError err{Pa_OpenStream(&stream, nullptr, &params, mDevice->Frequency, mDevice->UpdateSize,
+        paNoFlag, &PortPlayback::writeCallbackC, this)};
+    if(err != paNoError)
+    {
+        if(params.sampleFormat == paFloat32)
+        {
+            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;
+}
+
+bool PortPlayback::reset()
+{
+    const PaStreamInfo *streamInfo{Pa_GetStreamInfo(mStream)};
+    mDevice->Frequency = static_cast<uint>(streamInfo->sampleRate);
+    mDevice->UpdateSize = mUpdateSize;
+
+    if(mParams.sampleFormat == paInt8)
+        mDevice->FmtType = DevFmtByte;
+    else if(mParams.sampleFormat == paUInt8)
+        mDevice->FmtType = DevFmtUByte;
+    else if(mParams.sampleFormat == paInt16)
+        mDevice->FmtType = DevFmtShort;
+    else if(mParams.sampleFormat == paInt32)
+        mDevice->FmtType = DevFmtInt;
+    else if(mParams.sampleFormat == paFloat32)
+        mDevice->FmtType = DevFmtFloat;
+    else
+    {
+        ERR("Unexpected sample format: 0x%lx\n", mParams.sampleFormat);
+        return false;
+    }
+
+    if(mParams.channelCount >= 2)
+        mDevice->FmtChans = DevFmtStereo;
+    else if(mParams.channelCount == 1)
+        mDevice->FmtChans = DevFmtMono;
+    else
+    {
+        ERR("Unexpected channel count: %u\n", mParams.channelCount);
+        return false;
+    }
+    setDefaultChannelOrder();
+
+    return true;
+}
+
+void PortPlayback::start()
+{
+    const PaError err{Pa_StartStream(mStream)};
+    if(err == paNoError)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to start playback: %s",
+            Pa_GetErrorText(err)};
+}
+
+void PortPlayback::stop()
+{
+    PaError err{Pa_StopStream(mStream)};
+    if(err != paNoError)
+        ERR("Error stopping stream: %s\n", Pa_GetErrorText(err));
+}
+
+
+struct PortCapture final : public BackendBase {
+    PortCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~PortCapture() override;
+
+    int readCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
+        const PaStreamCallbackTimeInfo *timeInfo, const PaStreamCallbackFlags statusFlags) noexcept;
+    static int readCallbackC(const void *inputBuffer, void *outputBuffer,
+        unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo,
+        const PaStreamCallbackFlags statusFlags, void *userData) noexcept
+    {
+        return static_cast<PortCapture*>(userData)->readCallback(inputBuffer, outputBuffer,
+            framesPerBuffer, timeInfo, statusFlags);
+    }
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    PaStream *mStream{nullptr};
+    PaStreamParameters mParams;
+
+    RingBufferPtr mRing{nullptr};
+
+    DEF_NEWDEL(PortCapture)
+};
+
+PortCapture::~PortCapture()
+{
+    PaError err{mStream ? Pa_CloseStream(mStream) : paNoError};
+    if(err != paNoError)
+        ERR("Error closing stream: %s\n", Pa_GetErrorText(err));
+    mStream = nullptr;
+}
+
+
+int PortCapture::readCallback(const void *inputBuffer, void*, unsigned long framesPerBuffer,
+    const PaStreamCallbackTimeInfo*, const PaStreamCallbackFlags) noexcept
+{
+    mRing->write(inputBuffer, framesPerBuffer);
+    return 0;
+}
+
+
+void PortCapture::open(const char *name)
+{
+    if(!name)
+        name = pa_device;
+    else if(strcmp(name, pa_device) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    uint samples{mDevice->BufferSize};
+    samples = maxu(samples, 100 * mDevice->Frequency / 1000);
+    uint frame_size{mDevice->frameSizeFromFmt()};
+
+    mRing = RingBuffer::Create(samples, frame_size, false);
+
+    auto devidopt = ConfigValueInt(nullptr, "port", "capture");
+    if(devidopt && *devidopt >= 0) mParams.device = *devidopt;
+    else mParams.device = Pa_GetDefaultOutputDevice();
+    mParams.suggestedLatency = 0.0f;
+    mParams.hostApiSpecificStreamInfo = nullptr;
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        mParams.sampleFormat = paInt8;
+        break;
+    case DevFmtUByte:
+        mParams.sampleFormat = paUInt8;
+        break;
+    case DevFmtShort:
+        mParams.sampleFormat = paInt16;
+        break;
+    case DevFmtInt:
+        mParams.sampleFormat = paInt32;
+        break;
+    case DevFmtFloat:
+        mParams.sampleFormat = paFloat32;
+        break;
+    case DevFmtUInt:
+    case DevFmtUShort:
+        throw al::backend_exception{al::backend_error::DeviceError, "%s samples not supported",
+            DevFmtTypeString(mDevice->FmtType)};
+    }
+    mParams.channelCount = static_cast<int>(mDevice->channelsFromFmt());
+
+    PaError err{Pa_OpenStream(&mStream, &mParams, nullptr, mDevice->Frequency,
+        paFramesPerBufferUnspecified, paNoFlag, &PortCapture::readCallbackC, this)};
+    if(err != paNoError)
+        throw al::backend_exception{al::backend_error::NoDevice, "Failed to open stream: %s",
+            Pa_GetErrorText(err)};
+
+    mDevice->DeviceName = name;
+}
+
+
+void PortCapture::start()
+{
+    const PaError err{Pa_StartStream(mStream)};
+    if(err != paNoError)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start recording: %s", Pa_GetErrorText(err)};
+}
+
+void PortCapture::stop()
+{
+    PaError err{Pa_StopStream(mStream)};
+    if(err != paNoError)
+        ERR("Error stopping stream: %s\n", Pa_GetErrorText(err));
+}
+
+
+uint PortCapture::availableSamples()
+{ return static_cast<uint>(mRing->readSpace()); }
+
+void PortCapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, samples); }
+
+} // namespace
+
+
+bool PortBackendFactory::init()
+{
+    PaError err;
+
+#ifdef HAVE_DYNLOAD
+    if(!pa_handle)
+    {
+#ifdef _WIN32
+# define PALIB "portaudio.dll"
+#elif defined(__APPLE__) && defined(__MACH__)
+# define PALIB "libportaudio.2.dylib"
+#elif defined(__OpenBSD__)
+# define PALIB "libportaudio.so"
+#else
+# define PALIB "libportaudio.so.2"
+#endif
+
+        pa_handle = LoadLib(PALIB);
+        if(!pa_handle)
+            return false;
+
+#define LOAD_FUNC(f) do {                                                     \
+    p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(pa_handle, #f));        \
+    if(p##f == nullptr)                                                       \
+    {                                                                         \
+        CloseLib(pa_handle);                                                  \
+        pa_handle = nullptr;                                                  \
+        return false;                                                         \
+    }                                                                         \
+} while(0)
+        LOAD_FUNC(Pa_Initialize);
+        LOAD_FUNC(Pa_Terminate);
+        LOAD_FUNC(Pa_GetErrorText);
+        LOAD_FUNC(Pa_StartStream);
+        LOAD_FUNC(Pa_StopStream);
+        LOAD_FUNC(Pa_OpenStream);
+        LOAD_FUNC(Pa_CloseStream);
+        LOAD_FUNC(Pa_GetDefaultOutputDevice);
+        LOAD_FUNC(Pa_GetDefaultInputDevice);
+        LOAD_FUNC(Pa_GetStreamInfo);
+#undef LOAD_FUNC
+
+        if((err=Pa_Initialize()) != paNoError)
+        {
+            ERR("Pa_Initialize() returned an error: %s\n", Pa_GetErrorText(err));
+            CloseLib(pa_handle);
+            pa_handle = nullptr;
+            return false;
+        }
+    }
+#else
+    if((err=Pa_Initialize()) != paNoError)
+    {
+        ERR("Pa_Initialize() returned an error: %s\n", Pa_GetErrorText(err));
+        return false;
+    }
+#endif
+    return true;
+}
+
+bool PortBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+std::string PortBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    switch(type)
+    {
+    case BackendType::Playback:
+    case BackendType::Capture:
+        /* Includes null char. */
+        outnames.append(pa_device, sizeof(pa_device));
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr PortBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new PortPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new PortCapture{device}};
+    return nullptr;
+}
+
+BackendFactory &PortBackendFactory::getFactory()
+{
+    static PortBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/portaudio.h b/alc/backends/portaudio.h
new file mode 100644 (file)
index 0000000..c35ccff
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_PORTAUDIO_H
+#define BACKENDS_PORTAUDIO_H
+
+#include "base.h"
+
+struct PortBackendFactory 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_PORTAUDIO_H */
diff --git a/alc/backends/pulseaudio.cpp b/alc/backends/pulseaudio.cpp
new file mode 100644 (file)
index 0000000..4b0e316
--- /dev/null
@@ -0,0 +1,1469 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2009 by Konstantinos Natsakis <konstantinos.natsakis@gmail.com>
+ * Copyright (C) 2010 by Chris Robinson <chris.kcat@gmail.com>
+ * 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 "pulseaudio.h"
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <bitset>
+#include <chrono>
+#include <cstring>
+#include <limits>
+#include <mutex>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string>
+#include <sys/types.h>
+#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/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_context_new);                                                    \
+    MAGIC(pa_context_unref);                                                  \
+    MAGIC(pa_context_get_state);                                              \
+    MAGIC(pa_context_disconnect);                                             \
+    MAGIC(pa_context_set_state_callback);                                     \
+    MAGIC(pa_context_errno);                                                  \
+    MAGIC(pa_context_connect);                                                \
+    MAGIC(pa_context_get_server_info);                                        \
+    MAGIC(pa_context_get_sink_info_by_name);                                  \
+    MAGIC(pa_context_get_sink_info_list);                                     \
+    MAGIC(pa_context_get_source_info_by_name);                                \
+    MAGIC(pa_context_get_source_info_list);                                   \
+    MAGIC(pa_stream_new);                                                     \
+    MAGIC(pa_stream_unref);                                                   \
+    MAGIC(pa_stream_drop);                                                    \
+    MAGIC(pa_stream_get_state);                                               \
+    MAGIC(pa_stream_peek);                                                    \
+    MAGIC(pa_stream_write);                                                   \
+    MAGIC(pa_stream_connect_record);                                          \
+    MAGIC(pa_stream_connect_playback);                                        \
+    MAGIC(pa_stream_readable_size);                                           \
+    MAGIC(pa_stream_writable_size);                                           \
+    MAGIC(pa_stream_is_corked);                                               \
+    MAGIC(pa_stream_cork);                                                    \
+    MAGIC(pa_stream_is_suspended);                                            \
+    MAGIC(pa_stream_get_device_name);                                         \
+    MAGIC(pa_stream_get_latency);                                             \
+    MAGIC(pa_stream_set_write_callback);                                      \
+    MAGIC(pa_stream_set_buffer_attr);                                         \
+    MAGIC(pa_stream_get_buffer_attr);                                         \
+    MAGIC(pa_stream_get_sample_spec);                                         \
+    MAGIC(pa_stream_get_time);                                                \
+    MAGIC(pa_stream_set_read_callback);                                       \
+    MAGIC(pa_stream_set_state_callback);                                      \
+    MAGIC(pa_stream_set_moved_callback);                                      \
+    MAGIC(pa_stream_set_underflow_callback);                                  \
+    MAGIC(pa_stream_new_with_proplist);                                       \
+    MAGIC(pa_stream_disconnect);                                              \
+    MAGIC(pa_stream_set_buffer_attr_callback);                                \
+    MAGIC(pa_stream_begin_write);                                             \
+    MAGIC(pa_threaded_mainloop_free);                                         \
+    MAGIC(pa_threaded_mainloop_get_api);                                      \
+    MAGIC(pa_threaded_mainloop_lock);                                         \
+    MAGIC(pa_threaded_mainloop_new);                                          \
+    MAGIC(pa_threaded_mainloop_signal);                                       \
+    MAGIC(pa_threaded_mainloop_start);                                        \
+    MAGIC(pa_threaded_mainloop_stop);                                         \
+    MAGIC(pa_threaded_mainloop_unlock);                                       \
+    MAGIC(pa_threaded_mainloop_wait);                                         \
+    MAGIC(pa_channel_map_init_auto);                                          \
+    MAGIC(pa_channel_map_parse);                                              \
+    MAGIC(pa_channel_map_snprint);                                            \
+    MAGIC(pa_channel_map_equal);                                              \
+    MAGIC(pa_channel_map_superset);                                           \
+    MAGIC(pa_channel_position_to_string);                                     \
+    MAGIC(pa_operation_get_state);                                            \
+    MAGIC(pa_operation_unref);                                                \
+    MAGIC(pa_sample_spec_valid);                                              \
+    MAGIC(pa_frame_size);                                                     \
+    MAGIC(pa_strerror);                                                       \
+    MAGIC(pa_path_get_filename);                                              \
+    MAGIC(pa_get_binary_name);                                                \
+    MAGIC(pa_xmalloc);                                                        \
+    MAGIC(pa_xfree);
+
+void *pulse_handle;
+#define MAKE_FUNC(x) decltype(x) * p##x
+PULSE_FUNCS(MAKE_FUNC)
+#undef MAKE_FUNC
+
+#ifndef IN_IDE_PARSER
+#define pa_context_new ppa_context_new
+#define pa_context_unref ppa_context_unref
+#define pa_context_get_state ppa_context_get_state
+#define pa_context_disconnect ppa_context_disconnect
+#define pa_context_set_state_callback ppa_context_set_state_callback
+#define pa_context_errno ppa_context_errno
+#define pa_context_connect ppa_context_connect
+#define pa_context_get_server_info ppa_context_get_server_info
+#define pa_context_get_sink_info_by_name ppa_context_get_sink_info_by_name
+#define pa_context_get_sink_info_list ppa_context_get_sink_info_list
+#define pa_context_get_source_info_by_name ppa_context_get_source_info_by_name
+#define pa_context_get_source_info_list ppa_context_get_source_info_list
+#define pa_stream_new ppa_stream_new
+#define pa_stream_unref ppa_stream_unref
+#define pa_stream_disconnect ppa_stream_disconnect
+#define pa_stream_drop ppa_stream_drop
+#define pa_stream_set_write_callback ppa_stream_set_write_callback
+#define pa_stream_set_buffer_attr ppa_stream_set_buffer_attr
+#define pa_stream_get_buffer_attr ppa_stream_get_buffer_attr
+#define pa_stream_get_sample_spec ppa_stream_get_sample_spec
+#define pa_stream_get_time ppa_stream_get_time
+#define pa_stream_set_read_callback ppa_stream_set_read_callback
+#define pa_stream_set_state_callback ppa_stream_set_state_callback
+#define pa_stream_set_moved_callback ppa_stream_set_moved_callback
+#define pa_stream_set_underflow_callback ppa_stream_set_underflow_callback
+#define pa_stream_connect_record ppa_stream_connect_record
+#define pa_stream_connect_playback ppa_stream_connect_playback
+#define pa_stream_readable_size ppa_stream_readable_size
+#define pa_stream_writable_size ppa_stream_writable_size
+#define pa_stream_is_corked ppa_stream_is_corked
+#define pa_stream_cork ppa_stream_cork
+#define pa_stream_is_suspended ppa_stream_is_suspended
+#define pa_stream_get_device_name ppa_stream_get_device_name
+#define pa_stream_get_latency ppa_stream_get_latency
+#define pa_stream_set_buffer_attr_callback ppa_stream_set_buffer_attr_callback
+#define pa_stream_begin_write ppa_stream_begin_write
+#define pa_threaded_mainloop_free ppa_threaded_mainloop_free
+#define pa_threaded_mainloop_get_api ppa_threaded_mainloop_get_api
+#define pa_threaded_mainloop_lock ppa_threaded_mainloop_lock
+#define pa_threaded_mainloop_new ppa_threaded_mainloop_new
+#define pa_threaded_mainloop_signal ppa_threaded_mainloop_signal
+#define pa_threaded_mainloop_start ppa_threaded_mainloop_start
+#define pa_threaded_mainloop_stop ppa_threaded_mainloop_stop
+#define pa_threaded_mainloop_unlock ppa_threaded_mainloop_unlock
+#define pa_threaded_mainloop_wait ppa_threaded_mainloop_wait
+#define pa_channel_map_init_auto ppa_channel_map_init_auto
+#define pa_channel_map_parse ppa_channel_map_parse
+#define pa_channel_map_snprint ppa_channel_map_snprint
+#define pa_channel_map_equal ppa_channel_map_equal
+#define pa_channel_map_superset ppa_channel_map_superset
+#define pa_channel_position_to_string ppa_channel_position_to_string
+#define pa_operation_get_state ppa_operation_get_state
+#define pa_operation_unref ppa_operation_unref
+#define pa_sample_spec_valid ppa_sample_spec_valid
+#define pa_frame_size ppa_frame_size
+#define pa_strerror ppa_strerror
+#define pa_stream_get_state ppa_stream_get_state
+#define pa_stream_peek ppa_stream_peek
+#define pa_stream_write ppa_stream_write
+#define pa_xfree ppa_xfree
+#define pa_path_get_filename ppa_path_get_filename
+#define pa_get_binary_name ppa_get_binary_name
+#define pa_xmalloc ppa_xmalloc
+#endif /* IN_IDE_PARSER */
+
+#endif
+
+
+constexpr pa_channel_map MonoChanMap{
+    1, {PA_CHANNEL_POSITION_MONO}
+}, StereoChanMap{
+    2, {PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT}
+}, QuadChanMap{
+    4, {
+        PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT,
+        PA_CHANNEL_POSITION_REAR_LEFT, PA_CHANNEL_POSITION_REAR_RIGHT
+    }
+}, X51ChanMap{
+    6, {
+        PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT,
+        PA_CHANNEL_POSITION_FRONT_CENTER, PA_CHANNEL_POSITION_LFE,
+        PA_CHANNEL_POSITION_SIDE_LEFT, PA_CHANNEL_POSITION_SIDE_RIGHT
+    }
+}, X51RearChanMap{
+    6, {
+        PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT,
+        PA_CHANNEL_POSITION_FRONT_CENTER, PA_CHANNEL_POSITION_LFE,
+        PA_CHANNEL_POSITION_REAR_LEFT, PA_CHANNEL_POSITION_REAR_RIGHT
+    }
+}, X61ChanMap{
+    7, {
+        PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT,
+        PA_CHANNEL_POSITION_FRONT_CENTER, PA_CHANNEL_POSITION_LFE,
+        PA_CHANNEL_POSITION_REAR_CENTER,
+        PA_CHANNEL_POSITION_SIDE_LEFT, PA_CHANNEL_POSITION_SIDE_RIGHT
+    }
+}, X71ChanMap{
+    8, {
+        PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT,
+        PA_CHANNEL_POSITION_FRONT_CENTER, PA_CHANNEL_POSITION_LFE,
+        PA_CHANNEL_POSITION_REAR_LEFT, PA_CHANNEL_POSITION_REAR_RIGHT,
+        PA_CHANNEL_POSITION_SIDE_LEFT, PA_CHANNEL_POSITION_SIDE_RIGHT
+    }
+}, X714ChanMap{
+    12, {
+        PA_CHANNEL_POSITION_FRONT_LEFT, PA_CHANNEL_POSITION_FRONT_RIGHT,
+        PA_CHANNEL_POSITION_FRONT_CENTER, PA_CHANNEL_POSITION_LFE,
+        PA_CHANNEL_POSITION_REAR_LEFT, PA_CHANNEL_POSITION_REAR_RIGHT,
+        PA_CHANNEL_POSITION_SIDE_LEFT, PA_CHANNEL_POSITION_SIDE_RIGHT,
+        PA_CHANNEL_POSITION_TOP_FRONT_LEFT, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT,
+        PA_CHANNEL_POSITION_TOP_REAR_LEFT, PA_CHANNEL_POSITION_TOP_REAR_RIGHT
+    }
+};
+
+
+/* *grumble* Don't use enums for bitflags. */
+constexpr pa_stream_flags_t operator|(pa_stream_flags_t lhs, pa_stream_flags_t rhs)
+{ return pa_stream_flags_t(lhs | al::to_underlying(rhs)); }
+constexpr pa_stream_flags_t& operator|=(pa_stream_flags_t &lhs, pa_stream_flags_t rhs)
+{
+    lhs = lhs | rhs;
+    return lhs;
+}
+constexpr pa_stream_flags_t operator~(pa_stream_flags_t flag)
+{ return pa_stream_flags_t(~al::to_underlying(flag)); }
+constexpr pa_stream_flags_t& operator&=(pa_stream_flags_t &lhs, pa_stream_flags_t rhs)
+{
+    lhs = pa_stream_flags_t(al::to_underlying(lhs) & rhs);
+    return lhs;
+}
+
+constexpr pa_context_flags_t operator|(pa_context_flags_t lhs, pa_context_flags_t rhs)
+{ return pa_context_flags_t(lhs | al::to_underlying(rhs)); }
+constexpr pa_context_flags_t& operator|=(pa_context_flags_t &lhs, pa_context_flags_t rhs)
+{
+    lhs = lhs | rhs;
+    return lhs;
+}
+
+
+struct DevMap {
+    std::string name;
+    std::string device_name;
+};
+
+bool checkName(const al::span<const DevMap> list, const std::string &name)
+{
+    auto match_name = [&name](const DevMap &entry) -> bool { return entry.name == name; };
+    return std::find_if(list.cbegin(), list.cend(), match_name) != list.cend();
+}
+
+al::vector<DevMap> PlaybackDevices;
+al::vector<DevMap> CaptureDevices;
+
+
+/* Global flags and properties */
+pa_context_flags_t pulse_ctx_flags;
+
+class PulseMainloop {
+    pa_threaded_mainloop *mLoop{};
+
+public:
+    PulseMainloop() = default;
+    PulseMainloop(const PulseMainloop&) = delete;
+    PulseMainloop(PulseMainloop&& rhs) noexcept : mLoop{rhs.mLoop} { rhs.mLoop = nullptr; }
+    explicit PulseMainloop(pa_threaded_mainloop *loop) noexcept : mLoop{loop} { }
+    ~PulseMainloop() { if(mLoop) pa_threaded_mainloop_free(mLoop); }
+
+    PulseMainloop& operator=(const PulseMainloop&) = delete;
+    PulseMainloop& operator=(PulseMainloop&& rhs) noexcept
+    { std::swap(mLoop, rhs.mLoop); return *this; }
+    PulseMainloop& operator=(std::nullptr_t) noexcept
+    {
+        if(mLoop)
+            pa_threaded_mainloop_free(mLoop);
+        mLoop = nullptr;
+        return *this;
+    }
+
+    explicit operator bool() const noexcept { return mLoop != nullptr; }
+
+    auto start() const { return pa_threaded_mainloop_start(mLoop); }
+    auto stop() const { return pa_threaded_mainloop_stop(mLoop); }
+
+    auto getApi() const { return pa_threaded_mainloop_get_api(mLoop); }
+
+    auto lock() const { return pa_threaded_mainloop_lock(mLoop); }
+    auto unlock() const { return pa_threaded_mainloop_unlock(mLoop); }
+
+    auto signal(bool wait=false) const { return pa_threaded_mainloop_signal(mLoop, wait); }
+
+    static auto Create() { return PulseMainloop{pa_threaded_mainloop_new()}; }
+
+
+    void streamSuccessCallback(pa_stream*, int) noexcept { signal(); }
+    static void streamSuccessCallbackC(pa_stream *stream, int success, void *pdata) noexcept
+    { static_cast<PulseMainloop*>(pdata)->streamSuccessCallback(stream, success); }
+
+    void close(pa_context *context, pa_stream *stream=nullptr);
+
+
+    void deviceSinkCallback(pa_context*, const pa_sink_info *info, int eol) noexcept
+    {
+        if(eol)
+        {
+            signal();
+            return;
+        }
+
+        /* Skip this device is if it's already in the list. */
+        auto match_devname = [info](const DevMap &entry) -> bool
+        { return entry.device_name == info->name; };
+        if(std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(), match_devname) != PlaybackDevices.cend())
+            return;
+
+        /* Make sure the display name (description) is unique. Append a number
+         * counter as needed.
+         */
+        int count{1};
+        std::string newname{info->description};
+        while(checkName(PlaybackDevices, newname))
+        {
+            newname = info->description;
+            newname += " #";
+            newname += std::to_string(++count);
+        }
+        PlaybackDevices.emplace_back(DevMap{std::move(newname), info->name});
+        DevMap &newentry = PlaybackDevices.back();
+
+        TRACE("Got device \"%s\", \"%s\"\n", newentry.name.c_str(), newentry.device_name.c_str());
+    }
+
+    void deviceSourceCallback(pa_context*, const pa_source_info *info, int eol) noexcept
+    {
+        if(eol)
+        {
+            signal();
+            return;
+        }
+
+        /* Skip this device is if it's already in the list. */
+        auto match_devname = [info](const DevMap &entry) -> bool
+        { return entry.device_name == info->name; };
+        if(std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(), match_devname) != CaptureDevices.cend())
+            return;
+
+        /* Make sure the display name (description) is unique. Append a number
+         * counter as needed.
+         */
+        int count{1};
+        std::string newname{info->description};
+        while(checkName(CaptureDevices, newname))
+        {
+            newname = info->description;
+            newname += " #";
+            newname += std::to_string(++count);
+        }
+        CaptureDevices.emplace_back(DevMap{std::move(newname), info->name});
+        DevMap &newentry = CaptureDevices.back();
+
+        TRACE("Got device \"%s\", \"%s\"\n", newentry.name.c_str(), newentry.device_name.c_str());
+    }
+
+    void probePlaybackDevices();
+    void probeCaptureDevices();
+
+    friend struct MainloopUniqueLock;
+};
+struct MainloopUniqueLock : public std::unique_lock<PulseMainloop> {
+    using std::unique_lock<PulseMainloop>::unique_lock;
+    MainloopUniqueLock& operator=(MainloopUniqueLock&&) = default;
+
+    auto wait() const -> void
+    { pa_threaded_mainloop_wait(mutex()->mLoop); }
+
+    template<typename Predicate>
+    auto wait(Predicate done_waiting) const -> void
+    { while(!done_waiting()) wait(); }
+
+    void waitForOperation(pa_operation *op)
+    {
+        if(op)
+        {
+            wait([op]{ return pa_operation_get_state(op) != PA_OPERATION_RUNNING; });
+            pa_operation_unref(op);
+        }
+    }
+
+
+    void contextStateCallback(pa_context *context) noexcept
+    {
+        pa_context_state_t state{pa_context_get_state(context)};
+        if(state == PA_CONTEXT_READY || !PA_CONTEXT_IS_GOOD(state))
+            mutex()->signal();
+    }
+
+    void streamStateCallback(pa_stream *stream) noexcept
+    {
+        pa_stream_state_t state{pa_stream_get_state(stream)};
+        if(state == PA_STREAM_READY || !PA_STREAM_IS_GOOD(state))
+            mutex()->signal();
+    }
+
+    pa_context *connectContext();
+    pa_stream *connectStream(const char *device_name, pa_context *context, pa_stream_flags_t flags,
+        pa_buffer_attr *attr, pa_sample_spec *spec, pa_channel_map *chanmap, BackendType type);
+};
+using MainloopLockGuard = std::lock_guard<PulseMainloop>;
+
+
+pa_context *MainloopUniqueLock::connectContext()
+{
+    pa_context *context{pa_context_new(mutex()->getApi(), nullptr)};
+    if(!context) throw al::backend_exception{al::backend_error::OutOfMemory,
+        "pa_context_new() failed"};
+
+    pa_context_set_state_callback(context, [](pa_context *ctx, void *pdata) noexcept
+    { return static_cast<MainloopUniqueLock*>(pdata)->contextStateCallback(ctx); }, this);
+
+    int err;
+    if((err=pa_context_connect(context, nullptr, pulse_ctx_flags, nullptr)) >= 0)
+    {
+        pa_context_state_t state;
+        while((state=pa_context_get_state(context)) != PA_CONTEXT_READY)
+        {
+            if(!PA_CONTEXT_IS_GOOD(state))
+            {
+                err = pa_context_errno(context);
+                if(err > 0)  err = -err;
+                break;
+            }
+
+            wait();
+        }
+    }
+    pa_context_set_state_callback(context, nullptr, nullptr);
+
+    if(err < 0)
+    {
+        pa_context_unref(context);
+        throw al::backend_exception{al::backend_error::DeviceError, "Context did not connect (%s)",
+            pa_strerror(err)};
+    }
+
+    return context;
+}
+
+pa_stream *MainloopUniqueLock::connectStream(const char *device_name, pa_context *context,
+    pa_stream_flags_t flags, pa_buffer_attr *attr, pa_sample_spec *spec, pa_channel_map *chanmap,
+    BackendType type)
+{
+    const char *stream_id{(type==BackendType::Playback) ? "Playback Stream" : "Capture Stream"};
+    pa_stream *stream{pa_stream_new(context, stream_id, spec, chanmap)};
+    if(!stream)
+        throw al::backend_exception{al::backend_error::OutOfMemory, "pa_stream_new() failed (%s)",
+            pa_strerror(pa_context_errno(context))};
+
+    pa_stream_set_state_callback(stream, [](pa_stream *strm, void *pdata) noexcept
+    { return static_cast<MainloopUniqueLock*>(pdata)->streamStateCallback(strm); }, this);
+
+    int err{(type==BackendType::Playback) ?
+        pa_stream_connect_playback(stream, device_name, attr, flags, nullptr, nullptr) :
+        pa_stream_connect_record(stream, device_name, attr, flags)};
+    if(err < 0)
+    {
+        pa_stream_unref(stream);
+        throw al::backend_exception{al::backend_error::DeviceError, "%s did not connect (%s)",
+            stream_id, pa_strerror(err)};
+    }
+
+    pa_stream_state_t state;
+    while((state=pa_stream_get_state(stream)) != PA_STREAM_READY)
+    {
+        if(!PA_STREAM_IS_GOOD(state))
+        {
+            err = pa_context_errno(context);
+            pa_stream_unref(stream);
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "%s did not get ready (%s)", stream_id, pa_strerror(err)};
+        }
+
+        wait();
+    }
+    pa_stream_set_state_callback(stream, nullptr, nullptr);
+
+    return stream;
+}
+
+void PulseMainloop::close(pa_context *context, pa_stream *stream)
+{
+    MainloopUniqueLock _{*this};
+    if(stream)
+    {
+        pa_stream_set_state_callback(stream, nullptr, nullptr);
+        pa_stream_set_moved_callback(stream, nullptr, nullptr);
+        pa_stream_set_write_callback(stream, nullptr, nullptr);
+        pa_stream_set_buffer_attr_callback(stream, nullptr, nullptr);
+        pa_stream_disconnect(stream);
+        pa_stream_unref(stream);
+    }
+
+    pa_context_disconnect(context);
+    pa_context_unref(context);
+}
+
+
+void PulseMainloop::probePlaybackDevices()
+{
+    pa_context *context{};
+
+    PlaybackDevices.clear();
+    try {
+        MainloopUniqueLock plock{*this};
+        auto sink_callback = [](pa_context *ctx, const pa_sink_info *info, int eol, void *pdata) noexcept
+        { return static_cast<PulseMainloop*>(pdata)->deviceSinkCallback(ctx, info, eol); };
+
+        context = plock.connectContext();
+        pa_operation *op{pa_context_get_sink_info_by_name(context, nullptr, sink_callback, this)};
+        plock.waitForOperation(op);
+
+        op = pa_context_get_sink_info_list(context, sink_callback, this);
+        plock.waitForOperation(op);
+
+        pa_context_disconnect(context);
+        pa_context_unref(context);
+        context = nullptr;
+    }
+    catch(std::exception &e) {
+        ERR("Error enumerating devices: %s\n", e.what());
+        if(context) close(context);
+    }
+}
+
+void PulseMainloop::probeCaptureDevices()
+{
+    pa_context *context{};
+
+    CaptureDevices.clear();
+    try {
+        MainloopUniqueLock plock{*this};
+        auto src_callback = [](pa_context *ctx, const pa_source_info *info, int eol, void *pdata) noexcept
+        { return static_cast<PulseMainloop*>(pdata)->deviceSourceCallback(ctx, info, eol); };
+
+        context = plock.connectContext();
+        pa_operation *op{pa_context_get_source_info_by_name(context, nullptr, src_callback, this)};
+        plock.waitForOperation(op);
+
+        op = pa_context_get_source_info_list(context, src_callback, this);
+        plock.waitForOperation(op);
+
+        pa_context_disconnect(context);
+        pa_context_unref(context);
+        context = nullptr;
+    }
+    catch(std::exception &e) {
+        ERR("Error enumerating devices: %s\n", e.what());
+        if(context) close(context);
+    }
+}
+
+
+/* Used for initial connection test and enumeration. */
+PulseMainloop gGlobalMainloop;
+
+
+struct PulsePlayback final : public BackendBase {
+    PulsePlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~PulsePlayback() override;
+
+    void bufferAttrCallback(pa_stream *stream) noexcept;
+    void streamStateCallback(pa_stream *stream) noexcept;
+    void streamWriteCallback(pa_stream *stream, size_t nbytes) noexcept;
+    void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int eol) noexcept;
+    void sinkNameCallback(pa_context *context, const pa_sink_info *info, int eol) noexcept;
+    void streamMovedCallback(pa_stream *stream) noexcept;
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+    ClockLatency getClockLatency() override;
+
+    PulseMainloop mMainloop;
+
+    al::optional<std::string> mDeviceName{al::nullopt};
+
+    bool mIs51Rear{false};
+    pa_buffer_attr mAttr;
+    pa_sample_spec mSpec;
+
+    pa_stream *mStream{nullptr};
+    pa_context *mContext{nullptr};
+
+    uint mFrameSize{0u};
+
+    DEF_NEWDEL(PulsePlayback)
+};
+
+PulsePlayback::~PulsePlayback()
+{
+    if(!mContext)
+        return;
+
+    mMainloop.close(mContext, mStream);
+    mContext = nullptr;
+    mStream = nullptr;
+}
+
+
+void PulsePlayback::bufferAttrCallback(pa_stream *stream) noexcept
+{
+    /* FIXME: Update the device's UpdateSize (and/or BufferSize) using the new
+     * buffer attributes? Changing UpdateSize will change the ALC_REFRESH
+     * property, which probably shouldn't change between device resets. But
+     * leaving it alone means ALC_REFRESH will be off.
+     */
+    mAttr = *(pa_stream_get_buffer_attr(stream));
+    TRACE("minreq=%d, tlength=%d, prebuf=%d\n", mAttr.minreq, mAttr.tlength, mAttr.prebuf);
+}
+
+void PulsePlayback::streamStateCallback(pa_stream *stream) noexcept
+{
+    if(pa_stream_get_state(stream) == PA_STREAM_FAILED)
+    {
+        ERR("Received stream failure!\n");
+        mDevice->handleDisconnect("Playback stream failure");
+    }
+    mMainloop.signal();
+}
+
+void PulsePlayback::streamWriteCallback(pa_stream *stream, size_t nbytes) noexcept
+{
+    do {
+        pa_free_cb_t free_func{nullptr};
+        auto buflen = static_cast<size_t>(-1);
+        void *buf{};
+        if(pa_stream_begin_write(stream, &buf, &buflen) || !buf) UNLIKELY
+        {
+            buflen = nbytes;
+            buf = pa_xmalloc(buflen);
+            free_func = pa_xfree;
+        }
+        else
+            buflen = minz(buflen, nbytes);
+        nbytes -= buflen;
+
+        mDevice->renderSamples(buf, static_cast<uint>(buflen/mFrameSize), mSpec.channels);
+
+        int ret{pa_stream_write(stream, buf, buflen, free_func, 0, PA_SEEK_RELATIVE)};
+        if(ret != PA_OK) UNLIKELY
+            ERR("Failed to write to stream: %d, %s\n", ret, pa_strerror(ret));
+    } while(nbytes > 0);
+}
+
+void PulsePlayback::sinkInfoCallback(pa_context*, const pa_sink_info *info, int eol) noexcept
+{
+    struct ChannelMap {
+        DevFmtChannels fmt;
+        pa_channel_map map;
+        bool is_51rear;
+    };
+    static constexpr std::array<ChannelMap,8> chanmaps{{
+        { DevFmtX714, X714ChanMap, false },
+        { DevFmtX71, X71ChanMap, false },
+        { DevFmtX61, X61ChanMap, false },
+        { DevFmtX51, X51ChanMap, false },
+        { DevFmtX51, X51RearChanMap, true },
+        { DevFmtQuad, QuadChanMap, false },
+        { DevFmtStereo, StereoChanMap, false },
+        { DevFmtMono, MonoChanMap, false }
+    }};
+
+    if(eol)
+    {
+        mMainloop.signal();
+        return;
+    }
+
+    auto chaniter = std::find_if(chanmaps.cbegin(), chanmaps.cend(),
+        [info](const ChannelMap &chanmap) -> bool
+        { return pa_channel_map_superset(&info->channel_map, &chanmap.map); }
+    );
+    if(chaniter != chanmaps.cend())
+    {
+        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);
+    }
+
+    if(info->active_port)
+        TRACE("Active port: %s (%s)\n", info->active_port->name, info->active_port->description);
+    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
+{
+    if(eol)
+    {
+        mMainloop.signal();
+        return;
+    }
+    mDevice->DeviceName = info->description;
+}
+
+void PulsePlayback::streamMovedCallback(pa_stream *stream) noexcept
+{
+    mDeviceName = pa_stream_get_device_name(stream);
+    TRACE("Stream moved to %s\n", mDeviceName->c_str());
+}
+
+
+void PulsePlayback::open(const char *name)
+{
+    mMainloop = PulseMainloop::Create();
+    mMainloop.start();
+
+    const char *pulse_name{nullptr};
+    const char *dev_name{nullptr};
+    if(name)
+    {
+        if(PlaybackDevices.empty())
+            mMainloop.probePlaybackDevices();
+
+        auto iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+            [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};
+        pulse_name = iter->device_name.c_str();
+        dev_name = iter->name.c_str();
+    }
+
+    MainloopUniqueLock plock{mMainloop};
+    mContext = plock.connectContext();
+
+    pa_stream_flags_t flags{PA_STREAM_START_CORKED | PA_STREAM_FIX_FORMAT | PA_STREAM_FIX_RATE |
+        PA_STREAM_FIX_CHANNELS};
+    if(!GetConfigValueBool(nullptr, "pulse", "allow-moves", true))
+        flags |= PA_STREAM_DONT_MOVE;
+
+    pa_sample_spec spec{};
+    spec.format = PA_SAMPLE_S16NE;
+    spec.rate = 44100;
+    spec.channels = 2;
+
+    if(!pulse_name)
+    {
+        static const auto defname = al::getenv("ALSOFT_PULSE_DEFAULT");
+        if(defname) pulse_name = defname->c_str();
+    }
+    TRACE("Connecting to \"%s\"\n", pulse_name ? pulse_name : "(default)");
+    mStream = plock.connectStream(pulse_name, mContext, flags, nullptr, &spec, nullptr,
+        BackendType::Playback);
+
+    pa_stream_set_moved_callback(mStream, [](pa_stream *stream, void *pdata) noexcept
+    { return static_cast<PulsePlayback*>(pdata)->streamMovedCallback(stream); }, this);
+    mFrameSize = static_cast<uint>(pa_frame_size(pa_stream_get_sample_spec(mStream)));
+
+    if(pulse_name) mDeviceName.emplace(pulse_name);
+    else mDeviceName.reset();
+    if(!dev_name)
+    {
+        auto name_callback = [](pa_context *context, const pa_sink_info *info, int eol, void *pdata) noexcept
+        { return static_cast<PulsePlayback*>(pdata)->sinkNameCallback(context, info, eol); };
+        pa_operation *op{pa_context_get_sink_info_by_name(mContext,
+            pa_stream_get_device_name(mStream), name_callback, this)};
+        plock.waitForOperation(op);
+    }
+    else
+        mDevice->DeviceName = dev_name;
+}
+
+bool PulsePlayback::reset()
+{
+    MainloopUniqueLock plock{mMainloop};
+    const auto deviceName = mDeviceName ? mDeviceName->c_str() : nullptr;
+
+    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 = nullptr;
+    }
+
+    auto info_callback = [](pa_context *context, const pa_sink_info *info, int eol, void *pdata) noexcept
+    { return static_cast<PulsePlayback*>(pdata)->sinkInfoCallback(context, info, eol); };
+    pa_operation *op{pa_context_get_sink_info_by_name(mContext, deviceName, info_callback, this)};
+    plock.waitForOperation(op);
+
+    pa_stream_flags_t flags{PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING |
+        PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_EARLY_REQUESTS};
+    if(!GetConfigValueBool(nullptr, "pulse", "allow-moves", true))
+        flags |= PA_STREAM_DONT_MOVE;
+    if(GetConfigValueBool(mDevice->DeviceName.c_str(), "pulse", "adjust-latency", false))
+    {
+        /* ADJUST_LATENCY can't be specified with EARLY_REQUESTS, for some
+         * reason. So if the user wants to adjust the overall device latency,
+         * we can't ask to get write signals as soon as minreq is reached.
+         */
+        flags &= ~PA_STREAM_EARLY_REQUESTS;
+        flags |= PA_STREAM_ADJUST_LATENCY;
+    }
+    if(GetConfigValueBool(mDevice->DeviceName.c_str(), "pulse", "fix-rate", false)
+        || !mDevice->Flags.test(FrequencyRequest))
+        flags |= PA_STREAM_FIX_RATE;
+
+    pa_channel_map chanmap{};
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+        chanmap = MonoChanMap;
+        break;
+    case DevFmtAmbi3D:
+        mDevice->FmtChans = DevFmtStereo;
+        /*fall-through*/
+    case DevFmtStereo:
+        chanmap = StereoChanMap;
+        break;
+    case DevFmtQuad:
+        chanmap = QuadChanMap;
+        break;
+    case DevFmtX51:
+        chanmap = (mIs51Rear ? X51RearChanMap : X51ChanMap);
+        break;
+    case DevFmtX61:
+        chanmap = X61ChanMap;
+        break;
+    case DevFmtX71:
+    case DevFmtX3D71:
+        chanmap = X71ChanMap;
+        break;
+    case DevFmtX714:
+        chanmap = X714ChanMap;
+        break;
+    }
+    setDefaultWFXChannelOrder();
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        mDevice->FmtType = DevFmtUByte;
+        /* fall-through */
+    case DevFmtUByte:
+        mSpec.format = PA_SAMPLE_U8;
+        break;
+    case DevFmtUShort:
+        mDevice->FmtType = DevFmtShort;
+        /* fall-through */
+    case DevFmtShort:
+        mSpec.format = PA_SAMPLE_S16NE;
+        break;
+    case DevFmtUInt:
+        mDevice->FmtType = DevFmtInt;
+        /* fall-through */
+    case DevFmtInt:
+        mSpec.format = PA_SAMPLE_S32NE;
+        break;
+    case DevFmtFloat:
+        mSpec.format = PA_SAMPLE_FLOAT32NE;
+        break;
+    }
+    mSpec.rate = mDevice->Frequency;
+    mSpec.channels = static_cast<uint8_t>(mDevice->channelsFromFmt());
+    if(pa_sample_spec_valid(&mSpec) == 0)
+        throw al::backend_exception{al::backend_error::DeviceError, "Invalid sample spec"};
+
+    const auto frame_size = static_cast<uint>(pa_frame_size(&mSpec));
+    mAttr.maxlength = ~0u;
+    mAttr.tlength = mDevice->BufferSize * frame_size;
+    mAttr.prebuf = 0u;
+    mAttr.minreq = mDevice->UpdateSize * frame_size;
+    mAttr.fragsize = ~0u;
+
+    mStream = plock.connectStream(deviceName, mContext, flags, &mAttr, &mSpec, &chanmap,
+        BackendType::Playback);
+
+    pa_stream_set_state_callback(mStream, [](pa_stream *stream, void *pdata) noexcept
+    { return static_cast<PulsePlayback*>(pdata)->streamStateCallback(stream); }, this);
+    pa_stream_set_moved_callback(mStream, [](pa_stream *stream, void *pdata) noexcept
+    { return static_cast<PulsePlayback*>(pdata)->streamMovedCallback(stream); }, this);
+
+    mSpec = *(pa_stream_get_sample_spec(mStream));
+    mFrameSize = static_cast<uint>(pa_frame_size(&mSpec));
+
+    if(mDevice->Frequency != mSpec.rate)
+    {
+        /* Server updated our playback rate, so modify the buffer attribs
+         * accordingly.
+         */
+        const auto scale = static_cast<double>(mSpec.rate) / mDevice->Frequency;
+        const auto perlen = static_cast<uint>(clampd(scale*mDevice->UpdateSize + 0.5, 64.0,
+            8192.0));
+        const auto buflen = static_cast<uint>(clampd(scale*mDevice->BufferSize + 0.5, perlen*2,
+            std::numeric_limits<int>::max()/mFrameSize));
+
+        mAttr.maxlength = ~0u;
+        mAttr.tlength = buflen * mFrameSize;
+        mAttr.prebuf = 0u;
+        mAttr.minreq = perlen * mFrameSize;
+
+        op = pa_stream_set_buffer_attr(mStream, &mAttr, &PulseMainloop::streamSuccessCallbackC,
+            &mMainloop);
+        plock.waitForOperation(op);
+
+        mDevice->Frequency = mSpec.rate;
+    }
+
+    auto attr_callback = [](pa_stream *stream, void *pdata) noexcept
+    { return static_cast<PulsePlayback*>(pdata)->bufferAttrCallback(stream); };
+    pa_stream_set_buffer_attr_callback(mStream, attr_callback, this);
+    bufferAttrCallback(mStream);
+
+    mDevice->BufferSize = mAttr.tlength / mFrameSize;
+    mDevice->UpdateSize = mAttr.minreq / mFrameSize;
+
+    return true;
+}
+
+void PulsePlayback::start()
+{
+    MainloopUniqueLock plock{mMainloop};
+
+    /* Write some samples to fill the buffer before we start feeding it newly
+     * mixed samples.
+     */
+    if(size_t todo{pa_stream_writable_size(mStream)})
+    {
+        void *buf{pa_xmalloc(todo)};
+        mDevice->renderSamples(buf, static_cast<uint>(todo/mFrameSize), mSpec.channels);
+        pa_stream_write(mStream, buf, todo, pa_xfree, 0, PA_SEEK_RELATIVE);
+    }
+
+    pa_stream_set_write_callback(mStream, [](pa_stream *stream, size_t nbytes, void *pdata)noexcept
+    { return static_cast<PulsePlayback*>(pdata)->streamWriteCallback(stream, nbytes); }, this);
+    pa_operation *op{pa_stream_cork(mStream, 0, &PulseMainloop::streamSuccessCallbackC,
+        &mMainloop)};
+
+    plock.waitForOperation(op);
+}
+
+void PulsePlayback::stop()
+{
+    MainloopUniqueLock plock{mMainloop};
+
+    pa_operation *op{pa_stream_cork(mStream, 1, &PulseMainloop::streamSuccessCallbackC,
+        &mMainloop)};
+    plock.waitForOperation(op);
+    pa_stream_set_write_callback(mStream, nullptr, nullptr);
+}
+
+
+ClockLatency PulsePlayback::getClockLatency()
+{
+    ClockLatency ret;
+    pa_usec_t latency;
+    int neg, err;
+
+    {
+        MainloopUniqueLock plock{mMainloop};
+        ret.ClockTime = GetDeviceClockTime(mDevice);
+        err = pa_stream_get_latency(mStream, &latency, &neg);
+    }
+
+    if(err != 0) UNLIKELY
+    {
+        /* If err = -PA_ERR_NODATA, it means we were called too soon after
+         * starting the stream and no timing info has been received from the
+         * server yet. Give a generic value since nothing better is available.
+         */
+        if(err != -PA_ERR_NODATA)
+            ERR("Failed to get stream latency: 0x%x\n", err);
+        latency = mDevice->BufferSize - mDevice->UpdateSize;
+        neg = 0;
+    }
+    else if(neg) UNLIKELY
+        latency = 0;
+    ret.Latency = std::chrono::microseconds{latency};
+
+    return ret;
+}
+
+
+struct PulseCapture final : public BackendBase {
+    PulseCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~PulseCapture() override;
+
+    void streamStateCallback(pa_stream *stream) noexcept;
+    void sourceNameCallback(pa_context *context, const pa_source_info *info, int eol) noexcept;
+    void streamMovedCallback(pa_stream *stream) noexcept;
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+    ClockLatency getClockLatency() override;
+
+    PulseMainloop mMainloop;
+
+    al::optional<std::string> mDeviceName{al::nullopt};
+
+    al::span<const al::byte> mCapBuffer;
+    size_t mHoleLength{0};
+    size_t mPacketLength{0};
+
+    uint mLastReadable{0u};
+    al::byte mSilentVal{};
+
+    pa_buffer_attr mAttr{};
+    pa_sample_spec mSpec{};
+
+    pa_stream *mStream{nullptr};
+    pa_context *mContext{nullptr};
+
+    DEF_NEWDEL(PulseCapture)
+};
+
+PulseCapture::~PulseCapture()
+{
+    if(!mContext)
+        return;
+
+    mMainloop.close(mContext, mStream);
+    mContext = nullptr;
+    mStream = nullptr;
+}
+
+
+void PulseCapture::streamStateCallback(pa_stream *stream) noexcept
+{
+    if(pa_stream_get_state(stream) == PA_STREAM_FAILED)
+    {
+        ERR("Received stream failure!\n");
+        mDevice->handleDisconnect("Capture stream failure");
+    }
+    mMainloop.signal();
+}
+
+void PulseCapture::sourceNameCallback(pa_context*, const pa_source_info *info, int eol) noexcept
+{
+    if(eol)
+    {
+        mMainloop.signal();
+        return;
+    }
+    mDevice->DeviceName = info->description;
+}
+
+void PulseCapture::streamMovedCallback(pa_stream *stream) noexcept
+{
+    mDeviceName = pa_stream_get_device_name(stream);
+    TRACE("Stream moved to %s\n", mDeviceName->c_str());
+}
+
+
+void PulseCapture::open(const char *name)
+{
+    if(!mMainloop)
+    {
+        mMainloop = PulseMainloop::Create();
+        mMainloop.start();
+    }
+
+    const char *pulse_name{nullptr};
+    if(name)
+    {
+        if(CaptureDevices.empty())
+            mMainloop.probeCaptureDevices();
+
+        auto iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+            [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};
+        pulse_name = iter->device_name.c_str();
+        mDevice->DeviceName = iter->name;
+    }
+
+    MainloopUniqueLock plock{mMainloop};
+    mContext = plock.connectContext();
+
+    pa_channel_map chanmap{};
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+        chanmap = MonoChanMap;
+        break;
+    case DevFmtStereo:
+        chanmap = StereoChanMap;
+        break;
+    case DevFmtQuad:
+        chanmap = QuadChanMap;
+        break;
+    case DevFmtX51:
+        chanmap = X51ChanMap;
+        break;
+    case DevFmtX61:
+        chanmap = X61ChanMap;
+        break;
+    case DevFmtX71:
+        chanmap = X71ChanMap;
+        break;
+    case DevFmtX714:
+        chanmap = X714ChanMap;
+        break;
+    case DevFmtX3D71:
+    case DevFmtAmbi3D:
+        throw al::backend_exception{al::backend_error::DeviceError, "%s capture not supported",
+            DevFmtChannelsString(mDevice->FmtChans)};
+    }
+    setDefaultWFXChannelOrder();
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtUByte:
+        mSilentVal = al::byte(0x80);
+        mSpec.format = PA_SAMPLE_U8;
+        break;
+    case DevFmtShort:
+        mSpec.format = PA_SAMPLE_S16NE;
+        break;
+    case DevFmtInt:
+        mSpec.format = PA_SAMPLE_S32NE;
+        break;
+    case DevFmtFloat:
+        mSpec.format = PA_SAMPLE_FLOAT32NE;
+        break;
+    case DevFmtByte:
+    case DevFmtUShort:
+    case DevFmtUInt:
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "%s capture samples not supported", DevFmtTypeString(mDevice->FmtType)};
+    }
+    mSpec.rate = mDevice->Frequency;
+    mSpec.channels = static_cast<uint8_t>(mDevice->channelsFromFmt());
+    if(pa_sample_spec_valid(&mSpec) == 0)
+        throw al::backend_exception{al::backend_error::DeviceError, "Invalid sample format"};
+
+    const auto frame_size = static_cast<uint>(pa_frame_size(&mSpec));
+    const uint samples{maxu(mDevice->BufferSize, 100 * mDevice->Frequency / 1000)};
+    mAttr.minreq = ~0u;
+    mAttr.prebuf = ~0u;
+    mAttr.maxlength = samples * frame_size;
+    mAttr.tlength = ~0u;
+    mAttr.fragsize = minu(samples, 50*mDevice->Frequency/1000) * frame_size;
+
+    pa_stream_flags_t flags{PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY};
+    if(!GetConfigValueBool(nullptr, "pulse", "allow-moves", true))
+        flags |= PA_STREAM_DONT_MOVE;
+
+    TRACE("Connecting to \"%s\"\n", pulse_name ? pulse_name : "(default)");
+    mStream = plock.connectStream(pulse_name, mContext, flags, &mAttr, &mSpec, &chanmap,
+        BackendType::Capture);
+
+    pa_stream_set_moved_callback(mStream, [](pa_stream *stream, void *pdata) noexcept
+    { return static_cast<PulseCapture*>(pdata)->streamMovedCallback(stream); }, this);
+    pa_stream_set_state_callback(mStream, [](pa_stream *stream, void *pdata) noexcept
+    { return static_cast<PulseCapture*>(pdata)->streamStateCallback(stream); }, this);
+
+    if(pulse_name) mDeviceName.emplace(pulse_name);
+    else mDeviceName.reset();
+    if(mDevice->DeviceName.empty())
+    {
+        auto name_callback = [](pa_context *context, const pa_source_info *info, int eol, void *pdata) noexcept
+        { return static_cast<PulseCapture*>(pdata)->sourceNameCallback(context, info, eol); };
+        pa_operation *op{pa_context_get_source_info_by_name(mContext,
+            pa_stream_get_device_name(mStream), name_callback, this)};
+        plock.waitForOperation(op);
+    }
+}
+
+void PulseCapture::start()
+{
+    MainloopUniqueLock plock{mMainloop};
+    pa_operation *op{pa_stream_cork(mStream, 0, &PulseMainloop::streamSuccessCallbackC,
+        &mMainloop)};
+    plock.waitForOperation(op);
+}
+
+void PulseCapture::stop()
+{
+    MainloopUniqueLock plock{mMainloop};
+    pa_operation *op{pa_stream_cork(mStream, 1, &PulseMainloop::streamSuccessCallbackC,
+        &mMainloop)};
+    plock.waitForOperation(op);
+}
+
+void PulseCapture::captureSamples(al::byte *buffer, uint samples)
+{
+    al::span<al::byte> dstbuf{buffer, samples * pa_frame_size(&mSpec)};
+
+    /* Capture is done in fragment-sized chunks, so we loop until we get all
+     * that's available.
+     */
+    mLastReadable -= static_cast<uint>(dstbuf.size());
+    while(!dstbuf.empty())
+    {
+        if(mHoleLength > 0) UNLIKELY
+        {
+            const size_t rem{minz(dstbuf.size(), mHoleLength)};
+            std::fill_n(dstbuf.begin(), rem, mSilentVal);
+            dstbuf = dstbuf.subspan(rem);
+            mHoleLength -= rem;
+
+            continue;
+        }
+        if(!mCapBuffer.empty())
+        {
+            const size_t rem{minz(dstbuf.size(), mCapBuffer.size())};
+            std::copy_n(mCapBuffer.begin(), rem, dstbuf.begin());
+            dstbuf = dstbuf.subspan(rem);
+            mCapBuffer = mCapBuffer.subspan(rem);
+
+            continue;
+        }
+
+        if(!mDevice->Connected.load(std::memory_order_acquire)) UNLIKELY
+            break;
+
+        MainloopUniqueLock plock{mMainloop};
+        if(mPacketLength > 0)
+        {
+            pa_stream_drop(mStream);
+            mPacketLength = 0;
+        }
+
+        const pa_stream_state_t state{pa_stream_get_state(mStream)};
+        if(!PA_STREAM_IS_GOOD(state)) UNLIKELY
+        {
+            mDevice->handleDisconnect("Bad capture state: %u", state);
+            break;
+        }
+
+        const void *capbuf;
+        size_t caplen;
+        if(pa_stream_peek(mStream, &capbuf, &caplen) < 0) UNLIKELY
+        {
+            mDevice->handleDisconnect("Failed retrieving capture samples: %s",
+                pa_strerror(pa_context_errno(mContext)));
+            break;
+        }
+        plock.unlock();
+
+        if(caplen == 0) break;
+        if(!capbuf) UNLIKELY
+            mHoleLength = caplen;
+        else
+            mCapBuffer = {static_cast<const al::byte*>(capbuf), caplen};
+        mPacketLength = caplen;
+    }
+    if(!dstbuf.empty())
+        std::fill(dstbuf.begin(), dstbuf.end(), mSilentVal);
+}
+
+uint PulseCapture::availableSamples()
+{
+    size_t readable{maxz(mCapBuffer.size(), mHoleLength)};
+
+    if(mDevice->Connected.load(std::memory_order_acquire))
+    {
+        MainloopUniqueLock plock{mMainloop};
+        size_t got{pa_stream_readable_size(mStream)};
+        if(static_cast<ssize_t>(got) < 0) UNLIKELY
+        {
+            const char *err{pa_strerror(static_cast<int>(got))};
+            ERR("pa_stream_readable_size() failed: %s\n", err);
+            mDevice->handleDisconnect("Failed getting readable size: %s", err);
+        }
+        else
+        {
+            /* "readable" is the number of bytes from the last packet that have
+             * not yet been read by the caller. So add the stream's readable
+             * size excluding the last packet (the stream size includes the
+             * last packet until it's dropped).
+             */
+            if(got > mPacketLength)
+                readable += got - mPacketLength;
+        }
+    }
+
+    /* Avoid uint overflow, and avoid decreasing the readable count. */
+    readable = std::min<size_t>(readable, std::numeric_limits<uint>::max());
+    mLastReadable = std::max(mLastReadable, static_cast<uint>(readable));
+    return mLastReadable / static_cast<uint>(pa_frame_size(&mSpec));
+}
+
+
+ClockLatency PulseCapture::getClockLatency()
+{
+    ClockLatency ret;
+    pa_usec_t latency;
+    int neg, err;
+
+    {
+        MainloopUniqueLock plock{mMainloop};
+        ret.ClockTime = GetDeviceClockTime(mDevice);
+        err = pa_stream_get_latency(mStream, &latency, &neg);
+    }
+
+    if(err != 0) UNLIKELY
+    {
+        ERR("Failed to get stream latency: 0x%x\n", err);
+        latency = 0;
+        neg = 0;
+    }
+    else if(neg) UNLIKELY
+        latency = 0;
+    ret.Latency = std::chrono::microseconds{latency};
+
+    return ret;
+}
+
+} // namespace
+
+
+bool PulseBackendFactory::init()
+{
+#ifdef HAVE_DYNLOAD
+    if(!pulse_handle)
+    {
+        bool ret{true};
+        std::string missing_funcs;
+
+#ifdef _WIN32
+#define PALIB "libpulse-0.dll"
+#elif defined(__APPLE__) && defined(__MACH__)
+#define PALIB "libpulse.0.dylib"
+#else
+#define PALIB "libpulse.so.0"
+#endif
+        pulse_handle = LoadLib(PALIB);
+        if(!pulse_handle)
+        {
+            WARN("Failed to load %s\n", PALIB);
+            return false;
+        }
+
+#define LOAD_FUNC(x) do {                                                     \
+    p##x = reinterpret_cast<decltype(p##x)>(GetSymbol(pulse_handle, #x));     \
+    if(!(p##x)) {                                                             \
+        ret = false;                                                          \
+        missing_funcs += "\n" #x;                                             \
+    }                                                                         \
+} while(0)
+        PULSE_FUNCS(LOAD_FUNC)
+#undef LOAD_FUNC
+
+        if(!ret)
+        {
+            WARN("Missing expected functions:%s\n", missing_funcs.c_str());
+            CloseLib(pulse_handle);
+            pulse_handle = nullptr;
+            return false;
+        }
+    }
+#endif /* HAVE_DYNLOAD */
+
+    pulse_ctx_flags = PA_CONTEXT_NOFLAGS;
+    if(!GetConfigValueBool(nullptr, "pulse", "spawn-server", false))
+        pulse_ctx_flags |= PA_CONTEXT_NOAUTOSPAWN;
+
+    try {
+        if(!gGlobalMainloop)
+        {
+            gGlobalMainloop = PulseMainloop::Create();
+            gGlobalMainloop.start();
+        }
+
+        MainloopUniqueLock plock{gGlobalMainloop};
+        pa_context *context{plock.connectContext()};
+        pa_context_disconnect(context);
+        pa_context_unref(context);
+        return true;
+    }
+    catch(...) {
+        return false;
+    }
+}
+
+bool PulseBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+std::string PulseBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+
+    auto add_device = [&outnames](const DevMap &entry) -> void
+    {
+        /* +1 to also append the null char (to ensure a null-separated list and
+         * double-null terminated list).
+         */
+        outnames.append(entry.name.c_str(), entry.name.length()+1);
+    };
+
+    switch(type)
+    {
+    case BackendType::Playback:
+        gGlobalMainloop.probePlaybackDevices();
+        std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+        break;
+
+    case BackendType::Capture:
+        gGlobalMainloop.probeCaptureDevices();
+        std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+        break;
+    }
+
+    return outnames;
+}
+
+BackendPtr PulseBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new PulsePlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new PulseCapture{device}};
+    return nullptr;
+}
+
+BackendFactory &PulseBackendFactory::getFactory()
+{
+    static PulseBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/pulseaudio.h b/alc/backends/pulseaudio.h
new file mode 100644 (file)
index 0000000..6690fe8
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_PULSEAUDIO_H
+#define BACKENDS_PULSEAUDIO_H
+
+#include "base.h"
+
+class PulseBackendFactory 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_PULSEAUDIO_H */
diff --git a/alc/backends/sdl2.cpp b/alc/backends/sdl2.cpp
new file mode 100644 (file)
index 0000000..a4a5a9a
--- /dev/null
@@ -0,0 +1,224 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2018 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 "sdl2.h"
+
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/logging.h"
+
+_Pragma("GCC diagnostic push")
+_Pragma("GCC diagnostic ignored \"-Wold-style-cast\"")
+#include "SDL.h"
+_Pragma("GCC diagnostic pop")
+
+
+namespace {
+
+#ifdef _WIN32
+#define DEVNAME_PREFIX "OpenAL Soft on "
+#else
+#define DEVNAME_PREFIX ""
+#endif
+
+constexpr char defaultDeviceName[] = DEVNAME_PREFIX "Default Device";
+
+struct Sdl2Backend final : public BackendBase {
+    Sdl2Backend(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~Sdl2Backend() override;
+
+    void audioCallback(Uint8 *stream, int len) noexcept;
+    static void audioCallbackC(void *ptr, Uint8 *stream, int len) noexcept
+    { static_cast<Sdl2Backend*>(ptr)->audioCallback(stream, len); }
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    SDL_AudioDeviceID mDeviceID{0u};
+    uint mFrameSize{0};
+
+    uint mFrequency{0u};
+    DevFmtChannels mFmtChans{};
+    DevFmtType     mFmtType{};
+    uint mUpdateSize{0u};
+
+    DEF_NEWDEL(Sdl2Backend)
+};
+
+Sdl2Backend::~Sdl2Backend()
+{
+    if(mDeviceID)
+        SDL_CloseAudioDevice(mDeviceID);
+    mDeviceID = 0;
+}
+
+void Sdl2Backend::audioCallback(Uint8 *stream, int len) noexcept
+{
+    const auto ulen = static_cast<unsigned int>(len);
+    assert((ulen % mFrameSize) == 0);
+    mDevice->renderSamples(stream, ulen / mFrameSize, mDevice->channelsFromFmt());
+}
+
+void Sdl2Backend::open(const char *name)
+{
+    SDL_AudioSpec want{}, have{};
+
+    want.freq = static_cast<int>(mDevice->Frequency);
+    switch(mDevice->FmtType)
+    {
+    case DevFmtUByte: want.format = AUDIO_U8; break;
+    case DevFmtByte: want.format = AUDIO_S8; break;
+    case DevFmtUShort: want.format = AUDIO_U16SYS; break;
+    case DevFmtShort: want.format = AUDIO_S16SYS; break;
+    case DevFmtUInt: /* fall-through */
+    case DevFmtInt: want.format = AUDIO_S32SYS; break;
+    case DevFmtFloat: want.format = AUDIO_F32; break;
+    }
+    want.channels = (mDevice->FmtChans == DevFmtMono) ? 1 : 2;
+    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)
+        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)
+            devid = SDL_OpenAudioDevice(name+prefix_len, SDL_FALSE, &want, &have,
+                SDL_AUDIO_ALLOW_ANY_CHANGE);
+        else
+            devid = SDL_OpenAudioDevice(name, SDL_FALSE, &want, &have, SDL_AUDIO_ALLOW_ANY_CHANGE);
+    }
+    if(!devid)
+        throw al::backend_exception{al::backend_error::NoDevice, "%s", SDL_GetError()};
+
+    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:     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};
+    }
+
+    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;
+}
+
+bool Sdl2Backend::reset()
+{
+    mDevice->Frequency = mFrequency;
+    mDevice->FmtChans = mFmtChans;
+    mDevice->FmtType = mFmtType;
+    mDevice->UpdateSize = mUpdateSize;
+    mDevice->BufferSize = mUpdateSize * 2; /* SDL always (tries to) use two periods. */
+    setDefaultWFXChannelOrder();
+    return true;
+}
+
+void Sdl2Backend::start()
+{ SDL_PauseAudioDevice(mDeviceID, 0); }
+
+void Sdl2Backend::stop()
+{ SDL_PauseAudioDevice(mDeviceID, 1); }
+
+} // namespace
+
+BackendFactory &SDL2BackendFactory::getFactory()
+{
+    static SDL2BackendFactory factory{};
+    return factory;
+}
+
+bool SDL2BackendFactory::init()
+{ return (SDL_InitSubSystem(SDL_INIT_AUDIO) == 0); }
+
+bool SDL2BackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback; }
+
+std::string SDL2BackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+
+    if(type != BackendType::Playback)
+        return outnames;
+
+    int num_devices{SDL_GetNumAudioDevices(SDL_FALSE)};
+
+    /* Includes null char. */
+    outnames.append(defaultDeviceName, sizeof(defaultDeviceName));
+    for(int i{0};i < num_devices;++i)
+    {
+        std::string name{DEVNAME_PREFIX};
+        name += SDL_GetAudioDeviceName(i, SDL_FALSE);
+        if(!name.empty())
+            outnames.append(name.c_str(), name.length()+1);
+    }
+    return outnames;
+}
+
+BackendPtr SDL2BackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new Sdl2Backend{device}};
+    return nullptr;
+}
diff --git a/alc/backends/sdl2.h b/alc/backends/sdl2.h
new file mode 100644 (file)
index 0000000..3bd8df8
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_SDL2_H
+#define BACKENDS_SDL2_H
+
+#include "base.h"
+
+struct SDL2BackendFactory 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_SDL2_H */
diff --git a/alc/backends/sndio.cpp b/alc/backends/sndio.cpp
new file mode 100644 (file)
index 0000000..077e77f
--- /dev/null
@@ -0,0 +1,540 @@
+/**
+ * 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 "sndio.h"
+
+#include <functional>
+#include <inttypes.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <thread>
+
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "vector.h"
+
+#include <sndio.h>
+
+
+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(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~SndioPlayback() override;
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    sio_hdl *mSndHandle{nullptr};
+    uint mFrameStep{};
+
+    al::vector<al::byte> mBuffer;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(SndioPlayback)
+};
+
+SndioPlayback::~SndioPlayback()
+{
+    if(mSndHandle)
+        sio_close(mSndHandle);
+    mSndHandle = nullptr;
+}
+
+int SndioPlayback::mixerProc()
+{
+    const size_t frameStep{mFrameStep};
+    const size_t frameSize{frameStep * mDevice->bytesFromFmt()};
+
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        al::span<al::byte> buffer{mBuffer};
+
+        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, buffer.data(), buffer.size())};
+            if(wrote > buffer.size() || wrote == 0)
+            {
+                ERR("sio_write failed: 0x%" PRIx64 "\n", wrote);
+                mDevice->handleDisconnect("Failed to write playback samples");
+                break;
+            }
+            buffer = buffer.subspan(wrote);
+        }
+    }
+
+    return 0;
+}
+
+
+void SndioPlayback::open(const char *name)
+{
+    if(!name)
+        name = sndio_device;
+    else if(strcmp(name, sndio_device) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    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()
+{
+    SioPar par;
+
+    auto tryfmt = mDevice->FmtType;
+retry_params:
+    switch(tryfmt)
+    {
+    case DevFmtByte:
+        par.bits = 8;
+        par.sig = 1;
+        break;
+    case DevFmtUByte:
+        par.bits = 8;
+        par.sig = 0;
+        break;
+    case DevFmtShort:
+        par.bits = 16;
+        par.sig = 1;
+        break;
+    case DevFmtUShort:
+        par.bits = 16;
+        par.sig = 0;
+        break;
+    case DevFmtFloat:
+    case DevFmtInt:
+        par.bits = 32;
+        par.sig = 1;
+        break;
+    case DevFmtUInt:
+        par.bits = 32;
+        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;
+
+    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"};
+    }
+    catch(al::backend_exception &e) {
+        if(tryfmt == DevFmtShort)
+            throw;
+        par.clear();
+        tryfmt = DevFmtShort;
+        goto retry_params;
+    }
+
+    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())
+    {
+        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();
+
+    mDevice->UpdateSize = par.round;
+    mDevice->BufferSize = par.bufsz + par.round;
+
+    mBuffer.resize(mDevice->UpdateSize * par.pchan*par.bps);
+    if(par.sig == 1)
+        std::fill(mBuffer.begin(), mBuffer.end(), al::byte{});
+    else if(par.bits == 8)
+        std::fill_n(mBuffer.data(), mBuffer.size(), al::byte(0x80));
+    else if(par.bits == 16)
+        std::fill_n(reinterpret_cast<uint16_t*>(mBuffer.data()), mBuffer.size()/2, 0x8000);
+    else if(par.bits == 32)
+        std::fill_n(reinterpret_cast<uint32_t*>(mBuffer.data()), mBuffer.size()/4, 0x80000000u);
+
+    return true;
+}
+
+void SndioPlayback::start()
+{
+    if(!sio_start(mSndHandle))
+        throw al::backend_exception{al::backend_error::DeviceError, "Error starting playback"};
+
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&SndioPlayback::mixerProc), this};
+    }
+    catch(std::exception& e) {
+        sio_stop(mSndHandle);
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void SndioPlayback::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    if(!sio_stop(mSndHandle))
+        ERR("Error stopping device\n");
+}
+
+
+/* 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(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~SndioCapture() override;
+
+    int recordProc();
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    sio_hdl *mSndHandle{nullptr};
+
+    RingBufferPtr mRing;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(SndioCapture)
+};
+
+SndioCapture::~SndioCapture()
+{
+    if(mSndHandle)
+        sio_close(mSndHandle);
+    mSndHandle = nullptr;
+}
+
+int SndioCapture::recordProc()
+{
+    SetRTPriority();
+    althrd_setname(RECORD_THREAD_NAME);
+
+    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))
+    {
+        /* Wait until there's some samples to read. */
+        const int nfds{sio_pollfd(mSndHandle, fds.get(), POLLIN)};
+        if(nfds <= 0)
+        {
+            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;
+
+        auto data = mRing->getWriteVector();
+        al::span<al::byte> buffer{data.first.buf, data.first.len*frameSize};
+        while(!buffer.empty())
+        {
+            size_t got{sio_read(mSndHandle, buffer.data(), buffer.size())};
+            if(got == 0)
+                break;
+            if(got > buffer.size())
+            {
+                ERR("sio_read failed: 0x%" PRIx64 "\n", got);
+                mDevice->handleDisconnect("sio_read failed: 0x%" PRIx64, got);
+                break;
+            }
+
+            mRing->writeAdvance(got / frameSize);
+            buffer = buffer.subspan(got);
+            if(buffer.empty())
+            {
+                data = mRing->getWriteVector();
+                buffer = {data.first.buf, data.first.len*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;
+}
+
+
+void SndioCapture::open(const char *name)
+{
+    if(!name)
+        name = sndio_device;
+    else if(strcmp(name, sndio_device) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    mSndHandle = sio_open(nullptr, SIO_REC, true);
+    if(mSndHandle == nullptr)
+        throw al::backend_exception{al::backend_error::NoDevice, "Could not open backend device"};
+
+    SioPar par;
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        par.bits = 8;
+        par.sig = 1;
+        break;
+    case DevFmtUByte:
+        par.bits = 8;
+        par.sig = 0;
+        break;
+    case DevFmtShort:
+        par.bits = 16;
+        par.sig = 1;
+        break;
+    case DevFmtUShort:
+        par.bits = 16;
+        par.sig = 0;
+        break;
+    case DevFmtInt:
+        par.bits = 32;
+        par.sig = 1;
+        break;
+    case DevFmtUInt:
+        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.bps = SIO_BPS(par.bits);
+    par.le = SIO_LE_NATIVE;
+    par.msb = 1;
+    par.rchan = mDevice->channelsFromFmt();
+    par.rate = mDevice->Frequency;
+
+    par.appbufsz = maxu(mDevice->BufferSize, mDevice->Frequency/10);
+    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.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};
+
+    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.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();
+
+    mDevice->DeviceName = name;
+}
+
+void SndioCapture::start()
+{
+    if(!sio_start(mSndHandle))
+        throw al::backend_exception{al::backend_error::DeviceError, "Error starting capture"};
+
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&SndioCapture::recordProc), this};
+    }
+    catch(std::exception& e) {
+        sio_stop(mSndHandle);
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start capture thread: %s", e.what()};
+    }
+}
+
+void SndioCapture::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    if(!sio_stop(mSndHandle))
+        ERR("Error stopping device\n");
+}
+
+void SndioCapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, samples); }
+
+uint SndioCapture::availableSamples()
+{ return static_cast<uint>(mRing->readSpace()); }
+
+} // namespace
+
+BackendFactory &SndIOBackendFactory::getFactory()
+{
+    static SndIOBackendFactory factory{};
+    return factory;
+}
+
+bool SndIOBackendFactory::init()
+{ return true; }
+
+bool SndIOBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+std::string SndIOBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    switch(type)
+    {
+    case BackendType::Playback:
+    case BackendType::Capture:
+        /* Includes null char. */
+        outnames.append(sndio_device, sizeof(sndio_device));
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr SndIOBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new SndioPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new SndioCapture{device}};
+    return nullptr;
+}
diff --git a/alc/backends/sndio.h b/alc/backends/sndio.h
new file mode 100644 (file)
index 0000000..d943319
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_SNDIO_H
+#define BACKENDS_SNDIO_H
+
+#include "base.h"
+
+struct SndIOBackendFactory 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_SNDIO_H */
diff --git a/alc/backends/solaris.cpp b/alc/backends/solaris.cpp
new file mode 100644 (file)
index 0000000..791609c
--- /dev/null
@@ -0,0 +1,303 @@
+/**
+ * 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 "solaris.h"
+
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <memory.h>
+#include <unistd.h>
+#include <errno.h>
+#include <poll.h>
+#include <math.h>
+#include <string.h>
+
+#include <thread>
+#include <functional>
+
+#include "albyte.h"
+#include "alc/alconfig.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "threads.h"
+#include "vector.h"
+
+#include <sys/audioio.h>
+
+
+namespace {
+
+constexpr char solaris_device[] = "Solaris Default";
+
+std::string solaris_driver{"/dev/audio"};
+
+
+struct SolarisBackend final : public BackendBase {
+    SolarisBackend(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~SolarisBackend() override;
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    int mFd{-1};
+
+    uint mFrameStep{};
+    al::vector<al::byte> mBuffer;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(SolarisBackend)
+};
+
+SolarisBackend::~SolarisBackend()
+{
+    if(mFd != -1)
+        close(mFd);
+    mFd = -1;
+}
+
+int SolarisBackend::mixerProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    const size_t frame_step{mDevice->channelsFromFmt()};
+    const uint frame_size{mDevice->frameSizeFromFmt()};
+
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        pollfd pollitem{};
+        pollitem.fd = mFd;
+        pollitem.events = POLLOUT;
+
+        int pret{poll(&pollitem, 1, 1000)};
+        if(pret < 0)
+        {
+            if(errno == EINTR || errno == EAGAIN)
+                continue;
+            ERR("poll failed: %s\n", strerror(errno));
+            mDevice->handleDisconnect("Failed to wait for playback buffer: %s", strerror(errno));
+            break;
+        }
+        else if(pret == 0)
+        {
+            WARN("poll timeout\n");
+            continue;
+        }
+
+        al::byte *write_ptr{mBuffer.data()};
+        size_t to_write{mBuffer.size()};
+        mDevice->renderSamples(write_ptr, static_cast<uint>(to_write/frame_size), frame_step);
+        while(to_write > 0 && !mKillNow.load(std::memory_order_acquire))
+        {
+            ssize_t wrote{write(mFd, write_ptr, to_write)};
+            if(wrote < 0)
+            {
+                if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
+                    continue;
+                ERR("write failed: %s\n", strerror(errno));
+                mDevice->handleDisconnect("Failed to write playback samples: %s", strerror(errno));
+                break;
+            }
+
+            to_write -= static_cast<size_t>(wrote);
+            write_ptr += wrote;
+        }
+    }
+
+    return 0;
+}
+
+
+void SolarisBackend::open(const char *name)
+{
+    if(!name)
+        name = solaris_device;
+    else if(strcmp(name, solaris_device) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    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;
+}
+
+bool SolarisBackend::reset()
+{
+    audio_info_t info;
+    AUDIO_INITINFO(&info);
+
+    info.play.sample_rate = mDevice->Frequency;
+    info.play.channels = mDevice->channelsFromFmt();
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        info.play.precision = 8;
+        info.play.encoding = AUDIO_ENCODING_LINEAR;
+        break;
+    case DevFmtUByte:
+        info.play.precision = 8;
+        info.play.encoding = AUDIO_ENCODING_LINEAR8;
+        break;
+    case DevFmtUShort:
+    case DevFmtInt:
+    case DevFmtUInt:
+    case DevFmtFloat:
+        mDevice->FmtType = DevFmtShort;
+        /* fall-through */
+    case DevFmtShort:
+        info.play.precision = 16;
+        info.play.encoding = AUDIO_ENCODING_LINEAR;
+        break;
+    }
+    info.play.buffer_size = mDevice->BufferSize * mDevice->frameSizeFromFmt();
+
+    if(ioctl(mFd, AUDIO_SETINFO, &info) < 0)
+    {
+        ERR("ioctl failed: %s\n", strerror(errno));
+        return false;
+    }
+
+    if(mDevice->channelsFromFmt() != info.play.channels)
+    {
+        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;
+    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("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 / frame_size;
+    /* How to get the actual period size/count? */
+    mDevice->UpdateSize = mDevice->BufferSize / 2;
+
+    setDefaultChannelOrder();
+
+    mBuffer.resize(mDevice->UpdateSize * size_t{frame_size});
+    std::fill(mBuffer.begin(), mBuffer.end(), al::byte{});
+
+    return true;
+}
+
+void SolarisBackend::start()
+{
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&SolarisBackend::mixerProc), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void SolarisBackend::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    if(ioctl(mFd, AUDIO_DRAIN) < 0)
+        ERR("Error draining device: %s\n", strerror(errno));
+}
+
+} // namespace
+
+BackendFactory &SolarisBackendFactory::getFactory()
+{
+    static SolarisBackendFactory factory{};
+    return factory;
+}
+
+bool SolarisBackendFactory::init()
+{
+    if(auto devopt = ConfigValueStr(nullptr, "solaris", "device"))
+        solaris_driver = std::move(*devopt);
+    return true;
+}
+
+bool SolarisBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback; }
+
+std::string SolarisBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    switch(type)
+    {
+    case BackendType::Playback:
+    {
+        struct stat buf;
+        if(stat(solaris_driver.c_str(), &buf) == 0)
+            outnames.append(solaris_device, sizeof(solaris_device));
+    }
+    break;
+
+    case BackendType::Capture:
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr SolarisBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new SolarisBackend{device}};
+    return nullptr;
+}
diff --git a/alc/backends/solaris.h b/alc/backends/solaris.h
new file mode 100644 (file)
index 0000000..5da6ad3
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_SOLARIS_H
+#define BACKENDS_SOLARIS_H
+
+#include "base.h"
+
+struct SolarisBackendFactory 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_SOLARIS_H */
diff --git a/alc/backends/wasapi.cpp b/alc/backends/wasapi.cpp
new file mode 100644 (file)
index 0000000..e834eef
--- /dev/null
@@ -0,0 +1,1994 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2011 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 "wasapi.h"
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <memory.h>
+
+#include <wtypes.h>
+#include <mmdeviceapi.h>
+#include <audioclient.h>
+#include <cguid.h>
+#include <devpropdef.h>
+#include <mmreg.h>
+#include <propsys.h>
+#include <propkey.h>
+#include <devpkey.h>
+#ifndef _WAVEFORMATEXTENSIBLE_
+#include <ks.h>
+#include <ksmedia.h>
+#endif
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <condition_variable>
+#include <cstring>
+#include <deque>
+#include <functional>
+#include <future>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "albit.h"
+#include "alc/alconfig.h"
+#include "alnumeric.h"
+#include "comptr.h"
+#include "core/converter.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "ringbuffer.h"
+#include "strutils.h"
+#include "threads.h"
+
+
+/* Some headers seem to define these as macros for __uuidof, which is annoying
+ * since some headers don't declare them at all. Hopefully the ifdef is enough
+ * to tell if they need to be declared.
+ */
+#ifndef KSDATAFORMAT_SUBTYPE_PCM
+DEFINE_GUID(KSDATAFORMAT_SUBTYPE_PCM, 0x00000001, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+#endif
+#ifndef KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
+DEFINE_GUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x00000003, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+#endif
+
+DEFINE_DEVPROPKEY(DEVPKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80,0x20, 0x67,0xd1,0x46,0xa8,0x50,0xe0, 14);
+DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_FormFactor, 0x1da5d803, 0xd492, 0x4edd, 0x8c,0x23, 0xe0,0xc0,0xff,0xee,0x7f,0x0e, 0);
+DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_GUID, 0x1da5d803, 0xd492, 0x4edd, 0x8c, 0x23,0xe0, 0xc0,0xff,0xee,0x7f,0x0e, 4 );
+
+
+namespace {
+
+using std::chrono::nanoseconds;
+using std::chrono::milliseconds;
+using std::chrono::seconds;
+
+using ReferenceTime = std::chrono::duration<REFERENCE_TIME,std::ratio<1,10000000>>;
+
+inline constexpr ReferenceTime operator "" _reftime(unsigned long long int n) noexcept
+{ return ReferenceTime{static_cast<REFERENCE_TIME>(n)}; }
+
+
+#define MONO SPEAKER_FRONT_CENTER
+#define STEREO (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT)
+#define QUAD (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT)
+#define X5DOT1 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT)
+#define X5DOT1REAR (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT)
+#define X6DOT1 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_CENTER|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT)
+#define X7DOT1 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT)
+#define X7DOT1DOT4 (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT|SPEAKER_SIDE_LEFT|SPEAKER_SIDE_RIGHT|SPEAKER_TOP_FRONT_LEFT|SPEAKER_TOP_FRONT_RIGHT|SPEAKER_TOP_BACK_LEFT|SPEAKER_TOP_BACK_RIGHT)
+
+constexpr inline DWORD MaskFromTopBits(DWORD b) noexcept
+{
+    b |= b>>1;
+    b |= b>>2;
+    b |= b>>4;
+    b |= b>>8;
+    b |= b>>16;
+    return b;
+}
+constexpr DWORD MonoMask{MaskFromTopBits(MONO)};
+constexpr DWORD StereoMask{MaskFromTopBits(STEREO)};
+constexpr DWORD QuadMask{MaskFromTopBits(QUAD)};
+constexpr DWORD X51Mask{MaskFromTopBits(X5DOT1)};
+constexpr DWORD X51RearMask{MaskFromTopBits(X5DOT1REAR)};
+constexpr DWORD X61Mask{MaskFromTopBits(X6DOT1)};
+constexpr DWORD X71Mask{MaskFromTopBits(X7DOT1)};
+constexpr DWORD X714Mask{MaskFromTopBits(X7DOT1DOT4)};
+
+constexpr char DevNameHead[] = "OpenAL Soft on ";
+constexpr size_t DevNameHeadLen{al::size(DevNameHead) - 1};
+
+
+/* Scales the given reftime value, rounding the result. */
+inline uint RefTime2Samples(const ReferenceTime &val, uint srate)
+{
+    const auto retval = (val*srate + ReferenceTime{seconds{1}}/2) / seconds{1};
+    return static_cast<uint>(mini64(retval, std::numeric_limits<uint>::max()));
+}
+
+
+class GuidPrinter {
+    char mMsg[64];
+
+public:
+    GuidPrinter(const GUID &guid)
+    {
+        std::snprintf(mMsg, al::size(mMsg), "{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}",
+            DWORD{guid.Data1}, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2],
+            guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
+    }
+    const char *c_str() const { return mMsg; }
+};
+
+struct PropVariant {
+    PROPVARIANT mProp;
+
+public:
+    PropVariant() { PropVariantInit(&mProp); }
+    ~PropVariant() { clear(); }
+
+    void clear() { PropVariantClear(&mProp); }
+
+    PROPVARIANT* get() noexcept { return &mProp; }
+
+    PROPVARIANT& operator*() noexcept { return mProp; }
+    const PROPVARIANT& operator*() const noexcept { return mProp; }
+
+    PROPVARIANT* operator->() noexcept { return &mProp; }
+    const PROPVARIANT* operator->() const noexcept { return &mProp; }
+};
+
+struct DevMap {
+    std::string name;
+    std::string endpoint_guid; // obtained from PKEY_AudioEndpoint_GUID , set to "Unknown device GUID" if absent.
+    std::wstring devid;
+
+    template<typename T0, typename T1, typename T2>
+    DevMap(T0&& name_, T1&& guid_, T2&& devid_)
+      : name{std::forward<T0>(name_)}
+      , endpoint_guid{std::forward<T1>(guid_)}
+      , devid{std::forward<T2>(devid_)}
+    { }
+};
+
+bool checkName(const al::vector<DevMap> &list, const std::string &name)
+{
+    auto match_name = [&name](const DevMap &entry) -> bool
+    { return entry.name == name; };
+    return std::find_if(list.cbegin(), list.cend(), match_name) != list.cend();
+}
+
+al::vector<DevMap> PlaybackDevices;
+al::vector<DevMap> CaptureDevices;
+
+
+using NameGUIDPair = std::pair<std::string,std::string>;
+NameGUIDPair get_device_name_and_guid(IMMDevice *device)
+{
+    static constexpr char UnknownName[]{"Unknown Device Name"};
+    static constexpr char UnknownGuid[]{"Unknown Device GUID"};
+    std::string name, guid;
+
+    ComPtr<IPropertyStore> ps;
+    HRESULT hr = device->OpenPropertyStore(STGM_READ, ps.getPtr());
+    if(FAILED(hr))
+    {
+        WARN("OpenPropertyStore failed: 0x%08lx\n", hr);
+        return std::make_pair(UnknownName, UnknownGuid);
+    }
+
+    PropVariant pvprop;
+    hr = ps->GetValue(reinterpret_cast<const PROPERTYKEY&>(DEVPKEY_Device_FriendlyName), pvprop.get());
+    if(FAILED(hr))
+    {
+        WARN("GetValue Device_FriendlyName failed: 0x%08lx\n", hr);
+        name += UnknownName;
+    }
+    else if(pvprop->vt == VT_LPWSTR)
+        name += wstr_to_utf8(pvprop->pwszVal);
+    else
+    {
+        WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvprop->vt);
+        name += UnknownName;
+    }
+
+    pvprop.clear();
+    hr = ps->GetValue(reinterpret_cast<const PROPERTYKEY&>(PKEY_AudioEndpoint_GUID), pvprop.get());
+    if(FAILED(hr))
+    {
+        WARN("GetValue AudioEndpoint_GUID failed: 0x%08lx\n", hr);
+        guid = UnknownGuid;
+    }
+    else if(pvprop->vt == VT_LPWSTR)
+        guid = wstr_to_utf8(pvprop->pwszVal);
+    else
+    {
+        WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvprop->vt);
+        guid = UnknownGuid;
+    }
+
+    return std::make_pair(std::move(name), std::move(guid));
+}
+
+EndpointFormFactor get_device_formfactor(IMMDevice *device)
+{
+    ComPtr<IPropertyStore> ps;
+    HRESULT hr{device->OpenPropertyStore(STGM_READ, ps.getPtr())};
+    if(FAILED(hr))
+    {
+        WARN("OpenPropertyStore failed: 0x%08lx\n", hr);
+        return UnknownFormFactor;
+    }
+
+    EndpointFormFactor formfactor{UnknownFormFactor};
+    PropVariant pvform;
+    hr = ps->GetValue(PKEY_AudioEndpoint_FormFactor, pvform.get());
+    if(FAILED(hr))
+        WARN("GetValue AudioEndpoint_FormFactor failed: 0x%08lx\n", hr);
+    else if(pvform->vt == VT_UI4)
+        formfactor = static_cast<EndpointFormFactor>(pvform->ulVal);
+    else if(pvform->vt != VT_EMPTY)
+        WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvform->vt);
+    return formfactor;
+}
+
+
+void add_device(IMMDevice *device, const WCHAR *devid, al::vector<DevMap> &list)
+{
+    for(auto &entry : list)
+    {
+        if(entry.devid == devid)
+            return;
+    }
+
+    auto name_guid = get_device_name_and_guid(device);
+
+    int count{1};
+    std::string newname{name_guid.first};
+    while(checkName(list, newname))
+    {
+        newname = name_guid.first;
+        newname += " #";
+        newname += std::to_string(++count);
+    }
+    list.emplace_back(std::move(newname), std::move(name_guid.second), devid);
+    const DevMap &newentry = list.back();
+
+    TRACE("Got device \"%s\", \"%s\", \"%ls\"\n", newentry.name.c_str(),
+          newentry.endpoint_guid.c_str(), newentry.devid.c_str());
+}
+
+WCHAR *get_device_id(IMMDevice *device)
+{
+    WCHAR *devid;
+
+    const HRESULT hr{device->GetId(&devid)};
+    if(FAILED(hr))
+    {
+        ERR("Failed to get device id: %lx\n", hr);
+        return nullptr;
+    }
+
+    return devid;
+}
+
+void probe_devices(IMMDeviceEnumerator *devenum, EDataFlow flowdir, al::vector<DevMap> &list)
+{
+    al::vector<DevMap>{}.swap(list);
+
+    ComPtr<IMMDeviceCollection> coll;
+    HRESULT hr{devenum->EnumAudioEndpoints(flowdir, DEVICE_STATE_ACTIVE, coll.getPtr())};
+    if(FAILED(hr))
+    {
+        ERR("Failed to enumerate audio endpoints: 0x%08lx\n", hr);
+        return;
+    }
+
+    UINT count{0};
+    hr = coll->GetCount(&count);
+    if(SUCCEEDED(hr) && count > 0)
+        list.reserve(count);
+
+    ComPtr<IMMDevice> device;
+    hr = devenum->GetDefaultAudioEndpoint(flowdir, eMultimedia, device.getPtr());
+    if(SUCCEEDED(hr))
+    {
+        if(WCHAR *devid{get_device_id(device.get())})
+        {
+            add_device(device.get(), devid, list);
+            CoTaskMemFree(devid);
+        }
+        device = nullptr;
+    }
+
+    for(UINT i{0};i < count;++i)
+    {
+        hr = coll->Item(i, device.getPtr());
+        if(FAILED(hr)) continue;
+
+        if(WCHAR *devid{get_device_id(device.get())})
+        {
+            add_device(device.get(), devid, list);
+            CoTaskMemFree(devid);
+        }
+        device = nullptr;
+    }
+}
+
+
+bool MakeExtensible(WAVEFORMATEXTENSIBLE *out, const WAVEFORMATEX *in)
+{
+    *out = WAVEFORMATEXTENSIBLE{};
+    if(in->wFormatTag == WAVE_FORMAT_EXTENSIBLE)
+    {
+        *out = *CONTAINING_RECORD(in, const WAVEFORMATEXTENSIBLE, Format);
+        out->Format.cbSize = sizeof(*out) - sizeof(out->Format);
+    }
+    else if(in->wFormatTag == WAVE_FORMAT_PCM)
+    {
+        out->Format = *in;
+        out->Format.cbSize = 0;
+        out->Samples.wValidBitsPerSample = out->Format.wBitsPerSample;
+        if(out->Format.nChannels == 1)
+            out->dwChannelMask = MONO;
+        else if(out->Format.nChannels == 2)
+            out->dwChannelMask = STEREO;
+        else
+            ERR("Unhandled PCM channel count: %d\n", out->Format.nChannels);
+        out->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+    }
+    else if(in->wFormatTag == WAVE_FORMAT_IEEE_FLOAT)
+    {
+        out->Format = *in;
+        out->Format.cbSize = 0;
+        out->Samples.wValidBitsPerSample = out->Format.wBitsPerSample;
+        if(out->Format.nChannels == 1)
+            out->dwChannelMask = MONO;
+        else if(out->Format.nChannels == 2)
+            out->dwChannelMask = STEREO;
+        else
+            ERR("Unhandled IEEE float channel count: %d\n", out->Format.nChannels);
+        out->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+    }
+    else
+    {
+        ERR("Unhandled format tag: 0x%04x\n", in->wFormatTag);
+        return false;
+    }
+    return true;
+}
+
+void TraceFormat(const char *msg, const WAVEFORMATEX *format)
+{
+    constexpr size_t fmtex_extra_size{sizeof(WAVEFORMATEXTENSIBLE)-sizeof(WAVEFORMATEX)};
+    if(format->wFormatTag == WAVE_FORMAT_EXTENSIBLE && format->cbSize >= fmtex_extra_size)
+    {
+        const WAVEFORMATEXTENSIBLE *fmtex{
+            CONTAINING_RECORD(format, const WAVEFORMATEXTENSIBLE, Format)};
+        TRACE("%s:\n"
+            "    FormatTag      = 0x%04x\n"
+            "    Channels       = %d\n"
+            "    SamplesPerSec  = %lu\n"
+            "    AvgBytesPerSec = %lu\n"
+            "    BlockAlign     = %d\n"
+            "    BitsPerSample  = %d\n"
+            "    Size           = %d\n"
+            "    Samples        = %d\n"
+            "    ChannelMask    = 0x%lx\n"
+            "    SubFormat      = %s\n",
+            msg, fmtex->Format.wFormatTag, fmtex->Format.nChannels, fmtex->Format.nSamplesPerSec,
+            fmtex->Format.nAvgBytesPerSec, fmtex->Format.nBlockAlign, fmtex->Format.wBitsPerSample,
+            fmtex->Format.cbSize, fmtex->Samples.wReserved, fmtex->dwChannelMask,
+            GuidPrinter{fmtex->SubFormat}.c_str());
+    }
+    else
+        TRACE("%s:\n"
+            "    FormatTag      = 0x%04x\n"
+            "    Channels       = %d\n"
+            "    SamplesPerSec  = %lu\n"
+            "    AvgBytesPerSec = %lu\n"
+            "    BlockAlign     = %d\n"
+            "    BitsPerSample  = %d\n"
+            "    Size           = %d\n",
+            msg, format->wFormatTag, format->nChannels, format->nSamplesPerSec,
+            format->nAvgBytesPerSec, format->nBlockAlign, format->wBitsPerSample, format->cbSize);
+}
+
+
+enum class MsgType {
+    OpenDevice,
+    ResetDevice,
+    StartDevice,
+    StopDevice,
+    CloseDevice,
+    EnumeratePlayback,
+    EnumerateCapture,
+
+    Count,
+    QuitThread = Count
+};
+
+constexpr char MessageStr[static_cast<size_t>(MsgType::Count)][20]{
+    "Open Device",
+    "Reset Device",
+    "Start Device",
+    "Stop Device",
+    "Close Device",
+    "Enumerate Playback",
+    "Enumerate Capture"
+};
+
+
+/* Proxy interface used by the message handler. */
+struct WasapiProxy {
+    virtual ~WasapiProxy() = default;
+
+    virtual HRESULT openProxy(const char *name) = 0;
+    virtual void closeProxy() = 0;
+
+    virtual HRESULT resetProxy() = 0;
+    virtual HRESULT startProxy() = 0;
+    virtual void  stopProxy() = 0;
+
+    struct Msg {
+        MsgType mType;
+        WasapiProxy *mProxy;
+        const char *mParam;
+        std::promise<HRESULT> mPromise;
+
+        explicit operator bool() const noexcept { return mType != MsgType::QuitThread; }
+    };
+    static std::thread sThread;
+    static std::deque<Msg> mMsgQueue;
+    static std::mutex mMsgQueueLock;
+    static std::condition_variable mMsgQueueCond;
+    static std::mutex sThreadLock;
+    static size_t sInitCount;
+
+    std::future<HRESULT> pushMessage(MsgType type, const char *param=nullptr)
+    {
+        std::promise<HRESULT> promise;
+        std::future<HRESULT> future{promise.get_future()};
+        {
+            std::lock_guard<std::mutex> _{mMsgQueueLock};
+            mMsgQueue.emplace_back(Msg{type, this, param, std::move(promise)});
+        }
+        mMsgQueueCond.notify_one();
+        return future;
+    }
+
+    static std::future<HRESULT> pushMessageStatic(MsgType type)
+    {
+        std::promise<HRESULT> promise;
+        std::future<HRESULT> future{promise.get_future()};
+        {
+            std::lock_guard<std::mutex> _{mMsgQueueLock};
+            mMsgQueue.emplace_back(Msg{type, nullptr, nullptr, std::move(promise)});
+        }
+        mMsgQueueCond.notify_one();
+        return future;
+    }
+
+    static Msg popMessage()
+    {
+        std::unique_lock<std::mutex> lock{mMsgQueueLock};
+        mMsgQueueCond.wait(lock, []{return !mMsgQueue.empty();});
+        Msg msg{std::move(mMsgQueue.front())};
+        mMsgQueue.pop_front();
+        return msg;
+    }
+
+    static int messageHandler(std::promise<HRESULT> *promise);
+
+    static HRESULT InitThread()
+    {
+        std::lock_guard<std::mutex> _{sThreadLock};
+        HRESULT res{S_OK};
+        if(!sThread.joinable())
+        {
+            std::promise<HRESULT> promise;
+            auto future = promise.get_future();
+
+            sThread = std::thread{&WasapiProxy::messageHandler, &promise};
+            res = future.get();
+            if(FAILED(res))
+            {
+                sThread.join();
+                return res;
+            }
+        }
+        ++sInitCount;
+        return res;
+    }
+
+    static void DeinitThread()
+    {
+        std::lock_guard<std::mutex> _{sThreadLock};
+        if(!--sInitCount && sThread.joinable())
+        {
+            pushMessageStatic(MsgType::QuitThread);
+            sThread.join();
+        }
+    }
+};
+std::thread WasapiProxy::sThread;
+std::deque<WasapiProxy::Msg> WasapiProxy::mMsgQueue;
+std::mutex WasapiProxy::mMsgQueueLock;
+std::condition_variable WasapiProxy::mMsgQueueCond;
+std::mutex WasapiProxy::sThreadLock;
+size_t WasapiProxy::sInitCount{0};
+
+int WasapiProxy::messageHandler(std::promise<HRESULT> *promise)
+{
+    TRACE("Starting message thread\n");
+
+    HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)};
+    if(FAILED(hr))
+    {
+        WARN("Failed to initialize COM: 0x%08lx\n", hr);
+        promise->set_value(hr);
+        return 0;
+    }
+    promise->set_value(S_OK);
+    promise = nullptr;
+
+    TRACE("Starting message loop\n");
+    while(Msg msg{popMessage()})
+    {
+        TRACE("Got message \"%s\" (0x%04x, this=%p, param=%p)\n",
+            MessageStr[static_cast<size_t>(msg.mType)], static_cast<uint>(msg.mType),
+            static_cast<void*>(msg.mProxy), static_cast<const void*>(msg.mParam));
+
+        switch(msg.mType)
+        {
+        case MsgType::OpenDevice:
+            hr = msg.mProxy->openProxy(msg.mParam);
+            msg.mPromise.set_value(hr);
+            continue;
+
+        case MsgType::ResetDevice:
+            hr = msg.mProxy->resetProxy();
+            msg.mPromise.set_value(hr);
+            continue;
+
+        case MsgType::StartDevice:
+            hr = msg.mProxy->startProxy();
+            msg.mPromise.set_value(hr);
+            continue;
+
+        case MsgType::StopDevice:
+            msg.mProxy->stopProxy();
+            msg.mPromise.set_value(S_OK);
+            continue;
+
+        case MsgType::CloseDevice:
+            msg.mProxy->closeProxy();
+            msg.mPromise.set_value(S_OK);
+            continue;
+
+        case MsgType::EnumeratePlayback:
+        case MsgType::EnumerateCapture:
+            {
+                void *ptr{};
+                hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER,
+                    IID_IMMDeviceEnumerator, &ptr);
+                if(FAILED(hr))
+                    msg.mPromise.set_value(hr);
+                else
+                {
+                    ComPtr<IMMDeviceEnumerator> devenum{static_cast<IMMDeviceEnumerator*>(ptr)};
+
+                    if(msg.mType == MsgType::EnumeratePlayback)
+                        probe_devices(devenum.get(), eRender, PlaybackDevices);
+                    else if(msg.mType == MsgType::EnumerateCapture)
+                        probe_devices(devenum.get(), eCapture, CaptureDevices);
+                    msg.mPromise.set_value(S_OK);
+                }
+                continue;
+            }
+
+        case MsgType::QuitThread:
+            break;
+        }
+        ERR("Unexpected message: %u\n", static_cast<uint>(msg.mType));
+        msg.mPromise.set_value(E_FAIL);
+    }
+    TRACE("Message loop finished\n");
+    CoUninitialize();
+
+    return 0;
+}
+
+
+struct WasapiPlayback final : public BackendBase, WasapiProxy {
+    WasapiPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~WasapiPlayback() override;
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    HRESULT openProxy(const char *name) override;
+    void closeProxy() override;
+
+    bool reset() override;
+    HRESULT resetProxy() override;
+    void start() override;
+    HRESULT startProxy() override;
+    void stop() override;
+    void stopProxy() override;
+
+    ClockLatency getClockLatency() override;
+
+    HRESULT mOpenStatus{E_FAIL};
+    ComPtr<IMMDevice> mMMDev{nullptr};
+    ComPtr<IAudioClient> mClient{nullptr};
+    ComPtr<IAudioRenderClient> mRender{nullptr};
+    HANDLE mNotifyEvent{nullptr};
+
+    UINT32 mOrigBufferSize{}, mOrigUpdateSize{};
+    std::unique_ptr<char[]> mResampleBuffer{};
+    uint mBufferFilled{0};
+    SampleConverterPtr mResampler;
+
+    WAVEFORMATEXTENSIBLE mFormat{};
+    std::atomic<UINT32> mPadding{0u};
+
+    std::mutex mMutex;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(WasapiPlayback)
+};
+
+WasapiPlayback::~WasapiPlayback()
+{
+    if(SUCCEEDED(mOpenStatus))
+    {
+        pushMessage(MsgType::CloseDevice).wait();
+        DeinitThread();
+    }
+    mOpenStatus = E_FAIL;
+
+    if(mNotifyEvent != nullptr)
+        CloseHandle(mNotifyEvent);
+    mNotifyEvent = nullptr;
+}
+
+
+FORCE_ALIGN int WasapiPlayback::mixerProc()
+{
+    HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)};
+    if(FAILED(hr))
+    {
+        ERR("CoInitializeEx(nullptr, COINIT_MULTITHREADED) failed: 0x%08lx\n", hr);
+        mDevice->handleDisconnect("COM init failed: 0x%08lx", hr);
+        return 1;
+    }
+
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    const uint frame_size{mFormat.Format.nChannels * mFormat.Format.wBitsPerSample / 8u};
+    const uint update_size{mOrigUpdateSize};
+    const UINT32 buffer_len{mOrigBufferSize};
+    while(!mKillNow.load(std::memory_order_relaxed))
+    {
+        UINT32 written;
+        hr = mClient->GetCurrentPadding(&written);
+        if(FAILED(hr))
+        {
+            ERR("Failed to get padding: 0x%08lx\n", hr);
+            mDevice->handleDisconnect("Failed to retrieve buffer padding: 0x%08lx", hr);
+            break;
+        }
+        mPadding.store(written, std::memory_order_relaxed);
+
+        uint len{buffer_len - written};
+        if(len < update_size)
+        {
+            DWORD res{WaitForSingleObjectEx(mNotifyEvent, 2000, FALSE)};
+            if(res != WAIT_OBJECT_0)
+                ERR("WaitForSingleObjectEx error: 0x%lx\n", res);
+            continue;
+        }
+
+        BYTE *buffer;
+        hr = mRender->GetBuffer(len, &buffer);
+        if(SUCCEEDED(hr))
+        {
+            if(mResampler)
+            {
+                std::lock_guard<std::mutex> _{mMutex};
+                for(UINT32 done{0};done < len;)
+                {
+                    if(mBufferFilled == 0)
+                    {
+                        mDevice->renderSamples(mResampleBuffer.get(), mDevice->UpdateSize,
+                            mFormat.Format.nChannels);
+                        mBufferFilled = mDevice->UpdateSize;
+                    }
+
+                    const void *src{mResampleBuffer.get()};
+                    uint srclen{mBufferFilled};
+                    uint got{mResampler->convert(&src, &srclen, buffer, len-done)};
+                    buffer += got*frame_size;
+                    done += got;
+
+                    mPadding.store(written + done, std::memory_order_relaxed);
+                    if(srclen)
+                    {
+                        const char *bsrc{static_cast<const char*>(src)};
+                        std::copy(bsrc, bsrc + srclen*frame_size, mResampleBuffer.get());
+                    }
+                    mBufferFilled = srclen;
+                }
+            }
+            else
+            {
+                std::lock_guard<std::mutex> _{mMutex};
+                mDevice->renderSamples(buffer, len, mFormat.Format.nChannels);
+                mPadding.store(written + len, std::memory_order_relaxed);
+            }
+            hr = mRender->ReleaseBuffer(len, 0);
+        }
+        if(FAILED(hr))
+        {
+            ERR("Failed to buffer data: 0x%08lx\n", hr);
+            mDevice->handleDisconnect("Failed to send playback samples: 0x%08lx", hr);
+            break;
+        }
+    }
+    mPadding.store(0u, std::memory_order_release);
+
+    CoUninitialize();
+    return 0;
+}
+
+
+void WasapiPlayback::open(const char *name)
+{
+    if(SUCCEEDED(mOpenStatus))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Unexpected duplicate open call"};
+
+    mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+    if(mNotifyEvent == nullptr)
+    {
+        ERR("Failed to create notify events: %lu\n", GetLastError());
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to create notify events"};
+    }
+
+    HRESULT hr{InitThread()};
+    if(FAILED(hr))
+    {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to init COM thread: 0x%08lx", hr};
+    }
+
+    if(name)
+    {
+        if(PlaybackDevices.empty())
+            pushMessage(MsgType::EnumeratePlayback);
+        if(std::strncmp(name, DevNameHead, DevNameHeadLen) == 0)
+        {
+            name += DevNameHeadLen;
+            if(*name == '\0')
+                name = nullptr;
+        }
+    }
+
+    mOpenStatus = pushMessage(MsgType::OpenDevice, name).get();
+    if(FAILED(mOpenStatus))
+    {
+        DeinitThread();
+        throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx",
+            mOpenStatus};
+    }
+}
+
+HRESULT WasapiPlayback::openProxy(const char *name)
+{
+    const wchar_t *devid{nullptr};
+    if(name)
+    {
+        auto iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+            [name](const DevMap &entry) -> bool
+            { return entry.name == name || entry.endpoint_guid == name; });
+        if(iter == PlaybackDevices.cend())
+        {
+            const std::wstring wname{utf8_to_wstr(name)};
+            iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+                [&wname](const DevMap &entry) -> bool
+                { return entry.devid == wname; });
+        }
+        if(iter == PlaybackDevices.cend())
+        {
+            WARN("Failed to find device name matching \"%s\"\n", name);
+            return E_FAIL;
+        }
+        name = iter->name.c_str();
+        devid = iter->devid.c_str();
+    }
+
+    void *ptr;
+    ComPtr<IMMDevice> mmdev;
+    HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER,
+        IID_IMMDeviceEnumerator, &ptr)};
+    if(SUCCEEDED(hr))
+    {
+        ComPtr<IMMDeviceEnumerator> enumerator{static_cast<IMMDeviceEnumerator*>(ptr)};
+        if(!devid)
+            hr = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, mmdev.getPtr());
+        else
+            hr = enumerator->GetDevice(devid, mmdev.getPtr());
+    }
+    if(FAILED(hr))
+    {
+        WARN("Failed to open device \"%s\"\n", name?name:"(default)");
+        return hr;
+    }
+
+    mClient = nullptr;
+    mMMDev = std::move(mmdev);
+    if(name) mDevice->DeviceName = std::string{DevNameHead} + name;
+    else mDevice->DeviceName = DevNameHead + get_device_name_and_guid(mMMDev.get()).first;
+
+    return hr;
+}
+
+void WasapiPlayback::closeProxy()
+{
+    mClient = nullptr;
+    mMMDev = nullptr;
+}
+
+
+bool WasapiPlayback::reset()
+{
+    HRESULT hr{pushMessage(MsgType::ResetDevice).get()};
+    if(FAILED(hr))
+        throw al::backend_exception{al::backend_error::DeviceError, "0x%08lx", hr};
+    return true;
+}
+
+HRESULT WasapiPlayback::resetProxy()
+{
+    mClient = nullptr;
+
+    void *ptr;
+    HRESULT hr{mMMDev->Activate(IID_IAudioClient, CLSCTX_INPROC_SERVER, nullptr, &ptr)};
+    if(FAILED(hr))
+    {
+        ERR("Failed to reactivate audio client: 0x%08lx\n", hr);
+        return hr;
+    }
+    mClient = ComPtr<IAudioClient>{static_cast<IAudioClient*>(ptr)};
+
+    WAVEFORMATEX *wfx;
+    hr = mClient->GetMixFormat(&wfx);
+    if(FAILED(hr))
+    {
+        ERR("Failed to get mix format: 0x%08lx\n", hr);
+        return hr;
+    }
+    TraceFormat("Device mix format", wfx);
+
+    WAVEFORMATEXTENSIBLE OutputType;
+    if(!MakeExtensible(&OutputType, wfx))
+    {
+        CoTaskMemFree(wfx);
+        return E_FAIL;
+    }
+    CoTaskMemFree(wfx);
+    wfx = nullptr;
+
+    const ReferenceTime per_time{ReferenceTime{seconds{mDevice->UpdateSize}} / mDevice->Frequency};
+    const ReferenceTime buf_time{ReferenceTime{seconds{mDevice->BufferSize}} / mDevice->Frequency};
+    bool isRear51{false};
+
+    if(!mDevice->Flags.test(FrequencyRequest))
+        mDevice->Frequency = OutputType.Format.nSamplesPerSec;
+    if(!mDevice->Flags.test(ChannelsRequest))
+    {
+        /* If not requesting a channel configuration, auto-select given what
+         * fits the mask's lsb (to ensure no gaps in the output channels). If
+         * there's no mask, we can only assume mono or stereo.
+         */
+        const uint32_t chancount{OutputType.Format.nChannels};
+        const DWORD chanmask{OutputType.dwChannelMask};
+        if(chancount >= 12 && (chanmask&X714Mask) == X7DOT1DOT4)
+            mDevice->FmtChans = DevFmtX71;
+        else if(chancount >= 8 && (chanmask&X71Mask) == X7DOT1)
+            mDevice->FmtChans = DevFmtX71;
+        else if(chancount >= 7 && (chanmask&X61Mask) == X6DOT1)
+            mDevice->FmtChans = DevFmtX61;
+        else if(chancount >= 6 && (chanmask&X51Mask) == X5DOT1)
+            mDevice->FmtChans = DevFmtX51;
+        else if(chancount >= 6 && (chanmask&X51RearMask) == X5DOT1REAR)
+        {
+            mDevice->FmtChans = DevFmtX51;
+            isRear51 = true;
+        }
+        else if(chancount >= 4 && (chanmask&QuadMask) == QUAD)
+            mDevice->FmtChans = DevFmtQuad;
+        else if(chancount >= 2 && ((chanmask&StereoMask) == STEREO || !chanmask))
+            mDevice->FmtChans = DevFmtStereo;
+        else if(chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask))
+            mDevice->FmtChans = DevFmtMono;
+        else
+            ERR("Unhandled channel config: %d -- 0x%08lx\n", chancount, chanmask);
+    }
+    else
+    {
+        const uint32_t chancount{OutputType.Format.nChannels};
+        const DWORD chanmask{OutputType.dwChannelMask};
+        isRear51 = (chancount == 6 && (chanmask&X51RearMask) == X5DOT1REAR);
+    }
+
+    OutputType.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+        OutputType.Format.nChannels = 1;
+        OutputType.dwChannelMask = MONO;
+        break;
+    case DevFmtAmbi3D:
+        mDevice->FmtChans = DevFmtStereo;
+        /*fall-through*/
+    case DevFmtStereo:
+        OutputType.Format.nChannels = 2;
+        OutputType.dwChannelMask = STEREO;
+        break;
+    case DevFmtQuad:
+        OutputType.Format.nChannels = 4;
+        OutputType.dwChannelMask = QUAD;
+        break;
+    case DevFmtX51:
+        OutputType.Format.nChannels = 6;
+        OutputType.dwChannelMask = isRear51 ? X5DOT1REAR : X5DOT1;
+        break;
+    case DevFmtX61:
+        OutputType.Format.nChannels = 7;
+        OutputType.dwChannelMask = X6DOT1;
+        break;
+    case DevFmtX71:
+    case DevFmtX3D71:
+        OutputType.Format.nChannels = 8;
+        OutputType.dwChannelMask = X7DOT1;
+        break;
+    case DevFmtX714:
+        OutputType.Format.nChannels = 12;
+        OutputType.dwChannelMask = X7DOT1DOT4;
+        break;
+    }
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        mDevice->FmtType = DevFmtUByte;
+        /* fall-through */
+    case DevFmtUByte:
+        OutputType.Format.wBitsPerSample = 8;
+        OutputType.Samples.wValidBitsPerSample = 8;
+        OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+        break;
+    case DevFmtUShort:
+        mDevice->FmtType = DevFmtShort;
+        /* fall-through */
+    case DevFmtShort:
+        OutputType.Format.wBitsPerSample = 16;
+        OutputType.Samples.wValidBitsPerSample = 16;
+        OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+        break;
+    case DevFmtUInt:
+        mDevice->FmtType = DevFmtInt;
+        /* fall-through */
+    case DevFmtInt:
+        OutputType.Format.wBitsPerSample = 32;
+        OutputType.Samples.wValidBitsPerSample = 32;
+        OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+        break;
+    case DevFmtFloat:
+        OutputType.Format.wBitsPerSample = 32;
+        OutputType.Samples.wValidBitsPerSample = 32;
+        OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+        break;
+    }
+    OutputType.Format.nSamplesPerSec = mDevice->Frequency;
+
+    OutputType.Format.nBlockAlign = static_cast<WORD>(OutputType.Format.nChannels *
+        OutputType.Format.wBitsPerSample / 8);
+    OutputType.Format.nAvgBytesPerSec = OutputType.Format.nSamplesPerSec *
+        OutputType.Format.nBlockAlign;
+
+    TraceFormat("Requesting playback format", &OutputType.Format);
+    hr = mClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &OutputType.Format, &wfx);
+    if(FAILED(hr))
+    {
+        WARN("Failed to check format support: 0x%08lx\n", hr);
+        hr = mClient->GetMixFormat(&wfx);
+    }
+    if(FAILED(hr))
+    {
+        ERR("Failed to find a supported format: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    if(wfx != nullptr)
+    {
+        TraceFormat("Got playback format", wfx);
+        if(!MakeExtensible(&OutputType, wfx))
+        {
+            CoTaskMemFree(wfx);
+            return E_FAIL;
+        }
+        CoTaskMemFree(wfx);
+        wfx = nullptr;
+
+        if(!GetConfigValueBool(mDevice->DeviceName.c_str(), "wasapi", "allow-resampler", true))
+            mDevice->Frequency = OutputType.Format.nSamplesPerSec;
+        else
+            mDevice->Frequency = minu(mDevice->Frequency, OutputType.Format.nSamplesPerSec);
+
+        const uint32_t chancount{OutputType.Format.nChannels};
+        const DWORD chanmask{OutputType.dwChannelMask};
+        /* Don't update the channel format if the requested format fits what's
+         * supported.
+         */
+        bool chansok{false};
+        if(mDevice->Flags.test(ChannelsRequest))
+        {
+            /* When requesting a channel configuration, make sure it fits the
+             * mask's lsb (to ensure no gaps in the output channels). If
+             * there's no mask, assume the request fits with enough channels.
+             */
+            switch(mDevice->FmtChans)
+            {
+            case DevFmtMono:
+                chansok = (chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask));
+                break;
+            case DevFmtStereo:
+                chansok = (chancount >= 2 && ((chanmask&StereoMask) == STEREO || !chanmask));
+                break;
+            case DevFmtQuad:
+                chansok = (chancount >= 4 && ((chanmask&QuadMask) == QUAD || !chanmask));
+                break;
+            case DevFmtX51:
+                chansok = (chancount >= 6 && ((chanmask&X51Mask) == X5DOT1
+                        || (chanmask&X51RearMask) == X5DOT1REAR || !chanmask));
+                break;
+            case DevFmtX61:
+                chansok = (chancount >= 7 && ((chanmask&X61Mask) == X6DOT1 || !chanmask));
+                break;
+            case DevFmtX71:
+            case DevFmtX3D71:
+                chansok = (chancount >= 8 && ((chanmask&X71Mask) == X7DOT1 || !chanmask));
+                break;
+            case DevFmtX714:
+                chansok = (chancount >= 12 && ((chanmask&X714Mask) == X7DOT1DOT4 || !chanmask));
+            case DevFmtAmbi3D:
+                break;
+            }
+        }
+        if(!chansok)
+        {
+            if(chancount >= 12 && (chanmask&X714Mask) == X7DOT1DOT4)
+                mDevice->FmtChans = DevFmtX714;
+            else if(chancount >= 8 && (chanmask&X71Mask) == X7DOT1)
+                mDevice->FmtChans = DevFmtX71;
+            else if(chancount >= 7 && (chanmask&X61Mask) == X6DOT1)
+                mDevice->FmtChans = DevFmtX61;
+            else if(chancount >= 6 && ((chanmask&X51Mask) == X5DOT1
+                || (chanmask&X51RearMask) == X5DOT1REAR))
+                mDevice->FmtChans = DevFmtX51;
+            else if(chancount >= 4 && (chanmask&QuadMask) == QUAD)
+                mDevice->FmtChans = DevFmtQuad;
+            else if(chancount >= 2 && ((chanmask&StereoMask) == STEREO || !chanmask))
+                mDevice->FmtChans = DevFmtStereo;
+            else if(chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask))
+                mDevice->FmtChans = DevFmtMono;
+            else
+            {
+                ERR("Unhandled extensible channels: %d -- 0x%08lx\n", OutputType.Format.nChannels,
+                    OutputType.dwChannelMask);
+                mDevice->FmtChans = DevFmtStereo;
+                OutputType.Format.nChannels = 2;
+                OutputType.dwChannelMask = STEREO;
+            }
+        }
+
+        if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_PCM))
+        {
+            if(OutputType.Format.wBitsPerSample == 8)
+                mDevice->FmtType = DevFmtUByte;
+            else if(OutputType.Format.wBitsPerSample == 16)
+                mDevice->FmtType = DevFmtShort;
+            else if(OutputType.Format.wBitsPerSample == 32)
+                mDevice->FmtType = DevFmtInt;
+            else
+            {
+                mDevice->FmtType = DevFmtShort;
+                OutputType.Format.wBitsPerSample = 16;
+            }
+        }
+        else if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
+        {
+            mDevice->FmtType = DevFmtFloat;
+            OutputType.Format.wBitsPerSample = 32;
+        }
+        else
+        {
+            ERR("Unhandled format sub-type: %s\n", GuidPrinter{OutputType.SubFormat}.c_str());
+            mDevice->FmtType = DevFmtShort;
+            if(OutputType.Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE)
+                OutputType.Format.wFormatTag = WAVE_FORMAT_PCM;
+            OutputType.Format.wBitsPerSample = 16;
+            OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+        }
+        OutputType.Samples.wValidBitsPerSample = OutputType.Format.wBitsPerSample;
+    }
+    mFormat = OutputType;
+
+    const EndpointFormFactor formfactor{get_device_formfactor(mMMDev.get())};
+    mDevice->Flags.set(DirectEar, (formfactor == Headphones || formfactor == Headset));
+
+    setDefaultWFXChannelOrder();
+
+    hr = mClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
+        buf_time.count(), 0, &OutputType.Format, nullptr);
+    if(FAILED(hr))
+    {
+        ERR("Failed to initialize audio client: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    UINT32 buffer_len{};
+    ReferenceTime min_per{};
+    hr = mClient->GetDevicePeriod(&reinterpret_cast<REFERENCE_TIME&>(min_per), nullptr);
+    if(SUCCEEDED(hr))
+        hr = mClient->GetBufferSize(&buffer_len);
+    if(FAILED(hr))
+    {
+        ERR("Failed to get audio buffer info: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    /* Find the nearest multiple of the period size to the update size */
+    if(min_per < per_time)
+        min_per *= maxi64((per_time + min_per/2) / min_per, 1);
+
+    mOrigBufferSize = buffer_len;
+    mOrigUpdateSize = minu(RefTime2Samples(min_per, mFormat.Format.nSamplesPerSec), buffer_len/2);
+
+    mDevice->BufferSize = static_cast<uint>(uint64_t{buffer_len} * mDevice->Frequency /
+        mFormat.Format.nSamplesPerSec);
+    mDevice->UpdateSize = minu(RefTime2Samples(min_per, mDevice->Frequency),
+        mDevice->BufferSize/2);
+
+    mResampler = nullptr;
+    mResampleBuffer = nullptr;
+    mBufferFilled = 0;
+    if(mDevice->Frequency != mFormat.Format.nSamplesPerSec)
+    {
+        mResampler = SampleConverter::Create(mDevice->FmtType, mDevice->FmtType,
+            mFormat.Format.nChannels, mDevice->Frequency, mFormat.Format.nSamplesPerSec,
+            Resampler::FastBSinc24);
+        mResampleBuffer = std::make_unique<char[]>(size_t{mDevice->UpdateSize} *
+            mFormat.Format.nChannels * mFormat.Format.wBitsPerSample / 8);
+
+        TRACE("Created converter for %s/%s format, dst: %luhz (%u), src: %uhz (%u)\n",
+            DevFmtChannelsString(mDevice->FmtChans), DevFmtTypeString(mDevice->FmtType),
+            mFormat.Format.nSamplesPerSec, mOrigUpdateSize, mDevice->Frequency,
+            mDevice->UpdateSize);
+    }
+
+    hr = mClient->SetEventHandle(mNotifyEvent);
+    if(FAILED(hr))
+    {
+        ERR("Failed to set event handle: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    return hr;
+}
+
+
+void WasapiPlayback::start()
+{
+    const HRESULT hr{pushMessage(MsgType::StartDevice).get()};
+    if(FAILED(hr))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start playback: 0x%lx", hr};
+}
+
+HRESULT WasapiPlayback::startProxy()
+{
+    ResetEvent(mNotifyEvent);
+
+    HRESULT hr{mClient->Start()};
+    if(FAILED(hr))
+    {
+        ERR("Failed to start audio client: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    void *ptr;
+    hr = mClient->GetService(IID_IAudioRenderClient, &ptr);
+    if(SUCCEEDED(hr))
+    {
+        mRender = ComPtr<IAudioRenderClient>{static_cast<IAudioRenderClient*>(ptr)};
+        try {
+            mKillNow.store(false, std::memory_order_release);
+            mThread = std::thread{std::mem_fn(&WasapiPlayback::mixerProc), this};
+        }
+        catch(...) {
+            mRender = nullptr;
+            ERR("Failed to start thread\n");
+            hr = E_FAIL;
+        }
+    }
+
+    if(FAILED(hr))
+        mClient->Stop();
+
+    return hr;
+}
+
+
+void WasapiPlayback::stop()
+{ pushMessage(MsgType::StopDevice).wait(); }
+
+void WasapiPlayback::stopProxy()
+{
+    if(!mRender || !mThread.joinable())
+        return;
+
+    mKillNow.store(true, std::memory_order_release);
+    mThread.join();
+
+    mRender = nullptr;
+    mClient->Stop();
+}
+
+
+ClockLatency WasapiPlayback::getClockLatency()
+{
+    ClockLatency ret;
+
+    std::lock_guard<std::mutex> _{mMutex};
+    ret.ClockTime = GetDeviceClockTime(mDevice);
+    ret.Latency  = seconds{mPadding.load(std::memory_order_relaxed)};
+    ret.Latency /= mFormat.Format.nSamplesPerSec;
+    if(mResampler)
+    {
+        auto extra = mResampler->currentInputDelay();
+        ret.Latency += std::chrono::duration_cast<nanoseconds>(extra) / mDevice->Frequency;
+        ret.Latency += nanoseconds{seconds{mBufferFilled}} / mDevice->Frequency;
+    }
+
+    return ret;
+}
+
+
+struct WasapiCapture final : public BackendBase, WasapiProxy {
+    WasapiCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~WasapiCapture() override;
+
+    int recordProc();
+
+    void open(const char *name) override;
+    HRESULT openProxy(const char *name) override;
+    void closeProxy() override;
+
+    HRESULT resetProxy() override;
+    void start() override;
+    HRESULT startProxy() override;
+    void stop() override;
+    void stopProxy() override;
+
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    HRESULT mOpenStatus{E_FAIL};
+    ComPtr<IMMDevice> mMMDev{nullptr};
+    ComPtr<IAudioClient> mClient{nullptr};
+    ComPtr<IAudioCaptureClient> mCapture{nullptr};
+    HANDLE mNotifyEvent{nullptr};
+
+    ChannelConverter mChannelConv{};
+    SampleConverterPtr mSampleConv;
+    RingBufferPtr mRing;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(WasapiCapture)
+};
+
+WasapiCapture::~WasapiCapture()
+{
+    if(SUCCEEDED(mOpenStatus))
+    {
+        pushMessage(MsgType::CloseDevice).wait();
+        DeinitThread();
+    }
+    mOpenStatus = E_FAIL;
+
+    if(mNotifyEvent != nullptr)
+        CloseHandle(mNotifyEvent);
+    mNotifyEvent = nullptr;
+}
+
+
+FORCE_ALIGN int WasapiCapture::recordProc()
+{
+    HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)};
+    if(FAILED(hr))
+    {
+        ERR("CoInitializeEx(nullptr, COINIT_MULTITHREADED) failed: 0x%08lx\n", hr);
+        mDevice->handleDisconnect("COM init failed: 0x%08lx", hr);
+        return 1;
+    }
+
+    althrd_setname(RECORD_THREAD_NAME);
+
+    al::vector<float> samples;
+    while(!mKillNow.load(std::memory_order_relaxed))
+    {
+        UINT32 avail;
+        hr = mCapture->GetNextPacketSize(&avail);
+        if(FAILED(hr))
+            ERR("Failed to get next packet size: 0x%08lx\n", hr);
+        else if(avail > 0)
+        {
+            UINT32 numsamples;
+            DWORD flags;
+            BYTE *rdata;
+
+            hr = mCapture->GetBuffer(&rdata, &numsamples, &flags, nullptr, nullptr);
+            if(FAILED(hr))
+                ERR("Failed to get capture buffer: 0x%08lx\n", hr);
+            else
+            {
+                if(mChannelConv.is_active())
+                {
+                    samples.resize(numsamples*2);
+                    mChannelConv.convert(rdata, samples.data(), numsamples);
+                    rdata = reinterpret_cast<BYTE*>(samples.data());
+                }
+
+                auto data = mRing->getWriteVector();
+
+                size_t dstframes;
+                if(mSampleConv)
+                {
+                    const void *srcdata{rdata};
+                    uint srcframes{numsamples};
+
+                    dstframes = mSampleConv->convert(&srcdata, &srcframes, data.first.buf,
+                        static_cast<uint>(minz(data.first.len, INT_MAX)));
+                    if(srcframes > 0 && dstframes == data.first.len && data.second.len > 0)
+                    {
+                        /* If some source samples remain, all of the first dest
+                         * block was filled, and there's space in the second
+                         * dest block, do another run for the second block.
+                         */
+                        dstframes += mSampleConv->convert(&srcdata, &srcframes, data.second.buf,
+                            static_cast<uint>(minz(data.second.len, INT_MAX)));
+                    }
+                }
+                else
+                {
+                    const uint framesize{mDevice->frameSizeFromFmt()};
+                    size_t len1{minz(data.first.len, numsamples)};
+                    size_t len2{minz(data.second.len, numsamples-len1)};
+
+                    memcpy(data.first.buf, rdata, len1*framesize);
+                    if(len2 > 0)
+                        memcpy(data.second.buf, rdata+len1*framesize, len2*framesize);
+                    dstframes = len1 + len2;
+                }
+
+                mRing->writeAdvance(dstframes);
+
+                hr = mCapture->ReleaseBuffer(numsamples);
+                if(FAILED(hr)) ERR("Failed to release capture buffer: 0x%08lx\n", hr);
+            }
+        }
+
+        if(FAILED(hr))
+        {
+            mDevice->handleDisconnect("Failed to capture samples: 0x%08lx", hr);
+            break;
+        }
+
+        DWORD res{WaitForSingleObjectEx(mNotifyEvent, 2000, FALSE)};
+        if(res != WAIT_OBJECT_0)
+            ERR("WaitForSingleObjectEx error: 0x%lx\n", res);
+    }
+
+    CoUninitialize();
+    return 0;
+}
+
+
+void WasapiCapture::open(const char *name)
+{
+    if(SUCCEEDED(mOpenStatus))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Unexpected duplicate open call"};
+
+    mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+    if(mNotifyEvent == nullptr)
+    {
+        ERR("Failed to create notify events: %lu\n", GetLastError());
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to create notify events"};
+    }
+
+    HRESULT hr{InitThread()};
+    if(FAILED(hr))
+    {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to init COM thread: 0x%08lx", hr};
+    }
+
+    if(name)
+    {
+        if(CaptureDevices.empty())
+            pushMessage(MsgType::EnumerateCapture);
+        if(std::strncmp(name, DevNameHead, DevNameHeadLen) == 0)
+        {
+            name += DevNameHeadLen;
+            if(*name == '\0')
+                name = nullptr;
+        }
+    }
+
+    mOpenStatus = pushMessage(MsgType::OpenDevice, name).get();
+    if(FAILED(mOpenStatus))
+    {
+        DeinitThread();
+        throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx",
+            mOpenStatus};
+    }
+
+    hr = pushMessage(MsgType::ResetDevice).get();
+    if(FAILED(hr))
+    {
+        if(hr == E_OUTOFMEMORY)
+            throw al::backend_exception{al::backend_error::OutOfMemory, "Out of memory"};
+        throw al::backend_exception{al::backend_error::DeviceError, "Device reset failed"};
+    }
+}
+
+HRESULT WasapiCapture::openProxy(const char *name)
+{
+    const wchar_t *devid{nullptr};
+    if(name)
+    {
+        auto iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+            [name](const DevMap &entry) -> bool
+            { return entry.name == name || entry.endpoint_guid == name; });
+        if(iter == CaptureDevices.cend())
+        {
+            const std::wstring wname{utf8_to_wstr(name)};
+            iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+                [&wname](const DevMap &entry) -> bool
+                { return entry.devid == wname; });
+        }
+        if(iter == CaptureDevices.cend())
+        {
+            WARN("Failed to find device name matching \"%s\"\n", name);
+            return E_FAIL;
+        }
+        name = iter->name.c_str();
+        devid = iter->devid.c_str();
+    }
+
+    void *ptr;
+    HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER,
+        IID_IMMDeviceEnumerator, &ptr)};
+    if(SUCCEEDED(hr))
+    {
+        ComPtr<IMMDeviceEnumerator> enumerator{static_cast<IMMDeviceEnumerator*>(ptr)};
+        if(!devid)
+            hr = enumerator->GetDefaultAudioEndpoint(eCapture, eMultimedia, mMMDev.getPtr());
+        else
+            hr = enumerator->GetDevice(devid, mMMDev.getPtr());
+    }
+    if(FAILED(hr))
+    {
+        WARN("Failed to open device \"%s\"\n", name?name:"(default)");
+        return hr;
+    }
+
+    mClient = nullptr;
+    if(name) mDevice->DeviceName = std::string{DevNameHead} + name;
+    else mDevice->DeviceName = DevNameHead + get_device_name_and_guid(mMMDev.get()).first;
+
+    return hr;
+}
+
+void WasapiCapture::closeProxy()
+{
+    mClient = nullptr;
+    mMMDev = nullptr;
+}
+
+HRESULT WasapiCapture::resetProxy()
+{
+    mClient = nullptr;
+
+    void *ptr;
+    HRESULT hr{mMMDev->Activate(IID_IAudioClient, CLSCTX_INPROC_SERVER, nullptr, &ptr)};
+    if(FAILED(hr))
+    {
+        ERR("Failed to reactivate audio client: 0x%08lx\n", hr);
+        return hr;
+    }
+    mClient = ComPtr<IAudioClient>{static_cast<IAudioClient*>(ptr)};
+
+    WAVEFORMATEX *wfx;
+    hr = mClient->GetMixFormat(&wfx);
+    if(FAILED(hr))
+    {
+        ERR("Failed to get capture format: 0x%08lx\n", hr);
+        return hr;
+    }
+    TraceFormat("Device capture format", wfx);
+
+    WAVEFORMATEXTENSIBLE InputType{};
+    if(!MakeExtensible(&InputType, wfx))
+    {
+        CoTaskMemFree(wfx);
+        return E_FAIL;
+    }
+    CoTaskMemFree(wfx);
+    wfx = nullptr;
+
+    const bool isRear51{InputType.Format.nChannels == 6
+        && (InputType.dwChannelMask&X51RearMask) == X5DOT1REAR};
+
+    // Make sure buffer is at least 100ms in size
+    ReferenceTime buf_time{ReferenceTime{seconds{mDevice->BufferSize}} / mDevice->Frequency};
+    buf_time = std::max(buf_time, ReferenceTime{milliseconds{100}});
+
+    InputType = {};
+    InputType.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+        InputType.Format.nChannels = 1;
+        InputType.dwChannelMask = MONO;
+        break;
+    case DevFmtStereo:
+        InputType.Format.nChannels = 2;
+        InputType.dwChannelMask = STEREO;
+        break;
+    case DevFmtQuad:
+        InputType.Format.nChannels = 4;
+        InputType.dwChannelMask = QUAD;
+        break;
+    case DevFmtX51:
+        InputType.Format.nChannels = 6;
+        InputType.dwChannelMask = isRear51 ? X5DOT1REAR : X5DOT1;
+        break;
+    case DevFmtX61:
+        InputType.Format.nChannels = 7;
+        InputType.dwChannelMask = X6DOT1;
+        break;
+    case DevFmtX71:
+        InputType.Format.nChannels = 8;
+        InputType.dwChannelMask = X7DOT1;
+        break;
+    case DevFmtX714:
+        InputType.Format.nChannels = 12;
+        InputType.dwChannelMask = X7DOT1DOT4;
+        break;
+
+    case DevFmtX3D71:
+    case DevFmtAmbi3D:
+        return E_FAIL;
+    }
+    switch(mDevice->FmtType)
+    {
+    /* NOTE: Signedness doesn't matter, the converter will handle it. */
+    case DevFmtByte:
+    case DevFmtUByte:
+        InputType.Format.wBitsPerSample = 8;
+        InputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+        break;
+    case DevFmtShort:
+    case DevFmtUShort:
+        InputType.Format.wBitsPerSample = 16;
+        InputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+        break;
+    case DevFmtInt:
+    case DevFmtUInt:
+        InputType.Format.wBitsPerSample = 32;
+        InputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+        break;
+    case DevFmtFloat:
+        InputType.Format.wBitsPerSample = 32;
+        InputType.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+        break;
+    }
+    InputType.Samples.wValidBitsPerSample = InputType.Format.wBitsPerSample;
+    InputType.Format.nSamplesPerSec = mDevice->Frequency;
+
+    InputType.Format.nBlockAlign = static_cast<WORD>(InputType.Format.nChannels *
+        InputType.Format.wBitsPerSample / 8);
+    InputType.Format.nAvgBytesPerSec = InputType.Format.nSamplesPerSec *
+        InputType.Format.nBlockAlign;
+    InputType.Format.cbSize = sizeof(InputType) - sizeof(InputType.Format);
+
+    TraceFormat("Requesting capture format", &InputType.Format);
+    hr = mClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &InputType.Format, &wfx);
+    if(FAILED(hr))
+    {
+        WARN("Failed to check capture format support: 0x%08lx\n", hr);
+        hr = mClient->GetMixFormat(&wfx);
+    }
+    if(FAILED(hr))
+    {
+        ERR("Failed to find a supported capture format: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    mSampleConv = nullptr;
+    mChannelConv = {};
+
+    if(wfx != nullptr)
+    {
+        TraceFormat("Got capture format", wfx);
+        if(!MakeExtensible(&InputType, wfx))
+        {
+            CoTaskMemFree(wfx);
+            return E_FAIL;
+        }
+        CoTaskMemFree(wfx);
+        wfx = nullptr;
+
+        auto validate_fmt = [](DeviceBase *device, uint32_t chancount, DWORD chanmask) noexcept
+            -> bool
+        {
+            switch(device->FmtChans)
+            {
+            /* If the device wants mono, we can handle any input. */
+            case DevFmtMono:
+                return true;
+            /* If the device wants stereo, we can handle mono or stereo input. */
+            case DevFmtStereo:
+                return (chancount == 2 && (chanmask == 0 || (chanmask&StereoMask) == STEREO))
+                    || (chancount == 1 && (chanmask&MonoMask) == MONO);
+            /* Otherwise, the device must match the input type. */
+            case DevFmtQuad:
+                return (chancount == 4 && (chanmask == 0 || (chanmask&QuadMask) == QUAD));
+            /* 5.1 (Side) and 5.1 (Rear) are interchangeable here. */
+            case DevFmtX51:
+                return (chancount == 6 && (chanmask == 0 || (chanmask&X51Mask) == X5DOT1
+                        || (chanmask&X51RearMask) == X5DOT1REAR));
+            case DevFmtX61:
+                return (chancount == 7 && (chanmask == 0 || (chanmask&X61Mask) == X6DOT1));
+            case DevFmtX71:
+            case DevFmtX3D71:
+                return (chancount == 8 && (chanmask == 0 || (chanmask&X71Mask) == X7DOT1));
+            case DevFmtX714:
+                return (chancount == 12 && (chanmask == 0 || (chanmask&X714Mask) == X7DOT1DOT4));
+            case DevFmtAmbi3D:
+                return (chanmask == 0 && chancount == device->channelsFromFmt());
+            }
+            return false;
+        };
+        if(!validate_fmt(mDevice, InputType.Format.nChannels, InputType.dwChannelMask))
+        {
+            ERR("Failed to match format, wanted: %s %s %uhz, got: 0x%08lx mask %d channel%s %d-bit %luhz\n",
+                DevFmtChannelsString(mDevice->FmtChans), DevFmtTypeString(mDevice->FmtType),
+                mDevice->Frequency, InputType.dwChannelMask, InputType.Format.nChannels,
+                (InputType.Format.nChannels==1)?"":"s", InputType.Format.wBitsPerSample,
+                InputType.Format.nSamplesPerSec);
+            return E_FAIL;
+        }
+    }
+
+    DevFmtType srcType{};
+    if(IsEqualGUID(InputType.SubFormat, KSDATAFORMAT_SUBTYPE_PCM))
+    {
+        if(InputType.Format.wBitsPerSample == 8)
+            srcType = DevFmtUByte;
+        else if(InputType.Format.wBitsPerSample == 16)
+            srcType = DevFmtShort;
+        else if(InputType.Format.wBitsPerSample == 32)
+            srcType = DevFmtInt;
+        else
+        {
+            ERR("Unhandled integer bit depth: %d\n", InputType.Format.wBitsPerSample);
+            return E_FAIL;
+        }
+    }
+    else if(IsEqualGUID(InputType.SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
+    {
+        if(InputType.Format.wBitsPerSample == 32)
+            srcType = DevFmtFloat;
+        else
+        {
+            ERR("Unhandled float bit depth: %d\n", InputType.Format.wBitsPerSample);
+            return E_FAIL;
+        }
+    }
+    else
+    {
+        ERR("Unhandled format sub-type: %s\n", GuidPrinter{InputType.SubFormat}.c_str());
+        return E_FAIL;
+    }
+
+    if(mDevice->FmtChans == DevFmtMono && InputType.Format.nChannels != 1)
+    {
+        uint chanmask{(1u<<InputType.Format.nChannels) - 1u};
+        /* Exclude LFE from the downmix. */
+        if((InputType.dwChannelMask&SPEAKER_LOW_FREQUENCY))
+        {
+            constexpr auto lfemask = MaskFromTopBits(SPEAKER_LOW_FREQUENCY);
+            const int lfeidx{al::popcount(InputType.dwChannelMask&lfemask) - 1};
+            chanmask &= ~(1u << lfeidx);
+        }
+
+        mChannelConv = ChannelConverter{srcType, InputType.Format.nChannels, chanmask,
+            mDevice->FmtChans};
+        TRACE("Created %s multichannel-to-mono converter\n", DevFmtTypeString(srcType));
+        /* The channel converter always outputs float, so change the input type
+         * for the resampler/type-converter.
+         */
+        srcType = DevFmtFloat;
+    }
+    else if(mDevice->FmtChans == DevFmtStereo && InputType.Format.nChannels == 1)
+    {
+        mChannelConv = ChannelConverter{srcType, 1, 0x1, mDevice->FmtChans};
+        TRACE("Created %s mono-to-stereo converter\n", DevFmtTypeString(srcType));
+        srcType = DevFmtFloat;
+    }
+
+    if(mDevice->Frequency != InputType.Format.nSamplesPerSec || mDevice->FmtType != srcType)
+    {
+        mSampleConv = SampleConverter::Create(srcType, mDevice->FmtType,
+            mDevice->channelsFromFmt(), InputType.Format.nSamplesPerSec, mDevice->Frequency,
+            Resampler::FastBSinc24);
+        if(!mSampleConv)
+        {
+            ERR("Failed to create converter for %s format, dst: %s %uhz, src: %s %luhz\n",
+                DevFmtChannelsString(mDevice->FmtChans), DevFmtTypeString(mDevice->FmtType),
+                mDevice->Frequency, DevFmtTypeString(srcType), InputType.Format.nSamplesPerSec);
+            return E_FAIL;
+        }
+        TRACE("Created converter for %s format, dst: %s %uhz, src: %s %luhz\n",
+            DevFmtChannelsString(mDevice->FmtChans), DevFmtTypeString(mDevice->FmtType),
+            mDevice->Frequency, DevFmtTypeString(srcType), InputType.Format.nSamplesPerSec);
+    }
+
+    hr = mClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
+        buf_time.count(), 0, &InputType.Format, nullptr);
+    if(FAILED(hr))
+    {
+        ERR("Failed to initialize audio client: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    UINT32 buffer_len{};
+    ReferenceTime min_per{};
+    hr = mClient->GetDevicePeriod(&reinterpret_cast<REFERENCE_TIME&>(min_per), nullptr);
+    if(SUCCEEDED(hr))
+        hr = mClient->GetBufferSize(&buffer_len);
+    if(FAILED(hr))
+    {
+        ERR("Failed to get buffer size: 0x%08lx\n", hr);
+        return hr;
+    }
+    mDevice->UpdateSize = RefTime2Samples(min_per, mDevice->Frequency);
+    mDevice->BufferSize = buffer_len;
+
+    mRing = RingBuffer::Create(buffer_len, mDevice->frameSizeFromFmt(), false);
+
+    hr = mClient->SetEventHandle(mNotifyEvent);
+    if(FAILED(hr))
+    {
+        ERR("Failed to set event handle: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    return hr;
+}
+
+
+void WasapiCapture::start()
+{
+    const HRESULT hr{pushMessage(MsgType::StartDevice).get()};
+    if(FAILED(hr))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start recording: 0x%lx", hr};
+}
+
+HRESULT WasapiCapture::startProxy()
+{
+    ResetEvent(mNotifyEvent);
+
+    HRESULT hr{mClient->Start()};
+    if(FAILED(hr))
+    {
+        ERR("Failed to start audio client: 0x%08lx\n", hr);
+        return hr;
+    }
+
+    void *ptr;
+    hr = mClient->GetService(IID_IAudioCaptureClient, &ptr);
+    if(SUCCEEDED(hr))
+    {
+        mCapture = ComPtr<IAudioCaptureClient>{static_cast<IAudioCaptureClient*>(ptr)};
+        try {
+            mKillNow.store(false, std::memory_order_release);
+            mThread = std::thread{std::mem_fn(&WasapiCapture::recordProc), this};
+        }
+        catch(...) {
+            mCapture = nullptr;
+            ERR("Failed to start thread\n");
+            hr = E_FAIL;
+        }
+    }
+
+    if(FAILED(hr))
+    {
+        mClient->Stop();
+        mClient->Reset();
+    }
+
+    return hr;
+}
+
+
+void WasapiCapture::stop()
+{ pushMessage(MsgType::StopDevice).wait(); }
+
+void WasapiCapture::stopProxy()
+{
+    if(!mCapture || !mThread.joinable())
+        return;
+
+    mKillNow.store(true, std::memory_order_release);
+    mThread.join();
+
+    mCapture = nullptr;
+    mClient->Stop();
+    mClient->Reset();
+}
+
+
+void WasapiCapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, samples); }
+
+uint WasapiCapture::availableSamples()
+{ return static_cast<uint>(mRing->readSpace()); }
+
+} // namespace
+
+
+bool WasapiBackendFactory::init()
+{
+    static HRESULT InitResult{E_FAIL};
+
+    if(FAILED(InitResult)) try
+    {
+        auto res = std::async(std::launch::async, []() -> HRESULT
+        {
+            HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)};
+            if(FAILED(hr))
+            {
+                WARN("Failed to initialize COM: 0x%08lx\n", hr);
+                return hr;
+            }
+
+            void *ptr{};
+            hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER,
+                IID_IMMDeviceEnumerator, &ptr);
+            if(FAILED(hr))
+            {
+                WARN("Failed to create IMMDeviceEnumerator instance: 0x%08lx\n", hr);
+                CoUninitialize();
+                return hr;
+            }
+            static_cast<IMMDeviceEnumerator*>(ptr)->Release();
+            CoUninitialize();
+
+            return S_OK;
+        });
+
+        InitResult = res.get();
+    }
+    catch(...) {
+    }
+
+    return SUCCEEDED(InitResult);
+}
+
+bool WasapiBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+std::string WasapiBackendFactory::probe(BackendType type)
+{
+    struct ProxyControl {
+        HRESULT mResult{};
+        ProxyControl() { mResult = WasapiProxy::InitThread(); }
+        ~ProxyControl() { if(SUCCEEDED(mResult)) WasapiProxy::DeinitThread(); }
+    };
+    ProxyControl proxy;
+
+    std::string outnames;
+    if(FAILED(proxy.mResult))
+        return outnames;
+
+    switch(type)
+    {
+    case BackendType::Playback:
+        WasapiProxy::pushMessageStatic(MsgType::EnumeratePlayback).wait();
+        for(const DevMap &entry : PlaybackDevices)
+        {
+            /* +1 to also append the null char (to ensure a null-separated list
+             * and double-null terminated list).
+             */
+            outnames.append(DevNameHead).append(entry.name.c_str(), entry.name.length()+1);
+        }
+        break;
+
+    case BackendType::Capture:
+        WasapiProxy::pushMessageStatic(MsgType::EnumerateCapture).wait();
+        for(const DevMap &entry : CaptureDevices)
+            outnames.append(DevNameHead).append(entry.name.c_str(), entry.name.length()+1);
+        break;
+    }
+
+    return outnames;
+}
+
+BackendPtr WasapiBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new WasapiPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new WasapiCapture{device}};
+    return nullptr;
+}
+
+BackendFactory &WasapiBackendFactory::getFactory()
+{
+    static WasapiBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/wasapi.h b/alc/backends/wasapi.h
new file mode 100644 (file)
index 0000000..bb2671e
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_WASAPI_H
+#define BACKENDS_WASAPI_H
+
+#include "base.h"
+
+struct WasapiBackendFactory 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_WASAPI_H */
diff --git a/alc/backends/wave.cpp b/alc/backends/wave.cpp
new file mode 100644 (file)
index 0000000..1b40640
--- /dev/null
@@ -0,0 +1,407 @@
+/**
+ * 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 "wave.h"
+
+#include <algorithm>
+#include <atomic>
+#include <cerrno>
+#include <chrono>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <exception>
+#include <functional>
+#include <thread>
+
+#include "albit.h"
+#include "albyte.h"
+#include "alc/alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "opthelpers.h"
+#include "strutils.h"
+#include "threads.h"
+#include "vector.h"
+
+
+namespace {
+
+using std::chrono::seconds;
+using std::chrono::milliseconds;
+using std::chrono::nanoseconds;
+
+using ubyte = unsigned char;
+using ushort = unsigned short;
+
+constexpr char waveDevice[] = "Wave File Writer";
+
+constexpr ubyte SUBTYPE_PCM[]{
+    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa,
+    0x00, 0x38, 0x9b, 0x71
+};
+constexpr ubyte SUBTYPE_FLOAT[]{
+    0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa,
+    0x00, 0x38, 0x9b, 0x71
+};
+
+constexpr ubyte SUBTYPE_BFORMAT_PCM[]{
+    0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
+    0xca, 0x00, 0x00, 0x00
+};
+
+constexpr ubyte SUBTYPE_BFORMAT_FLOAT[]{
+    0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
+    0xca, 0x00, 0x00, 0x00
+};
+
+void fwrite16le(ushort val, FILE *f)
+{
+    ubyte data[2]{ static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff) };
+    fwrite(data, 1, 2, f);
+}
+
+void fwrite32le(uint val, FILE *f)
+{
+    ubyte data[4]{ static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff),
+        static_cast<ubyte>((val>>16)&0xff), static_cast<ubyte>((val>>24)&0xff) };
+    fwrite(data, 1, 4, f);
+}
+
+
+struct WaveBackend final : public BackendBase {
+    WaveBackend(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~WaveBackend() override;
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    FILE *mFile{nullptr};
+    long mDataStart{-1};
+
+    al::vector<al::byte> mBuffer;
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(WaveBackend)
+};
+
+WaveBackend::~WaveBackend()
+{
+    if(mFile)
+        fclose(mFile);
+    mFile = nullptr;
+}
+
+int WaveBackend::mixerProc()
+{
+    const milliseconds restTime{mDevice->UpdateSize*1000/mDevice->Frequency / 2};
+
+    althrd_setname(MIXER_THREAD_NAME);
+
+    const size_t frameStep{mDevice->channelsFromFmt()};
+    const size_t frameSize{mDevice->frameSizeFromFmt()};
+
+    int64_t done{0};
+    auto start = std::chrono::steady_clock::now();
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        auto now = std::chrono::steady_clock::now();
+
+        /* This converts from nanoseconds to nanosamples, then to samples. */
+        int64_t avail{std::chrono::duration_cast<seconds>((now-start) *
+            mDevice->Frequency).count()};
+        if(avail-done < mDevice->UpdateSize)
+        {
+            std::this_thread::sleep_for(restTime);
+            continue;
+        }
+        while(avail-done >= mDevice->UpdateSize)
+        {
+            mDevice->renderSamples(mBuffer.data(), mDevice->UpdateSize, frameStep);
+            done += mDevice->UpdateSize;
+
+            if(al::endian::native != al::endian::little)
+            {
+                const uint bytesize{mDevice->bytesFromFmt()};
+
+                if(bytesize == 2)
+                {
+                    const size_t len{mBuffer.size() & ~size_t{1}};
+                    for(size_t i{0};i < len;i+=2)
+                        std::swap(mBuffer[i], mBuffer[i+1]);
+                }
+                else if(bytesize == 4)
+                {
+                    const size_t len{mBuffer.size() & ~size_t{3}};
+                    for(size_t i{0};i < len;i+=4)
+                    {
+                        std::swap(mBuffer[i  ], mBuffer[i+3]);
+                        std::swap(mBuffer[i+1], mBuffer[i+2]);
+                    }
+                }
+            }
+
+            const size_t fs{fwrite(mBuffer.data(), frameSize, mDevice->UpdateSize, mFile)};
+            if(fs < mDevice->UpdateSize || ferror(mFile))
+            {
+                ERR("Error writing to file\n");
+                mDevice->handleDisconnect("Failed to write playback samples");
+                break;
+            }
+        }
+
+        /* For every completed second, increment the start time and reduce the
+         * samples done. This prevents the difference between the start time
+         * and current time from growing too large, while maintaining the
+         * correct number of samples to render.
+         */
+        if(done >= mDevice->Frequency)
+        {
+            seconds s{done/mDevice->Frequency};
+            done %= mDevice->Frequency;
+            start += s;
+        }
+    }
+
+    return 0;
+}
+
+void WaveBackend::open(const char *name)
+{
+    auto fname = ConfigValueStr(nullptr, "wave", "file");
+    if(!fname) throw al::backend_exception{al::backend_error::NoDevice,
+        "No wave output filename"};
+
+    if(!name)
+        name = waveDevice;
+    else if(strcmp(name, waveDevice) != 0)
+        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, we're done. */
+    if(mFile) return;
+
+#ifdef _WIN32
+    {
+        std::wstring wname{utf8_to_wstr(fname->c_str())};
+        mFile = _wfopen(wname.c_str(), L"wb");
+    }
+#else
+    mFile = fopen(fname->c_str(), "wb");
+#endif
+    if(!mFile)
+        throw al::backend_exception{al::backend_error::DeviceError, "Could not open file '%s': %s",
+            fname->c_str(), strerror(errno)};
+
+    mDevice->DeviceName = name;
+}
+
+bool WaveBackend::reset()
+{
+    uint channels{0}, bytes{0}, chanmask{0};
+    bool isbformat{false};
+    size_t val;
+
+    fseek(mFile, 0, SEEK_SET);
+    clearerr(mFile);
+
+    if(GetConfigValueBool(nullptr, "wave", "bformat", false))
+    {
+        mDevice->FmtChans = DevFmtAmbi3D;
+        mDevice->mAmbiOrder = 1;
+    }
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtByte:
+        mDevice->FmtType = DevFmtUByte;
+        break;
+    case DevFmtUShort:
+        mDevice->FmtType = DevFmtShort;
+        break;
+    case DevFmtUInt:
+        mDevice->FmtType = DevFmtInt;
+        break;
+    case DevFmtUByte:
+    case DevFmtShort:
+    case DevFmtInt:
+    case DevFmtFloat:
+        break;
+    }
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:   chanmask = 0x04; break;
+    case DevFmtStereo: chanmask = 0x01 | 0x02; break;
+    case DevFmtQuad:   chanmask = 0x01 | 0x02 | 0x10 | 0x20; break;
+    case DevFmtX51: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x200 | 0x400; break;
+    case DevFmtX61: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x100 | 0x200 | 0x400; break;
+    case DevFmtX71: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x010 | 0x020 | 0x200 | 0x400; break;
+    case DevFmtX714:
+        chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x010 | 0x020 | 0x200 | 0x400 | 0x1000 | 0x4000
+            | 0x8000 | 0x20000;
+        break;
+    /* NOTE: Same as 7.1. */
+    case DevFmtX3D71: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x010 | 0x020 | 0x200 | 0x400; break;
+    case DevFmtAmbi3D:
+        /* .amb output requires FuMa */
+        mDevice->mAmbiOrder = minu(mDevice->mAmbiOrder, 3);
+        mDevice->mAmbiLayout = DevAmbiLayout::FuMa;
+        mDevice->mAmbiScale = DevAmbiScaling::FuMa;
+        isbformat = true;
+        chanmask = 0;
+        break;
+    }
+    bytes = mDevice->bytesFromFmt();
+    channels = mDevice->channelsFromFmt();
+
+    rewind(mFile);
+
+    fputs("RIFF", mFile);
+    fwrite32le(0xFFFFFFFF, mFile); // 'RIFF' header len; filled in at close
+
+    fputs("WAVE", mFile);
+
+    fputs("fmt ", mFile);
+    fwrite32le(40, mFile); // 'fmt ' header len; 40 bytes for EXTENSIBLE
+
+    // 16-bit val, format type id (extensible: 0xFFFE)
+    fwrite16le(0xFFFE, mFile);
+    // 16-bit val, channel count
+    fwrite16le(static_cast<ushort>(channels), mFile);
+    // 32-bit val, frequency
+    fwrite32le(mDevice->Frequency, mFile);
+    // 32-bit val, bytes per second
+    fwrite32le(mDevice->Frequency * channels * bytes, mFile);
+    // 16-bit val, frame size
+    fwrite16le(static_cast<ushort>(channels * bytes), mFile);
+    // 16-bit val, bits per sample
+    fwrite16le(static_cast<ushort>(bytes * 8), mFile);
+    // 16-bit val, extra byte count
+    fwrite16le(22, mFile);
+    // 16-bit val, valid bits per sample
+    fwrite16le(static_cast<ushort>(bytes * 8), mFile);
+    // 32-bit val, channel mask
+    fwrite32le(chanmask, mFile);
+    // 16 byte GUID, sub-type format
+    val = fwrite((mDevice->FmtType == DevFmtFloat) ?
+        (isbformat ? SUBTYPE_BFORMAT_FLOAT : SUBTYPE_FLOAT) :
+        (isbformat ? SUBTYPE_BFORMAT_PCM : SUBTYPE_PCM), 1, 16, mFile);
+    (void)val;
+
+    fputs("data", mFile);
+    fwrite32le(0xFFFFFFFF, mFile); // 'data' header len; filled in at close
+
+    if(ferror(mFile))
+    {
+        ERR("Error writing header: %s\n", strerror(errno));
+        return false;
+    }
+    mDataStart = ftell(mFile);
+
+    setDefaultWFXChannelOrder();
+
+    const uint bufsize{mDevice->frameSizeFromFmt() * mDevice->UpdateSize};
+    mBuffer.resize(bufsize);
+
+    return true;
+}
+
+void WaveBackend::start()
+{
+    if(mDataStart > 0 && fseek(mFile, 0, SEEK_END) != 0)
+        WARN("Failed to seek on output file\n");
+    try {
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&WaveBackend::mixerProc), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void WaveBackend::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    if(mDataStart > 0)
+    {
+        long size{ftell(mFile)};
+        if(size > 0)
+        {
+            long dataLen{size - mDataStart};
+            if(fseek(mFile, 4, SEEK_SET) == 0)
+                fwrite32le(static_cast<uint>(size-8), mFile); // 'WAVE' header len
+            if(fseek(mFile, mDataStart-4, SEEK_SET) == 0)
+                fwrite32le(static_cast<uint>(dataLen), mFile); // 'data' header len
+        }
+    }
+}
+
+} // namespace
+
+
+bool WaveBackendFactory::init()
+{ return true; }
+
+bool WaveBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback; }
+
+std::string WaveBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    switch(type)
+    {
+    case BackendType::Playback:
+        /* Includes null char. */
+        outnames.append(waveDevice, sizeof(waveDevice));
+        break;
+    case BackendType::Capture:
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr WaveBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new WaveBackend{device}};
+    return nullptr;
+}
+
+BackendFactory &WaveBackendFactory::getFactory()
+{
+    static WaveBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/wave.h b/alc/backends/wave.h
new file mode 100644 (file)
index 0000000..e768d33
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_WAVE_H
+#define BACKENDS_WAVE_H
+
+#include "base.h"
+
+struct WaveBackendFactory 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_WAVE_H */
diff --git a/alc/backends/winmm.cpp b/alc/backends/winmm.cpp
new file mode 100644 (file)
index 0000000..38e1193
--- /dev/null
@@ -0,0 +1,628 @@
+/**
+ * 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 "winmm.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <memory.h>
+
+#include <windows.h>
+#include <mmsystem.h>
+#include <mmreg.h>
+
+#include <array>
+#include <atomic>
+#include <thread>
+#include <vector>
+#include <string>
+#include <algorithm>
+#include <functional>
+
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "ringbuffer.h"
+#include "strutils.h"
+#include "threads.h"
+
+#ifndef WAVE_FORMAT_IEEE_FLOAT
+#define WAVE_FORMAT_IEEE_FLOAT  0x0003
+#endif
+
+namespace {
+
+#define DEVNAME_HEAD "OpenAL Soft on "
+
+
+al::vector<std::string> PlaybackDevices;
+al::vector<std::string> CaptureDevices;
+
+bool checkName(const al::vector<std::string> &list, const std::string &name)
+{ return std::find(list.cbegin(), list.cend(), name) != list.cend(); }
+
+void ProbePlaybackDevices(void)
+{
+    PlaybackDevices.clear();
+
+    UINT numdevs{waveOutGetNumDevs()};
+    PlaybackDevices.reserve(numdevs);
+    for(UINT i{0};i < numdevs;++i)
+    {
+        std::string dname;
+
+        WAVEOUTCAPSW WaveCaps{};
+        if(waveOutGetDevCapsW(i, &WaveCaps, sizeof(WaveCaps)) == MMSYSERR_NOERROR)
+        {
+            const std::string basename{DEVNAME_HEAD + wstr_to_utf8(WaveCaps.szPname)};
+
+            int count{1};
+            std::string newname{basename};
+            while(checkName(PlaybackDevices, newname))
+            {
+                newname = basename;
+                newname += " #";
+                newname += std::to_string(++count);
+            }
+            dname = std::move(newname);
+
+            TRACE("Got device \"%s\", ID %u\n", dname.c_str(), i);
+        }
+        PlaybackDevices.emplace_back(std::move(dname));
+    }
+}
+
+void ProbeCaptureDevices(void)
+{
+    CaptureDevices.clear();
+
+    UINT numdevs{waveInGetNumDevs()};
+    CaptureDevices.reserve(numdevs);
+    for(UINT i{0};i < numdevs;++i)
+    {
+        std::string dname;
+
+        WAVEINCAPSW WaveCaps{};
+        if(waveInGetDevCapsW(i, &WaveCaps, sizeof(WaveCaps)) == MMSYSERR_NOERROR)
+        {
+            const std::string basename{DEVNAME_HEAD + wstr_to_utf8(WaveCaps.szPname)};
+
+            int count{1};
+            std::string newname{basename};
+            while(checkName(CaptureDevices, newname))
+            {
+                newname = basename;
+                newname += " #";
+                newname += std::to_string(++count);
+            }
+            dname = std::move(newname);
+
+            TRACE("Got device \"%s\", ID %u\n", dname.c_str(), i);
+        }
+        CaptureDevices.emplace_back(std::move(dname));
+    }
+}
+
+
+struct WinMMPlayback final : public BackendBase {
+    WinMMPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~WinMMPlayback() override;
+
+    void CALLBACK waveOutProc(HWAVEOUT device, UINT msg, DWORD_PTR param1, DWORD_PTR param2) noexcept;
+    static void CALLBACK waveOutProcC(HWAVEOUT device, UINT msg, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2) noexcept
+    { reinterpret_cast<WinMMPlayback*>(instance)->waveOutProc(device, msg, param1, param2); }
+
+    int mixerProc();
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+
+    std::atomic<uint> mWritable{0u};
+    al::semaphore mSem;
+    uint mIdx{0u};
+    std::array<WAVEHDR,4> mWaveBuffer{};
+
+    HWAVEOUT mOutHdl{nullptr};
+
+    WAVEFORMATEX mFormat{};
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(WinMMPlayback)
+};
+
+WinMMPlayback::~WinMMPlayback()
+{
+    if(mOutHdl)
+        waveOutClose(mOutHdl);
+    mOutHdl = nullptr;
+
+    al_free(mWaveBuffer[0].lpData);
+    std::fill(mWaveBuffer.begin(), mWaveBuffer.end(), WAVEHDR{});
+}
+
+/* WinMMPlayback::waveOutProc
+ *
+ * Posts a message to 'WinMMPlayback::mixerProc' everytime a WaveOut Buffer is
+ * completed and returns to the application (for more data)
+ */
+void CALLBACK WinMMPlayback::waveOutProc(HWAVEOUT, UINT msg, DWORD_PTR, DWORD_PTR) noexcept
+{
+    if(msg != WOM_DONE) return;
+    mWritable.fetch_add(1, std::memory_order_acq_rel);
+    mSem.post();
+}
+
+FORCE_ALIGN int WinMMPlayback::mixerProc()
+{
+    SetRTPriority();
+    althrd_setname(MIXER_THREAD_NAME);
+
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
+    {
+        uint todo{mWritable.load(std::memory_order_acquire)};
+        if(todo < 1)
+        {
+            mSem.wait();
+            continue;
+        }
+
+        size_t widx{mIdx};
+        do {
+            WAVEHDR &waveHdr = mWaveBuffer[widx];
+            if(++widx == mWaveBuffer.size()) widx = 0;
+
+            mDevice->renderSamples(waveHdr.lpData, mDevice->UpdateSize, mFormat.nChannels);
+            mWritable.fetch_sub(1, std::memory_order_acq_rel);
+            waveOutWrite(mOutHdl, &waveHdr, sizeof(WAVEHDR));
+        } while(--todo);
+        mIdx = static_cast<uint>(widx);
+    }
+
+    return 0;
+}
+
+
+void WinMMPlayback::open(const char *name)
+{
+    if(PlaybackDevices.empty())
+        ProbePlaybackDevices();
+
+    // Find the Device ID matching the deviceName if valid
+    auto iter = name ?
+        std::find(PlaybackDevices.cbegin(), PlaybackDevices.cend(), name) :
+        PlaybackDevices.cbegin();
+    if(iter == PlaybackDevices.cend())
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+    auto DeviceID = static_cast<UINT>(std::distance(PlaybackDevices.cbegin(), iter));
+
+    DevFmtType fmttype{mDevice->FmtType};
+retry_open:
+    WAVEFORMATEX format{};
+    if(fmttype == DevFmtFloat)
+    {
+        format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
+        format.wBitsPerSample = 32;
+    }
+    else
+    {
+        format.wFormatTag = WAVE_FORMAT_PCM;
+        if(fmttype == DevFmtUByte || fmttype == DevFmtByte)
+            format.wBitsPerSample = 8;
+        else
+            format.wBitsPerSample = 16;
+    }
+    format.nChannels = ((mDevice->FmtChans == DevFmtMono) ? 1 : 2);
+    format.nBlockAlign = static_cast<WORD>(format.wBitsPerSample * format.nChannels / 8);
+    format.nSamplesPerSec = mDevice->Frequency;
+    format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
+    format.cbSize = 0;
+
+    HWAVEOUT outHandle{};
+    MMRESULT res{waveOutOpen(&outHandle, DeviceID, &format,
+        reinterpret_cast<DWORD_PTR>(&WinMMPlayback::waveOutProcC),
+        reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION)};
+    if(res != MMSYSERR_NOERROR)
+    {
+        if(fmttype == DevFmtFloat)
+        {
+            fmttype = DevFmtShort;
+            goto retry_open;
+        }
+        throw al::backend_exception{al::backend_error::DeviceError, "waveOutOpen failed: %u", res};
+    }
+
+    if(mOutHdl)
+        waveOutClose(mOutHdl);
+    mOutHdl = outHandle;
+    mFormat = format;
+
+    mDevice->DeviceName = PlaybackDevices[DeviceID];
+}
+
+bool WinMMPlayback::reset()
+{
+    mDevice->BufferSize = static_cast<uint>(uint64_t{mDevice->BufferSize} *
+        mFormat.nSamplesPerSec / mDevice->Frequency);
+    mDevice->BufferSize = (mDevice->BufferSize+3) & ~0x3u;
+    mDevice->UpdateSize = mDevice->BufferSize / 4;
+    mDevice->Frequency = mFormat.nSamplesPerSec;
+
+    if(mFormat.wFormatTag == WAVE_FORMAT_IEEE_FLOAT)
+    {
+        if(mFormat.wBitsPerSample == 32)
+            mDevice->FmtType = DevFmtFloat;
+        else
+        {
+            ERR("Unhandled IEEE float sample depth: %d\n", mFormat.wBitsPerSample);
+            return false;
+        }
+    }
+    else if(mFormat.wFormatTag == WAVE_FORMAT_PCM)
+    {
+        if(mFormat.wBitsPerSample == 16)
+            mDevice->FmtType = DevFmtShort;
+        else if(mFormat.wBitsPerSample == 8)
+            mDevice->FmtType = DevFmtUByte;
+        else
+        {
+            ERR("Unhandled PCM sample depth: %d\n", mFormat.wBitsPerSample);
+            return false;
+        }
+    }
+    else
+    {
+        ERR("Unhandled format tag: 0x%04x\n", mFormat.wFormatTag);
+        return false;
+    }
+
+    if(mFormat.nChannels >= 2)
+        mDevice->FmtChans = DevFmtStereo;
+    else if(mFormat.nChannels == 1)
+        mDevice->FmtChans = DevFmtMono;
+    else
+    {
+        ERR("Unhandled channel count: %d\n", mFormat.nChannels);
+        return false;
+    }
+    setDefaultWFXChannelOrder();
+
+    uint BufferSize{mDevice->UpdateSize * mFormat.nChannels * mDevice->bytesFromFmt()};
+
+    al_free(mWaveBuffer[0].lpData);
+    mWaveBuffer[0] = WAVEHDR{};
+    mWaveBuffer[0].lpData = static_cast<char*>(al_calloc(16, BufferSize * mWaveBuffer.size()));
+    mWaveBuffer[0].dwBufferLength = BufferSize;
+    for(size_t i{1};i < mWaveBuffer.size();i++)
+    {
+        mWaveBuffer[i] = WAVEHDR{};
+        mWaveBuffer[i].lpData = mWaveBuffer[i-1].lpData + mWaveBuffer[i-1].dwBufferLength;
+        mWaveBuffer[i].dwBufferLength = BufferSize;
+    }
+    mIdx = 0;
+
+    return true;
+}
+
+void WinMMPlayback::start()
+{
+    try {
+        for(auto &waveHdr : mWaveBuffer)
+            waveOutPrepareHeader(mOutHdl, &waveHdr, sizeof(WAVEHDR));
+        mWritable.store(static_cast<uint>(mWaveBuffer.size()), std::memory_order_release);
+
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&WinMMPlayback::mixerProc), this};
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start mixing thread: %s", e.what()};
+    }
+}
+
+void WinMMPlayback::stop()
+{
+    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+        return;
+    mThread.join();
+
+    while(mWritable.load(std::memory_order_acquire) < mWaveBuffer.size())
+        mSem.wait();
+    for(auto &waveHdr : mWaveBuffer)
+        waveOutUnprepareHeader(mOutHdl, &waveHdr, sizeof(WAVEHDR));
+    mWritable.store(0, std::memory_order_release);
+}
+
+
+struct WinMMCapture final : public BackendBase {
+    WinMMCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~WinMMCapture() override;
+
+    void CALLBACK waveInProc(HWAVEIN device, UINT msg, DWORD_PTR param1, DWORD_PTR param2) noexcept;
+    static void CALLBACK waveInProcC(HWAVEIN device, UINT msg, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2) noexcept
+    { reinterpret_cast<WinMMCapture*>(instance)->waveInProc(device, msg, param1, param2); }
+
+    int captureProc();
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    std::atomic<uint> mReadable{0u};
+    al::semaphore mSem;
+    uint mIdx{0};
+    std::array<WAVEHDR,4> mWaveBuffer{};
+
+    HWAVEIN mInHdl{nullptr};
+
+    RingBufferPtr mRing{nullptr};
+
+    WAVEFORMATEX mFormat{};
+
+    std::atomic<bool> mKillNow{true};
+    std::thread mThread;
+
+    DEF_NEWDEL(WinMMCapture)
+};
+
+WinMMCapture::~WinMMCapture()
+{
+    // Close the Wave device
+    if(mInHdl)
+        waveInClose(mInHdl);
+    mInHdl = nullptr;
+
+    al_free(mWaveBuffer[0].lpData);
+    std::fill(mWaveBuffer.begin(), mWaveBuffer.end(), WAVEHDR{});
+}
+
+/* WinMMCapture::waveInProc
+ *
+ * Posts a message to 'WinMMCapture::captureProc' everytime a WaveIn Buffer is
+ * completed and returns to the application (with more data).
+ */
+void CALLBACK WinMMCapture::waveInProc(HWAVEIN, UINT msg, DWORD_PTR, DWORD_PTR) noexcept
+{
+    if(msg != WIM_DATA) return;
+    mReadable.fetch_add(1, std::memory_order_acq_rel);
+    mSem.post();
+}
+
+int WinMMCapture::captureProc()
+{
+    althrd_setname(RECORD_THREAD_NAME);
+
+    while(!mKillNow.load(std::memory_order_acquire) &&
+          mDevice->Connected.load(std::memory_order_acquire))
+    {
+        uint todo{mReadable.load(std::memory_order_acquire)};
+        if(todo < 1)
+        {
+            mSem.wait();
+            continue;
+        }
+
+        size_t widx{mIdx};
+        do {
+            WAVEHDR &waveHdr = mWaveBuffer[widx];
+            widx = (widx+1) % mWaveBuffer.size();
+
+            mRing->write(waveHdr.lpData, waveHdr.dwBytesRecorded / mFormat.nBlockAlign);
+            mReadable.fetch_sub(1, std::memory_order_acq_rel);
+            waveInAddBuffer(mInHdl, &waveHdr, sizeof(WAVEHDR));
+        } while(--todo);
+        mIdx = static_cast<uint>(widx);
+    }
+
+    return 0;
+}
+
+
+void WinMMCapture::open(const char *name)
+{
+    if(CaptureDevices.empty())
+        ProbeCaptureDevices();
+
+    // Find the Device ID matching the deviceName if valid
+    auto iter = name ?
+        std::find(CaptureDevices.cbegin(), CaptureDevices.cend(), name) :
+        CaptureDevices.cbegin();
+    if(iter == CaptureDevices.cend())
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+    auto DeviceID = static_cast<UINT>(std::distance(CaptureDevices.cbegin(), iter));
+
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+    case DevFmtStereo:
+        break;
+
+    case DevFmtQuad:
+    case DevFmtX51:
+    case DevFmtX61:
+    case DevFmtX71:
+    case DevFmtX714:
+    case DevFmtX3D71:
+    case DevFmtAmbi3D:
+        throw al::backend_exception{al::backend_error::DeviceError, "%s capture not supported",
+            DevFmtChannelsString(mDevice->FmtChans)};
+    }
+
+    switch(mDevice->FmtType)
+    {
+    case DevFmtUByte:
+    case DevFmtShort:
+    case DevFmtInt:
+    case DevFmtFloat:
+        break;
+
+    case DevFmtByte:
+    case DevFmtUShort:
+    case DevFmtUInt:
+        throw al::backend_exception{al::backend_error::DeviceError, "%s samples not supported",
+            DevFmtTypeString(mDevice->FmtType)};
+    }
+
+    mFormat = WAVEFORMATEX{};
+    mFormat.wFormatTag = (mDevice->FmtType == DevFmtFloat) ?
+                         WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM;
+    mFormat.nChannels = static_cast<WORD>(mDevice->channelsFromFmt());
+    mFormat.wBitsPerSample = static_cast<WORD>(mDevice->bytesFromFmt() * 8);
+    mFormat.nBlockAlign = static_cast<WORD>(mFormat.wBitsPerSample * mFormat.nChannels / 8);
+    mFormat.nSamplesPerSec = mDevice->Frequency;
+    mFormat.nAvgBytesPerSec = mFormat.nSamplesPerSec * mFormat.nBlockAlign;
+    mFormat.cbSize = 0;
+
+    MMRESULT res{waveInOpen(&mInHdl, DeviceID, &mFormat,
+        reinterpret_cast<DWORD_PTR>(&WinMMCapture::waveInProcC),
+        reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION)};
+    if(res != MMSYSERR_NOERROR)
+        throw al::backend_exception{al::backend_error::DeviceError, "waveInOpen failed: %u", res};
+
+    // Ensure each buffer is 50ms each
+    DWORD BufferSize{mFormat.nAvgBytesPerSec / 20u};
+    BufferSize -= (BufferSize % mFormat.nBlockAlign);
+
+    // Allocate circular memory buffer for the captured audio
+    // Make sure circular buffer is at least 100ms in size
+    uint CapturedDataSize{mDevice->BufferSize};
+    CapturedDataSize = static_cast<uint>(maxz(CapturedDataSize, BufferSize*mWaveBuffer.size()));
+
+    mRing = RingBuffer::Create(CapturedDataSize, mFormat.nBlockAlign, false);
+
+    al_free(mWaveBuffer[0].lpData);
+    mWaveBuffer[0] = WAVEHDR{};
+    mWaveBuffer[0].lpData = static_cast<char*>(al_calloc(16, BufferSize * mWaveBuffer.size()));
+    mWaveBuffer[0].dwBufferLength = BufferSize;
+    for(size_t i{1};i < mWaveBuffer.size();++i)
+    {
+        mWaveBuffer[i] = WAVEHDR{};
+        mWaveBuffer[i].lpData = mWaveBuffer[i-1].lpData + mWaveBuffer[i-1].dwBufferLength;
+        mWaveBuffer[i].dwBufferLength = mWaveBuffer[i-1].dwBufferLength;
+    }
+
+    mDevice->DeviceName = CaptureDevices[DeviceID];
+}
+
+void WinMMCapture::start()
+{
+    try {
+        for(size_t i{0};i < mWaveBuffer.size();++i)
+        {
+            waveInPrepareHeader(mInHdl, &mWaveBuffer[i], sizeof(WAVEHDR));
+            waveInAddBuffer(mInHdl, &mWaveBuffer[i], sizeof(WAVEHDR));
+        }
+
+        mKillNow.store(false, std::memory_order_release);
+        mThread = std::thread{std::mem_fn(&WinMMCapture::captureProc), this};
+
+        waveInStart(mInHdl);
+    }
+    catch(std::exception& e) {
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start recording thread: %s", e.what()};
+    }
+}
+
+void WinMMCapture::stop()
+{
+    waveInStop(mInHdl);
+
+    mKillNow.store(true, std::memory_order_release);
+    if(mThread.joinable())
+    {
+        mSem.post();
+        mThread.join();
+    }
+
+    waveInReset(mInHdl);
+    for(size_t i{0};i < mWaveBuffer.size();++i)
+        waveInUnprepareHeader(mInHdl, &mWaveBuffer[i], sizeof(WAVEHDR));
+
+    mReadable.store(0, std::memory_order_release);
+    mIdx = 0;
+}
+
+void WinMMCapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, samples); }
+
+uint WinMMCapture::availableSamples()
+{ return static_cast<uint>(mRing->readSpace()); }
+
+} // namespace
+
+
+bool WinMMBackendFactory::init()
+{ return true; }
+
+bool WinMMBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+std::string WinMMBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+    auto add_device = [&outnames](const std::string &dname) -> void
+    {
+        /* +1 to also append the null char (to ensure a null-separated list and
+         * double-null terminated list).
+         */
+        if(!dname.empty())
+            outnames.append(dname.c_str(), dname.length()+1);
+    };
+    switch(type)
+    {
+    case BackendType::Playback:
+        ProbePlaybackDevices();
+        std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+        break;
+
+    case BackendType::Capture:
+        ProbeCaptureDevices();
+        std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+        break;
+    }
+    return outnames;
+}
+
+BackendPtr WinMMBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new WinMMPlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new WinMMCapture{device}};
+    return nullptr;
+}
+
+BackendFactory &WinMMBackendFactory::getFactory()
+{
+    static WinMMBackendFactory factory{};
+    return factory;
+}
diff --git a/alc/backends/winmm.h b/alc/backends/winmm.h
new file mode 100644 (file)
index 0000000..45a706a
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_WINMM_H
+#define BACKENDS_WINMM_H
+
+#include "base.h"
+
+struct WinMMBackendFactory 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_WINMM_H */
diff --git a/alc/context.cpp b/alc/context.cpp
new file mode 100644 (file)
index 0000000..e02c549
--- /dev/null
@@ -0,0 +1,1105 @@
+
+#include "config.h"
+
+#include "context.h"
+
+#include <algorithm>
+#include <functional>
+#include <limits>
+#include <numeric>
+#include <stddef.h>
+#include <stdexcept>
+
+#include "AL/efx.h"
+
+#include "al/auxeffectslot.h"
+#include "al/source.h"
+#include "al/effect.h"
+#include "al/event.h"
+#include "al/listener.h"
+#include "albit.h"
+#include "alc/alu.h"
+#include "core/async_event.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/logging.h"
+#include "core/voice.h"
+#include "core/voice_change.h"
+#include "device.h"
+#include "ringbuffer.h"
+#include "vecmat.h"
+
+#ifdef ALSOFT_EAX
+#include <cstring>
+#include "alstring.h"
+#include "al/eax/globals.h"
+#endif // ALSOFT_EAX
+
+namespace {
+
+using namespace std::placeholders;
+
+using voidp = void*;
+
+/* Default context extensions */
+constexpr ALchar alExtList[] =
+    "AL_EXT_ALAW "
+    "AL_EXT_BFORMAT "
+    "AL_EXT_DOUBLE "
+    "AL_EXT_EXPONENT_DISTANCE "
+    "AL_EXT_FLOAT32 "
+    "AL_EXT_IMA4 "
+    "AL_EXT_LINEAR_DISTANCE "
+    "AL_EXT_MCFORMATS "
+    "AL_EXT_MULAW "
+    "AL_EXT_MULAW_BFORMAT "
+    "AL_EXT_MULAW_MCFORMATS "
+    "AL_EXT_OFFSET "
+    "AL_EXT_source_distance_model "
+    "AL_EXT_SOURCE_RADIUS "
+    "AL_EXT_STATIC_BUFFER "
+    "AL_EXT_STEREO_ANGLES "
+    "AL_LOKI_quadriphonic "
+    "AL_SOFT_bformat_ex "
+    "AL_SOFTX_bformat_hoa "
+    "AL_SOFT_block_alignment "
+    "AL_SOFT_buffer_length_query "
+    "AL_SOFT_callback_buffer "
+    "AL_SOFTX_convolution_reverb "
+    "AL_SOFT_deferred_updates "
+    "AL_SOFT_direct_channels "
+    "AL_SOFT_direct_channels_remix "
+    "AL_SOFT_effect_target "
+    "AL_SOFT_events "
+    "AL_SOFT_gain_clamp_ex "
+    "AL_SOFTX_hold_on_disconnect "
+    "AL_SOFT_loop_points "
+    "AL_SOFTX_map_buffer "
+    "AL_SOFT_MSADPCM "
+    "AL_SOFT_source_latency "
+    "AL_SOFT_source_length "
+    "AL_SOFT_source_resampler "
+    "AL_SOFT_source_spatialize "
+    "AL_SOFT_source_start_delay "
+    "AL_SOFT_UHJ "
+    "AL_SOFT_UHJ_ex";
+
+} // namespace
+
+
+std::atomic<bool> ALCcontext::sGlobalContextLock{false};
+std::atomic<ALCcontext*> ALCcontext::sGlobalContext{nullptr};
+
+thread_local ALCcontext *ALCcontext::sLocalContext{nullptr};
+ALCcontext::ThreadCtx::~ThreadCtx()
+{
+    if(ALCcontext *ctx{ALCcontext::sLocalContext})
+    {
+        const bool result{ctx->releaseIfNoDelete()};
+        ERR("Context %p current for thread being destroyed%s!\n", voidp{ctx},
+            result ? "" : ", leak detected");
+    }
+}
+thread_local ALCcontext::ThreadCtx ALCcontext::sThreadContext;
+
+ALeffect ALCcontext::sDefaultEffect;
+
+
+#ifdef __MINGW32__
+ALCcontext *ALCcontext::getThreadContext() noexcept
+{ return sLocalContext; }
+void ALCcontext::setThreadContext(ALCcontext *context) noexcept
+{ sThreadContext.set(context); }
+#endif
+
+ALCcontext::ALCcontext(al::intrusive_ptr<ALCdevice> device)
+  : ContextBase{device.get()}, mALDevice{std::move(device)}
+{
+}
+
+ALCcontext::~ALCcontext()
+{
+    TRACE("Freeing context %p\n", voidp{this});
+
+    size_t count{std::accumulate(mSourceList.cbegin(), mSourceList.cend(), size_t{0u},
+        [](size_t cur, const SourceSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<uint>(al::popcount(~sublist.FreeMask)); })};
+    if(count > 0)
+        WARN("%zu Source%s not deleted\n", count, (count==1)?"":"s");
+    mSourceList.clear();
+    mNumSources = 0;
+
+#ifdef ALSOFT_EAX
+    eaxUninitialize();
+#endif // ALSOFT_EAX
+
+    mDefaultSlot = nullptr;
+    count = std::accumulate(mEffectSlotList.cbegin(), mEffectSlotList.cend(), size_t{0u},
+        [](size_t cur, const EffectSlotSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<uint>(al::popcount(~sublist.FreeMask)); });
+    if(count > 0)
+        WARN("%zu AuxiliaryEffectSlot%s not deleted\n", count, (count==1)?"":"s");
+    mEffectSlotList.clear();
+    mNumEffectSlots = 0;
+}
+
+void ALCcontext::init()
+{
+    if(sDefaultEffect.type != AL_EFFECT_NULL && mDevice->Type == DeviceType::Playback)
+    {
+        mDefaultSlot = std::make_unique<ALeffectslot>(this);
+        aluInitEffectPanning(mDefaultSlot->mSlot, this);
+    }
+
+    EffectSlotArray *auxslots;
+    if(!mDefaultSlot)
+        auxslots = EffectSlot::CreatePtrArray(0);
+    else
+    {
+        auxslots = EffectSlot::CreatePtrArray(1);
+        (*auxslots)[0] = mDefaultSlot->mSlot;
+        mDefaultSlot->mState = SlotState::Playing;
+    }
+    mActiveAuxSlots.store(auxslots, std::memory_order_relaxed);
+
+    allocVoiceChanges();
+    {
+        VoiceChange *cur{mVoiceChangeTail};
+        while(VoiceChange *next{cur->mNext.load(std::memory_order_relaxed)})
+            cur = next;
+        mCurrentVoiceChange.store(cur, std::memory_order_relaxed);
+    }
+
+    mExtensionList = alExtList;
+
+    if(sBufferSubDataCompat)
+    {
+        std::string extlist{mExtensionList};
+
+        const auto pos = extlist.find("AL_EXT_SOURCE_RADIUS ");
+        if(pos != std::string::npos)
+            extlist.replace(pos, 20, "AL_SOFT_buffer_sub_data");
+        else
+            extlist += " AL_SOFT_buffer_sub_data";
+
+        mExtensionListOverride = std::move(extlist);
+        mExtensionList = mExtensionListOverride.c_str();
+    }
+
+#ifdef ALSOFT_EAX
+    eax_initialize_extensions();
+#endif // ALSOFT_EAX
+
+    mParams.Position = alu::Vector{0.0f, 0.0f, 0.0f, 1.0f};
+    mParams.Matrix = alu::Matrix::Identity();
+    mParams.Velocity = alu::Vector{};
+    mParams.Gain = mListener.Gain;
+    mParams.MetersPerUnit = mListener.mMetersPerUnit;
+    mParams.AirAbsorptionGainHF = mAirAbsorptionGainHF;
+    mParams.DopplerFactor = mDopplerFactor;
+    mParams.SpeedOfSound = mSpeedOfSound * mDopplerVelocity;
+    mParams.SourceDistanceModel = mSourceDistanceModel;
+    mParams.mDistanceModel = mDistanceModel;
+
+
+    mAsyncEvents = RingBuffer::Create(511, sizeof(AsyncEvent), false);
+    StartEventThrd(this);
+
+
+    allocVoices(256);
+    mActiveVoiceCount.store(64, std::memory_order_relaxed);
+}
+
+bool ALCcontext::deinit()
+{
+    if(sLocalContext == this)
+    {
+        WARN("%p released while current on thread\n", voidp{this});
+        sThreadContext.set(nullptr);
+        dec_ref();
+    }
+
+    ALCcontext *origctx{this};
+    if(sGlobalContext.compare_exchange_strong(origctx, nullptr))
+    {
+        while(sGlobalContextLock.load()) {
+            /* Wait to make sure another thread didn't get the context and is
+             * trying to increment its refcount.
+             */
+        }
+        dec_ref();
+    }
+
+    bool ret{};
+    /* First make sure this context exists in the device's list. */
+    auto *oldarray = mDevice->mContexts.load(std::memory_order_acquire);
+    if(auto toremove = static_cast<size_t>(std::count(oldarray->begin(), oldarray->end(), this)))
+    {
+        using ContextArray = al::FlexArray<ContextBase*>;
+        auto alloc_ctx_array = [](const size_t count) -> ContextArray*
+        {
+            if(count == 0) return &DeviceBase::sEmptyContextArray;
+            return ContextArray::Create(count).release();
+        };
+        auto *newarray = alloc_ctx_array(oldarray->size() - toremove);
+
+        /* Copy the current/old context handles to the new array, excluding the
+         * given context.
+         */
+        std::copy_if(oldarray->begin(), oldarray->end(), newarray->begin(),
+            [this](ContextBase *ctx) { return ctx != this; });
+
+        /* Store the new context array in the device. Wait for any current mix
+         * to finish before deleting the old array.
+         */
+        mDevice->mContexts.store(newarray);
+        if(oldarray != &DeviceBase::sEmptyContextArray)
+        {
+            mDevice->waitForMix();
+            delete oldarray;
+        }
+
+        ret = !newarray->empty();
+    }
+    else
+        ret = !oldarray->empty();
+
+    StopEventThrd(this);
+
+    return ret;
+}
+
+void ALCcontext::applyAllUpdates()
+{
+    /* Tell the mixer to stop applying updates, then wait for any active
+     * updating to finish, before providing updates.
+     */
+    mHoldUpdates.store(true, std::memory_order_release);
+    while((mUpdateCount.load(std::memory_order_acquire)&1) != 0) {
+        /* busy-wait */
+    }
+
+#ifdef ALSOFT_EAX
+    if(mEaxNeedsCommit)
+        eaxCommit();
+#endif
+
+    if(std::exchange(mPropsDirty, false))
+        UpdateContextProps(this);
+    UpdateAllEffectSlotProps(this);
+    UpdateAllSourceProps(this);
+
+    /* Now with all updates declared, let the mixer continue applying them so
+     * they all happen at once.
+     */
+    mHoldUpdates.store(false, std::memory_order_release);
+}
+
+#ifdef ALSOFT_EAX
+namespace {
+
+template<typename F>
+void ForEachSource(ALCcontext *context, F func)
+{
+    for(auto &sublist : context->mSourceList)
+    {
+        uint64_t usemask{~sublist.FreeMask};
+        while(usemask)
+        {
+            const int idx{al::countr_zero(usemask)};
+            usemask &= ~(1_u64 << idx);
+
+            func(sublist.Sources[idx]);
+        }
+    }
+}
+
+} // namespace
+
+
+bool ALCcontext::eaxIsCapable() const noexcept
+{
+    return eax_has_enough_aux_sends();
+}
+
+void ALCcontext::eaxUninitialize() noexcept
+{
+    if(!mEaxIsInitialized)
+        return;
+
+    mEaxIsInitialized = false;
+    mEaxIsTried = false;
+    mEaxFxSlots.uninitialize();
+}
+
+ALenum ALCcontext::eax_eax_set(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_value,
+    ALuint property_value_size)
+{
+    const auto call = create_eax_call(
+        EaxCallType::set,
+        property_set_id,
+        property_id,
+        property_source_id,
+        property_value,
+        property_value_size);
+
+    eax_initialize();
+
+    switch(call.get_property_set_id())
+    {
+    case EaxCallPropertySetId::context:
+        eax_set(call);
+        break;
+    case EaxCallPropertySetId::fx_slot:
+    case EaxCallPropertySetId::fx_slot_effect:
+        eax_dispatch_fx_slot(call);
+        break;
+    case EaxCallPropertySetId::source:
+        eax_dispatch_source(call);
+        break;
+    default:
+        eax_fail_unknown_property_set_id();
+    }
+    mEaxNeedsCommit = true;
+
+    if(!call.is_deferred())
+    {
+        eaxCommit();
+        if(!mDeferUpdates)
+            applyAllUpdates();
+    }
+
+    return AL_NO_ERROR;
+}
+
+ALenum ALCcontext::eax_eax_get(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_value,
+    ALuint property_value_size)
+{
+    const auto call = create_eax_call(
+        EaxCallType::get,
+        property_set_id,
+        property_id,
+        property_source_id,
+        property_value,
+        property_value_size);
+
+    eax_initialize();
+
+    switch(call.get_property_set_id())
+    {
+    case EaxCallPropertySetId::context:
+        eax_get(call);
+        break;
+    case EaxCallPropertySetId::fx_slot:
+    case EaxCallPropertySetId::fx_slot_effect:
+        eax_dispatch_fx_slot(call);
+        break;
+    case EaxCallPropertySetId::source:
+        eax_dispatch_source(call);
+        break;
+    default:
+        eax_fail_unknown_property_set_id();
+    }
+
+    return AL_NO_ERROR;
+}
+
+void ALCcontext::eaxSetLastError() noexcept
+{
+    mEaxLastError = EAXERR_INVALID_OPERATION;
+}
+
+[[noreturn]] void ALCcontext::eax_fail(const char* message)
+{
+    throw ContextException{message};
+}
+
+[[noreturn]] void ALCcontext::eax_fail_unknown_property_set_id()
+{
+    eax_fail("Unknown property ID.");
+}
+
+[[noreturn]] void ALCcontext::eax_fail_unknown_primary_fx_slot_id()
+{
+    eax_fail("Unknown primary FX Slot ID.");
+}
+
+[[noreturn]] void ALCcontext::eax_fail_unknown_property_id()
+{
+    eax_fail("Unknown property ID.");
+}
+
+[[noreturn]] void ALCcontext::eax_fail_unknown_version()
+{
+    eax_fail("Unknown version.");
+}
+
+void ALCcontext::eax_initialize_extensions()
+{
+    if(!eax_g_is_enabled)
+        return;
+
+    const auto string_max_capacity =
+        std::strlen(mExtensionList) + 1 +
+        std::strlen(eax1_ext_name) + 1 +
+        std::strlen(eax2_ext_name) + 1 +
+        std::strlen(eax3_ext_name) + 1 +
+        std::strlen(eax4_ext_name) + 1 +
+        std::strlen(eax5_ext_name) + 1 +
+        std::strlen(eax_x_ram_ext_name) + 1;
+
+    std::string extlist;
+    extlist.reserve(string_max_capacity);
+
+    if(eaxIsCapable())
+    {
+        extlist += eax1_ext_name;
+        extlist += ' ';
+
+        extlist += eax2_ext_name;
+        extlist += ' ';
+
+        extlist += eax3_ext_name;
+        extlist += ' ';
+
+        extlist += eax4_ext_name;
+        extlist += ' ';
+
+        extlist += eax5_ext_name;
+        extlist += ' ';
+    }
+
+    extlist += eax_x_ram_ext_name;
+    extlist += ' ';
+
+    extlist += mExtensionList;
+
+    mExtensionListOverride = std::move(extlist);
+    mExtensionList = mExtensionListOverride.c_str();
+}
+
+void ALCcontext::eax_initialize()
+{
+    if(mEaxIsInitialized)
+        return;
+
+    if(mEaxIsTried)
+        eax_fail("No EAX.");
+
+    mEaxIsTried = true;
+
+    if(!eax_g_is_enabled)
+        eax_fail("EAX disabled by a configuration.");
+
+    eax_ensure_compatibility();
+    eax_set_defaults();
+    eax_context_commit_air_absorbtion_hf();
+    eax_update_speaker_configuration();
+    eax_initialize_fx_slots();
+
+    mEaxIsInitialized = true;
+}
+
+bool ALCcontext::eax_has_no_default_effect_slot() const noexcept
+{
+    return mDefaultSlot == nullptr;
+}
+
+void ALCcontext::eax_ensure_no_default_effect_slot() const
+{
+    if(!eax_has_no_default_effect_slot())
+        eax_fail("There is a default effect slot in the context.");
+}
+
+bool ALCcontext::eax_has_enough_aux_sends() const noexcept
+{
+    return mALDevice->NumAuxSends >= EAX_MAX_FXSLOTS;
+}
+
+void ALCcontext::eax_ensure_enough_aux_sends() const
+{
+    if(!eax_has_enough_aux_sends())
+        eax_fail("Not enough aux sends.");
+}
+
+void ALCcontext::eax_ensure_compatibility()
+{
+    eax_ensure_enough_aux_sends();
+}
+
+unsigned long ALCcontext::eax_detect_speaker_configuration() const
+{
+#define EAX_PREFIX "[EAX_DETECT_SPEAKER_CONFIG]"
+
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono: return SPEAKERS_2;
+    case DevFmtStereo:
+        /* Pretend 7.1 if using UHJ output, since they both provide full
+         * horizontal surround.
+         */
+        if(mDevice->mUhjEncoder)
+            return SPEAKERS_7;
+        if(mDevice->Flags.test(DirectEar))
+            return HEADPHONES;
+        return SPEAKERS_2;
+    case DevFmtQuad: return SPEAKERS_4;
+    case DevFmtX51: return SPEAKERS_5;
+    case DevFmtX61: return SPEAKERS_6;
+    case DevFmtX71: return SPEAKERS_7;
+    /* 7.1.4 is compatible with 7.1. This could instead be HEADPHONES to
+     * suggest with-height surround sound (like HRTF).
+     */
+    case DevFmtX714: return SPEAKERS_7;
+    /* 3D7.1 is only compatible with 5.1. This could instead be HEADPHONES to
+     * suggest full-sphere surround sound (like HRTF).
+     */
+    case DevFmtX3D71: return SPEAKERS_5;
+    /* This could also be HEADPHONES, since headphones-based HRTF and Ambi3D
+     * provide full-sphere surround sound. Depends if apps are more likely to
+     * consider headphones or 7.1 for surround sound support.
+     */
+    case DevFmtAmbi3D: return SPEAKERS_7;
+    }
+    ERR(EAX_PREFIX "Unexpected device channel format 0x%x.\n", mDevice->FmtChans);
+    return HEADPHONES;
+
+#undef EAX_PREFIX
+}
+
+void ALCcontext::eax_update_speaker_configuration()
+{
+    mEaxSpeakerConfig = eax_detect_speaker_configuration();
+}
+
+void ALCcontext::eax_set_last_error_defaults() noexcept
+{
+    mEaxLastError = EAX_OK;
+}
+
+void ALCcontext::eax_session_set_defaults() noexcept
+{
+    mEaxSession.ulEAXVersion = EAXCONTEXT_DEFAULTEAXSESSION;
+    mEaxSession.ulMaxActiveSends = EAXCONTEXT_DEFAULTMAXACTIVESENDS;
+}
+
+void ALCcontext::eax4_context_set_defaults(Eax4Props& props) noexcept
+{
+    props.guidPrimaryFXSlotID = EAX40CONTEXT_DEFAULTPRIMARYFXSLOTID;
+    props.flDistanceFactor = EAXCONTEXT_DEFAULTDISTANCEFACTOR;
+    props.flAirAbsorptionHF = EAXCONTEXT_DEFAULTAIRABSORPTIONHF;
+    props.flHFReference = EAXCONTEXT_DEFAULTHFREFERENCE;
+}
+
+void ALCcontext::eax4_context_set_defaults(Eax4State& state) noexcept
+{
+    eax4_context_set_defaults(state.i);
+    state.d = state.i;
+}
+
+void ALCcontext::eax5_context_set_defaults(Eax5Props& props) noexcept
+{
+    props.guidPrimaryFXSlotID = EAX50CONTEXT_DEFAULTPRIMARYFXSLOTID;
+    props.flDistanceFactor = EAXCONTEXT_DEFAULTDISTANCEFACTOR;
+    props.flAirAbsorptionHF = EAXCONTEXT_DEFAULTAIRABSORPTIONHF;
+    props.flHFReference = EAXCONTEXT_DEFAULTHFREFERENCE;
+    props.flMacroFXFactor = EAXCONTEXT_DEFAULTMACROFXFACTOR;
+}
+
+void ALCcontext::eax5_context_set_defaults(Eax5State& state) noexcept
+{
+    eax5_context_set_defaults(state.i);
+    state.d = state.i;
+}
+
+void ALCcontext::eax_context_set_defaults()
+{
+    eax5_context_set_defaults(mEax123);
+    eax4_context_set_defaults(mEax4);
+    eax5_context_set_defaults(mEax5);
+    mEax = mEax5.i;
+    mEaxVersion = 5;
+    mEaxDf = EaxDirtyFlags{};
+}
+
+void ALCcontext::eax_set_defaults()
+{
+    eax_set_last_error_defaults();
+    eax_session_set_defaults();
+    eax_context_set_defaults();
+}
+
+void ALCcontext::eax_dispatch_fx_slot(const EaxCall& call)
+{
+    const auto fx_slot_index = call.get_fx_slot_index();
+    if(!fx_slot_index.has_value())
+        eax_fail("Invalid fx slot index.");
+
+    auto& fx_slot = eaxGetFxSlot(*fx_slot_index);
+    if(fx_slot.eax_dispatch(call))
+    {
+        std::lock_guard<std::mutex> source_lock{mSourceLock};
+        ForEachSource(this, std::mem_fn(&ALsource::eaxMarkAsChanged));
+    }
+}
+
+void ALCcontext::eax_dispatch_source(const EaxCall& call)
+{
+    const auto source_id = call.get_property_al_name();
+    std::lock_guard<std::mutex> source_lock{mSourceLock};
+    const auto source = ALsource::EaxLookupSource(*this, source_id);
+
+    if (source == nullptr)
+        eax_fail("Source not found.");
+
+    source->eaxDispatch(call);
+}
+
+void ALCcontext::eax_get_misc(const EaxCall& call)
+{
+    switch(call.get_property_id())
+    {
+    case EAXCONTEXT_NONE:
+        break;
+    case EAXCONTEXT_LASTERROR:
+        call.set_value<ContextException>(mEaxLastError);
+        break;
+    case EAXCONTEXT_SPEAKERCONFIG:
+        call.set_value<ContextException>(mEaxSpeakerConfig);
+        break;
+    case EAXCONTEXT_EAXSESSION:
+        call.set_value<ContextException>(mEaxSession);
+        break;
+    default:
+        eax_fail_unknown_property_id();
+    }
+}
+
+void ALCcontext::eax4_get(const EaxCall& call, const Eax4Props& props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXCONTEXT_ALLPARAMETERS:
+        call.set_value<ContextException>(props);
+        break;
+    case EAXCONTEXT_PRIMARYFXSLOTID:
+        call.set_value<ContextException>(props.guidPrimaryFXSlotID);
+        break;
+    case EAXCONTEXT_DISTANCEFACTOR:
+        call.set_value<ContextException>(props.flDistanceFactor);
+        break;
+    case EAXCONTEXT_AIRABSORPTIONHF:
+        call.set_value<ContextException>(props.flAirAbsorptionHF);
+        break;
+    case EAXCONTEXT_HFREFERENCE:
+        call.set_value<ContextException>(props.flHFReference);
+        break;
+    default:
+        eax_get_misc(call);
+        break;
+    }
+}
+
+void ALCcontext::eax5_get(const EaxCall& call, const Eax5Props& props)
+{
+    switch(call.get_property_id())
+    {
+    case EAXCONTEXT_ALLPARAMETERS:
+        call.set_value<ContextException>(props);
+        break;
+    case EAXCONTEXT_PRIMARYFXSLOTID:
+        call.set_value<ContextException>(props.guidPrimaryFXSlotID);
+        break;
+    case EAXCONTEXT_DISTANCEFACTOR:
+        call.set_value<ContextException>(props.flDistanceFactor);
+        break;
+    case EAXCONTEXT_AIRABSORPTIONHF:
+        call.set_value<ContextException>(props.flAirAbsorptionHF);
+        break;
+    case EAXCONTEXT_HFREFERENCE:
+        call.set_value<ContextException>(props.flHFReference);
+        break;
+    case EAXCONTEXT_MACROFXFACTOR:
+        call.set_value<ContextException>(props.flMacroFXFactor);
+        break;
+    default:
+        eax_get_misc(call);
+        break;
+    }
+}
+
+void ALCcontext::eax_get(const EaxCall& call)
+{
+    switch(call.get_version())
+    {
+    case 4: eax4_get(call, mEax4.i); break;
+    case 5: eax5_get(call, mEax5.i); break;
+    default: eax_fail_unknown_version();
+    }
+}
+
+void ALCcontext::eax_context_commit_primary_fx_slot_id()
+{
+    mEaxPrimaryFxSlotIndex = mEax.guidPrimaryFXSlotID;
+}
+
+void ALCcontext::eax_context_commit_distance_factor()
+{
+    if(mListener.mMetersPerUnit == mEax.flDistanceFactor)
+        return;
+
+    mListener.mMetersPerUnit = mEax.flDistanceFactor;
+    mPropsDirty = true;
+}
+
+void ALCcontext::eax_context_commit_air_absorbtion_hf()
+{
+    const auto new_value = level_mb_to_gain(mEax.flAirAbsorptionHF);
+
+    if(mAirAbsorptionGainHF == new_value)
+        return;
+
+    mAirAbsorptionGainHF = new_value;
+    mPropsDirty = true;
+}
+
+void ALCcontext::eax_context_commit_hf_reference()
+{
+    // TODO
+}
+
+void ALCcontext::eax_context_commit_macro_fx_factor()
+{
+    // TODO
+}
+
+void ALCcontext::eax_initialize_fx_slots()
+{
+    mEaxFxSlots.initialize(*this);
+    mEaxPrimaryFxSlotIndex = mEax.guidPrimaryFXSlotID;
+}
+
+void ALCcontext::eax_update_sources()
+{
+    std::unique_lock<std::mutex> source_lock{mSourceLock};
+    auto update_source = [](ALsource &source)
+    { source.eaxCommit(); };
+    ForEachSource(this, update_source);
+}
+
+void ALCcontext::eax_set_misc(const EaxCall& call)
+{
+    switch(call.get_property_id())
+    {
+    case EAXCONTEXT_NONE:
+        break;
+    case EAXCONTEXT_SPEAKERCONFIG:
+        eax_set<Eax5SpeakerConfigValidator>(call, mEaxSpeakerConfig);
+        break;
+    case EAXCONTEXT_EAXSESSION:
+        eax_set<Eax5SessionAllValidator>(call, mEaxSession);
+        break;
+    default:
+        eax_fail_unknown_property_id();
+    }
+}
+
+void ALCcontext::eax4_defer_all(const EaxCall& call, Eax4State& state)
+{
+    const auto& src = call.get_value<ContextException, const EAX40CONTEXTPROPERTIES>();
+    Eax4AllValidator{}(src);
+    const auto& dst_i = state.i;
+    auto& dst_d = state.d;
+    dst_d = src;
+
+    if(dst_i.guidPrimaryFXSlotID != dst_d.guidPrimaryFXSlotID)
+        mEaxDf |= eax_primary_fx_slot_id_dirty_bit;
+
+    if(dst_i.flDistanceFactor != dst_d.flDistanceFactor)
+        mEaxDf |= eax_distance_factor_dirty_bit;
+
+    if(dst_i.flAirAbsorptionHF != dst_d.flAirAbsorptionHF)
+        mEaxDf |= eax_air_absorption_hf_dirty_bit;
+
+    if(dst_i.flHFReference != dst_d.flHFReference)
+        mEaxDf |= eax_hf_reference_dirty_bit;
+}
+
+void ALCcontext::eax4_defer(const EaxCall& call, Eax4State& state)
+{
+    switch(call.get_property_id())
+    {
+    case EAXCONTEXT_ALLPARAMETERS:
+        eax4_defer_all(call, state);
+        break;
+    case EAXCONTEXT_PRIMARYFXSLOTID:
+        eax_defer<Eax4PrimaryFxSlotIdValidator, eax_primary_fx_slot_id_dirty_bit>(
+            call, state, &EAX40CONTEXTPROPERTIES::guidPrimaryFXSlotID);
+        break;
+    case EAXCONTEXT_DISTANCEFACTOR:
+        eax_defer<Eax4DistanceFactorValidator, eax_distance_factor_dirty_bit>(
+            call, state, &EAX40CONTEXTPROPERTIES::flDistanceFactor);
+        break;
+    case EAXCONTEXT_AIRABSORPTIONHF:
+        eax_defer<Eax4AirAbsorptionHfValidator, eax_air_absorption_hf_dirty_bit>(
+            call, state, &EAX40CONTEXTPROPERTIES::flAirAbsorptionHF);
+        break;
+    case EAXCONTEXT_HFREFERENCE:
+        eax_defer<Eax4HfReferenceValidator, eax_hf_reference_dirty_bit>(
+            call, state, &EAX40CONTEXTPROPERTIES::flHFReference);
+        break;
+    default:
+        eax_set_misc(call);
+        break;
+    }
+}
+
+void ALCcontext::eax5_defer_all(const EaxCall& call, Eax5State& state)
+{
+    const auto& src = call.get_value<ContextException, const EAX50CONTEXTPROPERTIES>();
+    Eax4AllValidator{}(src);
+    const auto& dst_i = state.i;
+    auto& dst_d = state.d;
+    dst_d = src;
+
+    if(dst_i.guidPrimaryFXSlotID != dst_d.guidPrimaryFXSlotID)
+        mEaxDf |= eax_primary_fx_slot_id_dirty_bit;
+
+    if(dst_i.flDistanceFactor != dst_d.flDistanceFactor)
+        mEaxDf |= eax_distance_factor_dirty_bit;
+
+    if(dst_i.flAirAbsorptionHF != dst_d.flAirAbsorptionHF)
+        mEaxDf |= eax_air_absorption_hf_dirty_bit;
+
+    if(dst_i.flHFReference != dst_d.flHFReference)
+        mEaxDf |= eax_hf_reference_dirty_bit;
+
+    if(dst_i.flMacroFXFactor != dst_d.flMacroFXFactor)
+        mEaxDf |= eax_macro_fx_factor_dirty_bit;
+}
+
+void ALCcontext::eax5_defer(const EaxCall& call, Eax5State& state)
+{
+    switch(call.get_property_id())
+    {
+    case EAXCONTEXT_ALLPARAMETERS:
+        eax5_defer_all(call, state);
+        break;
+    case EAXCONTEXT_PRIMARYFXSLOTID:
+        eax_defer<Eax5PrimaryFxSlotIdValidator, eax_primary_fx_slot_id_dirty_bit>(
+            call, state, &EAX50CONTEXTPROPERTIES::guidPrimaryFXSlotID);
+        break;
+    case EAXCONTEXT_DISTANCEFACTOR:
+        eax_defer<Eax4DistanceFactorValidator, eax_distance_factor_dirty_bit>(
+            call, state, &EAX50CONTEXTPROPERTIES::flDistanceFactor);
+        break;
+    case EAXCONTEXT_AIRABSORPTIONHF:
+        eax_defer<Eax4AirAbsorptionHfValidator, eax_air_absorption_hf_dirty_bit>(
+            call, state, &EAX50CONTEXTPROPERTIES::flAirAbsorptionHF);
+        break;
+    case EAXCONTEXT_HFREFERENCE:
+        eax_defer<Eax4HfReferenceValidator, eax_hf_reference_dirty_bit>(
+            call, state, &EAX50CONTEXTPROPERTIES::flHFReference);
+        break;
+    case EAXCONTEXT_MACROFXFACTOR:
+        eax_defer<Eax5MacroFxFactorValidator, eax_macro_fx_factor_dirty_bit>(
+            call, state, &EAX50CONTEXTPROPERTIES::flMacroFXFactor);
+        break;
+    default:
+        eax_set_misc(call);
+        break;
+    }
+}
+
+void ALCcontext::eax_set(const EaxCall& call)
+{
+    const auto version = call.get_version();
+    switch(version)
+    {
+    case 4: eax4_defer(call, mEax4); break;
+    case 5: eax5_defer(call, mEax5); break;
+    default: eax_fail_unknown_version();
+    }
+    if(version != mEaxVersion)
+        mEaxDf = ~EaxDirtyFlags();
+    mEaxVersion = version;
+}
+
+void ALCcontext::eax4_context_commit(Eax4State& state, EaxDirtyFlags& dst_df)
+{
+    if(mEaxDf == EaxDirtyFlags{})
+        return;
+
+    eax_context_commit_property<eax_primary_fx_slot_id_dirty_bit>(
+        state, dst_df, &EAX40CONTEXTPROPERTIES::guidPrimaryFXSlotID);
+    eax_context_commit_property<eax_distance_factor_dirty_bit>(
+        state, dst_df, &EAX40CONTEXTPROPERTIES::flDistanceFactor);
+    eax_context_commit_property<eax_air_absorption_hf_dirty_bit>(
+        state, dst_df, &EAX40CONTEXTPROPERTIES::flAirAbsorptionHF);
+    eax_context_commit_property<eax_hf_reference_dirty_bit>(
+        state, dst_df, &EAX40CONTEXTPROPERTIES::flHFReference);
+
+    mEaxDf = EaxDirtyFlags{};
+}
+
+void ALCcontext::eax5_context_commit(Eax5State& state, EaxDirtyFlags& dst_df)
+{
+    if(mEaxDf == EaxDirtyFlags{})
+        return;
+
+    eax_context_commit_property<eax_primary_fx_slot_id_dirty_bit>(
+        state, dst_df, &EAX50CONTEXTPROPERTIES::guidPrimaryFXSlotID);
+    eax_context_commit_property<eax_distance_factor_dirty_bit>(
+        state, dst_df, &EAX50CONTEXTPROPERTIES::flDistanceFactor);
+    eax_context_commit_property<eax_air_absorption_hf_dirty_bit>(
+        state, dst_df, &EAX50CONTEXTPROPERTIES::flAirAbsorptionHF);
+    eax_context_commit_property<eax_hf_reference_dirty_bit>(
+        state, dst_df, &EAX50CONTEXTPROPERTIES::flHFReference);
+    eax_context_commit_property<eax_macro_fx_factor_dirty_bit>(
+        state, dst_df, &EAX50CONTEXTPROPERTIES::flMacroFXFactor);
+
+    mEaxDf = EaxDirtyFlags{};
+}
+
+void ALCcontext::eax_context_commit()
+{
+    auto dst_df = EaxDirtyFlags{};
+
+    switch(mEaxVersion)
+    {
+    case 1:
+    case 2:
+    case 3:
+        eax5_context_commit(mEax123, dst_df);
+        break;
+    case 4:
+        eax4_context_commit(mEax4, dst_df);
+        break;
+    case 5:
+        eax5_context_commit(mEax5, dst_df);
+        break;
+    }
+
+    if(dst_df == EaxDirtyFlags{})
+        return;
+
+    if((dst_df & eax_primary_fx_slot_id_dirty_bit) != EaxDirtyFlags{})
+        eax_context_commit_primary_fx_slot_id();
+
+    if((dst_df & eax_distance_factor_dirty_bit) != EaxDirtyFlags{})
+        eax_context_commit_distance_factor();
+
+    if((dst_df & eax_air_absorption_hf_dirty_bit) != EaxDirtyFlags{})
+        eax_context_commit_air_absorbtion_hf();
+
+    if((dst_df & eax_hf_reference_dirty_bit) != EaxDirtyFlags{})
+        eax_context_commit_hf_reference();
+
+    if((dst_df & eax_macro_fx_factor_dirty_bit) != EaxDirtyFlags{})
+        eax_context_commit_macro_fx_factor();
+
+    if((dst_df & eax_primary_fx_slot_id_dirty_bit) != EaxDirtyFlags{})
+        eax_update_sources();
+}
+
+void ALCcontext::eaxCommit()
+{
+    mEaxNeedsCommit = false;
+    eax_context_commit();
+    eaxCommitFxSlots();
+    eax_update_sources();
+}
+
+namespace {
+
+class EaxSetException : public EaxException {
+public:
+    explicit EaxSetException(const char* message)
+        : EaxException{"EAX_SET", message}
+    {}
+};
+
+[[noreturn]] void eax_fail_set(const char* message)
+{
+    throw EaxSetException{message};
+}
+
+class EaxGetException : public EaxException {
+public:
+    explicit EaxGetException(const char* message)
+        : EaxException{"EAX_GET", message}
+    {}
+};
+
+[[noreturn]] void eax_fail_get(const char* message)
+{
+    throw EaxGetException{message};
+}
+
+} // namespace
+
+
+FORCE_ALIGN ALenum AL_APIENTRY EAXSet(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_value,
+    ALuint property_value_size) noexcept
+try
+{
+    auto context = GetContextRef();
+
+    if(!context)
+        eax_fail_set("No current context.");
+
+    std::lock_guard<std::mutex> prop_lock{context->mPropLock};
+
+    return context->eax_eax_set(
+        property_set_id,
+        property_id,
+        property_source_id,
+        property_value,
+        property_value_size);
+}
+catch (...)
+{
+    eax_log_exception(__func__);
+    return AL_INVALID_OPERATION;
+}
+
+FORCE_ALIGN ALenum AL_APIENTRY EAXGet(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_value,
+    ALuint property_value_size) noexcept
+try
+{
+    auto context = GetContextRef();
+
+    if(!context)
+        eax_fail_get("No current context.");
+
+    std::lock_guard<std::mutex> prop_lock{context->mPropLock};
+
+    return context->eax_eax_get(
+        property_set_id,
+        property_id,
+        property_source_id,
+        property_value,
+        property_value_size);
+}
+catch (...)
+{
+    eax_log_exception(__func__);
+    return AL_INVALID_OPERATION;
+}
+#endif // ALSOFT_EAX
diff --git a/alc/context.h b/alc/context.h
new file mode 100644 (file)
index 0000000..e8efdbf
--- /dev/null
@@ -0,0 +1,540 @@
+#ifndef ALC_CONTEXT_H
+#define ALC_CONTEXT_H
+
+#include <atomic>
+#include <memory>
+#include <mutex>
+#include <stdint.h>
+#include <utility>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "al/listener.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "atomic.h"
+#include "core/context.h"
+#include "intrusive_ptr.h"
+#include "vector.h"
+
+#ifdef ALSOFT_EAX
+#include "al/eax/call.h"
+#include "al/eax/exception.h"
+#include "al/eax/fx_slot_index.h"
+#include "al/eax/fx_slots.h"
+#include "al/eax/utils.h"
+#endif // ALSOFT_EAX
+
+struct ALeffect;
+struct ALeffectslot;
+struct ALsource;
+
+using uint = unsigned int;
+
+
+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>, ContextBase {
+    const al::intrusive_ptr<ALCdevice> mALDevice;
+
+
+    bool mPropsDirty{true};
+    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};
+    float mAirAbsorptionGainHF{AirAbsorbGainHF};
+
+    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};
+
+    std::string mExtensionListOverride{};
+
+
+    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. mPropLock must be held when called.
+     */
+    void deferUpdates() noexcept { mDeferUpdates = true; }
+
+    /**
+     * Resumes update processing after being deferred. mPropLock must be held
+     * when called.
+     */
+    void processUpdates()
+    {
+        if(std::exchange(mDeferUpdates, false))
+            applyAllUpdates();
+    }
+
+    /**
+     * Applies all pending updates for the context, listener, effect slots, and
+     * sources.
+     */
+    void applyAllUpdates();
+
+#ifdef __USE_MINGW_ANSI_STDIO
+    [[gnu::format(gnu_printf, 3, 4)]]
+#else
+    [[gnu::format(printf, 3, 4)]]
+#endif
+    void setError(ALenum errorCode, const char *msg, ...);
+
+    /* Process-wide current context */
+    static std::atomic<bool> sGlobalContextLock;
+    static std::atomic<ALCcontext*> sGlobalContext;
+
+private:
+    /* Thread-local current context. */
+    static thread_local ALCcontext *sLocalContext;
+
+    /* Thread-local context handling. This handles attempting to release the
+     * context which may have been left current when the thread is destroyed.
+     */
+    class ThreadCtx {
+    public:
+        ~ThreadCtx();
+        void set(ALCcontext *ctx) const noexcept { sLocalContext = ctx; }
+    };
+    static thread_local ThreadCtx sThreadContext;
+
+public:
+    /* HACK: MinGW generates bad code when accessing an extern thread_local
+     * object. Add a wrapper function for it that only accesses it where it's
+     * defined.
+     */
+#ifdef __MINGW32__
+    static ALCcontext *getThreadContext() noexcept;
+    static void setThreadContext(ALCcontext *context) noexcept;
+#else
+    static ALCcontext *getThreadContext() noexcept { return sLocalContext; }
+    static void setThreadContext(ALCcontext *context) noexcept { sThreadContext.set(context); }
+#endif
+
+    /* Default effect that applies to sources that don't have an effect on send 0. */
+    static ALeffect sDefaultEffect;
+
+    DEF_NEWDEL(ALCcontext)
+
+#ifdef ALSOFT_EAX
+public:
+    bool hasEax() const noexcept { return mEaxIsInitialized; }
+    bool eaxIsCapable() const noexcept;
+
+    void eaxUninitialize() noexcept;
+
+    ALenum eax_eax_set(
+        const GUID* property_set_id,
+        ALuint property_id,
+        ALuint property_source_id,
+        ALvoid* property_value,
+        ALuint property_value_size);
+
+    ALenum eax_eax_get(
+        const GUID* property_set_id,
+        ALuint property_id,
+        ALuint property_source_id,
+        ALvoid* property_value,
+        ALuint property_value_size);
+
+    void eaxSetLastError() noexcept;
+
+    EaxFxSlotIndex eaxGetPrimaryFxSlotIndex() const noexcept
+    { return mEaxPrimaryFxSlotIndex; }
+
+    const ALeffectslot& eaxGetFxSlot(EaxFxSlotIndexValue fx_slot_index) const
+    { return mEaxFxSlots.get(fx_slot_index); }
+    ALeffectslot& eaxGetFxSlot(EaxFxSlotIndexValue fx_slot_index)
+    { return mEaxFxSlots.get(fx_slot_index); }
+
+    bool eaxNeedsCommit() const noexcept { return mEaxNeedsCommit; }
+    void eaxCommit();
+
+    void eaxCommitFxSlots()
+    { mEaxFxSlots.commit(); }
+
+private:
+    static constexpr auto eax_primary_fx_slot_id_dirty_bit = EaxDirtyFlags{1} << 0;
+    static constexpr auto eax_distance_factor_dirty_bit = EaxDirtyFlags{1} << 1;
+    static constexpr auto eax_air_absorption_hf_dirty_bit = EaxDirtyFlags{1} << 2;
+    static constexpr auto eax_hf_reference_dirty_bit = EaxDirtyFlags{1} << 3;
+    static constexpr auto eax_macro_fx_factor_dirty_bit = EaxDirtyFlags{1} << 4;
+
+    using Eax4Props = EAX40CONTEXTPROPERTIES;
+
+    struct Eax4State {
+        Eax4Props i; // Immediate.
+        Eax4Props d; // Deferred.
+    };
+
+    using Eax5Props = EAX50CONTEXTPROPERTIES;
+
+    struct Eax5State {
+        Eax5Props i; // Immediate.
+        Eax5Props d; // Deferred.
+    };
+
+    class ContextException : public EaxException
+    {
+    public:
+        explicit ContextException(const char* message)
+            : EaxException{"EAX_CONTEXT", message}
+        {}
+    };
+
+    struct Eax4PrimaryFxSlotIdValidator {
+        void operator()(const GUID& guidPrimaryFXSlotID) const
+        {
+            if(guidPrimaryFXSlotID != EAX_NULL_GUID &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX40_FXSlot0 &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX40_FXSlot1 &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX40_FXSlot2 &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX40_FXSlot3)
+            {
+                eax_fail_unknown_primary_fx_slot_id();
+            }
+        }
+    };
+
+    struct Eax4DistanceFactorValidator {
+        void operator()(float flDistanceFactor) const
+        {
+            eax_validate_range<ContextException>(
+                "Distance Factor",
+                flDistanceFactor,
+                EAXCONTEXT_MINDISTANCEFACTOR,
+                EAXCONTEXT_MAXDISTANCEFACTOR);
+        }
+    };
+
+    struct Eax4AirAbsorptionHfValidator {
+        void operator()(float flAirAbsorptionHF) const
+        {
+            eax_validate_range<ContextException>(
+                "Air Absorption HF",
+                flAirAbsorptionHF,
+                EAXCONTEXT_MINAIRABSORPTIONHF,
+                EAXCONTEXT_MAXAIRABSORPTIONHF);
+        }
+    };
+
+    struct Eax4HfReferenceValidator {
+        void operator()(float flHFReference) const
+        {
+            eax_validate_range<ContextException>(
+                "HF Reference",
+                flHFReference,
+                EAXCONTEXT_MINHFREFERENCE,
+                EAXCONTEXT_MAXHFREFERENCE);
+        }
+    };
+
+    struct Eax4AllValidator {
+        void operator()(const EAX40CONTEXTPROPERTIES& all) const
+        {
+            Eax4PrimaryFxSlotIdValidator{}(all.guidPrimaryFXSlotID);
+            Eax4DistanceFactorValidator{}(all.flDistanceFactor);
+            Eax4AirAbsorptionHfValidator{}(all.flAirAbsorptionHF);
+            Eax4HfReferenceValidator{}(all.flHFReference);
+        }
+    };
+
+    struct Eax5PrimaryFxSlotIdValidator {
+        void operator()(const GUID& guidPrimaryFXSlotID) const
+        {
+            if(guidPrimaryFXSlotID != EAX_NULL_GUID &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX50_FXSlot0 &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX50_FXSlot1 &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX50_FXSlot2 &&
+                guidPrimaryFXSlotID != EAXPROPERTYID_EAX50_FXSlot3)
+            {
+                eax_fail_unknown_primary_fx_slot_id();
+            }
+        }
+    };
+
+    struct Eax5MacroFxFactorValidator {
+        void operator()(float flMacroFXFactor) const
+        {
+            eax_validate_range<ContextException>(
+                "Macro FX Factor",
+                flMacroFXFactor,
+                EAXCONTEXT_MINMACROFXFACTOR,
+                EAXCONTEXT_MAXMACROFXFACTOR);
+        }
+    };
+
+    struct Eax5AllValidator {
+        void operator()(const EAX50CONTEXTPROPERTIES& all) const
+        {
+            Eax5PrimaryFxSlotIdValidator{}(all.guidPrimaryFXSlotID);
+            Eax4DistanceFactorValidator{}(all.flDistanceFactor);
+            Eax4AirAbsorptionHfValidator{}(all.flAirAbsorptionHF);
+            Eax4HfReferenceValidator{}(all.flHFReference);
+            Eax5MacroFxFactorValidator{}(all.flMacroFXFactor);
+        }
+    };
+
+    struct Eax5EaxVersionValidator {
+        void operator()(unsigned long ulEAXVersion) const
+        {
+            eax_validate_range<ContextException>(
+                "EAX version",
+                ulEAXVersion,
+                EAXCONTEXT_MINEAXSESSION,
+                EAXCONTEXT_MAXEAXSESSION);
+        }
+    };
+
+    struct Eax5MaxActiveSendsValidator {
+        void operator()(unsigned long ulMaxActiveSends) const
+        {
+            eax_validate_range<ContextException>(
+                "Max Active Sends",
+                ulMaxActiveSends,
+                EAXCONTEXT_MINMAXACTIVESENDS,
+                EAXCONTEXT_MAXMAXACTIVESENDS);
+        }
+    };
+
+    struct Eax5SessionAllValidator {
+        void operator()(const EAXSESSIONPROPERTIES& all) const
+        {
+            Eax5EaxVersionValidator{}(all.ulEAXVersion);
+            Eax5MaxActiveSendsValidator{}(all.ulMaxActiveSends);
+        }
+    };
+
+    struct Eax5SpeakerConfigValidator {
+        void operator()(unsigned long ulSpeakerConfig) const
+        {
+            eax_validate_range<ContextException>(
+                "Speaker Config",
+                ulSpeakerConfig,
+                EAXCONTEXT_MINSPEAKERCONFIG,
+                EAXCONTEXT_MAXSPEAKERCONFIG);
+        }
+    };
+
+    bool mEaxIsInitialized{};
+    bool mEaxIsTried{};
+
+    long mEaxLastError{};
+    unsigned long mEaxSpeakerConfig{};
+
+    EaxFxSlotIndex mEaxPrimaryFxSlotIndex{};
+    EaxFxSlots mEaxFxSlots{};
+
+    int mEaxVersion{}; // Current EAX version.
+    bool mEaxNeedsCommit{};
+    EaxDirtyFlags mEaxDf{}; // Dirty flags for the current EAX version.
+    Eax5State mEax123{}; // EAX1/EAX2/EAX3 state.
+    Eax4State mEax4{}; // EAX4 state.
+    Eax5State mEax5{}; // EAX5 state.
+    Eax5Props mEax{}; // Current EAX state.
+    EAXSESSIONPROPERTIES mEaxSession{};
+
+    [[noreturn]] static void eax_fail(const char* message);
+    [[noreturn]] static void eax_fail_unknown_property_set_id();
+    [[noreturn]] static void eax_fail_unknown_primary_fx_slot_id();
+    [[noreturn]] static void eax_fail_unknown_property_id();
+    [[noreturn]] static void eax_fail_unknown_version();
+
+    // Gets a value from EAX call,
+    // validates it,
+    // and updates the current value.
+    template<typename TValidator, typename TProperty>
+    static void eax_set(const EaxCall& call, TProperty& property)
+    {
+        const auto& value = call.get_value<ContextException, const TProperty>();
+        TValidator{}(value);
+        property = value;
+    }
+
+    // Gets a new value from EAX call,
+    // validates it,
+    // updates the deferred value,
+    // updates a dirty flag.
+    template<
+        typename TValidator,
+        EaxDirtyFlags TDirtyBit,
+        typename TMemberResult,
+        typename TProps,
+        typename TState>
+    void eax_defer(const EaxCall& call, TState& state, TMemberResult TProps::*member) noexcept
+    {
+        const auto& src = call.get_value<ContextException, const TMemberResult>();
+        TValidator{}(src);
+        const auto& dst_i = state.i.*member;
+        auto& dst_d = state.d.*member;
+        dst_d = src;
+
+        if(dst_i != dst_d)
+            mEaxDf |= TDirtyBit;
+    }
+
+    template<
+        EaxDirtyFlags TDirtyBit,
+        typename TMemberResult,
+        typename TProps,
+        typename TState>
+    void eax_context_commit_property(TState& state, EaxDirtyFlags& dst_df,
+        TMemberResult TProps::*member) noexcept
+    {
+        if((mEaxDf & TDirtyBit) != EaxDirtyFlags{})
+        {
+            dst_df |= TDirtyBit;
+            const auto& src_d = state.d.*member;
+            state.i.*member = src_d;
+            mEax.*member = src_d;
+        }
+    }
+
+    void eax_initialize_extensions();
+    void eax_initialize();
+
+    bool eax_has_no_default_effect_slot() const noexcept;
+    void eax_ensure_no_default_effect_slot() const;
+    bool eax_has_enough_aux_sends() const noexcept;
+    void eax_ensure_enough_aux_sends() const;
+    void eax_ensure_compatibility();
+
+    unsigned long eax_detect_speaker_configuration() const;
+    void eax_update_speaker_configuration();
+
+    void eax_set_last_error_defaults() noexcept;
+    void eax_session_set_defaults() noexcept;
+    static void eax4_context_set_defaults(Eax4Props& props) noexcept;
+    static void eax4_context_set_defaults(Eax4State& state) noexcept;
+    static void eax5_context_set_defaults(Eax5Props& props) noexcept;
+    static void eax5_context_set_defaults(Eax5State& state) noexcept;
+    void eax_context_set_defaults();
+    void eax_set_defaults();
+
+    void eax_dispatch_fx_slot(const EaxCall& call);
+    void eax_dispatch_source(const EaxCall& call);
+
+    void eax_get_misc(const EaxCall& call);
+    void eax4_get(const EaxCall& call, const Eax4Props& props);
+    void eax5_get(const EaxCall& call, const Eax5Props& props);
+    void eax_get(const EaxCall& call);
+
+    void eax_context_commit_primary_fx_slot_id();
+    void eax_context_commit_distance_factor();
+    void eax_context_commit_air_absorbtion_hf();
+    void eax_context_commit_hf_reference();
+    void eax_context_commit_macro_fx_factor();
+
+    void eax_initialize_fx_slots();
+
+    void eax_update_sources();
+
+    void eax_set_misc(const EaxCall& call);
+    void eax4_defer_all(const EaxCall& call, Eax4State& state);
+    void eax4_defer(const EaxCall& call, Eax4State& state);
+    void eax5_defer_all(const EaxCall& call, Eax5State& state);
+    void eax5_defer(const EaxCall& call, Eax5State& state);
+    void eax_set(const EaxCall& call);
+
+    void eax4_context_commit(Eax4State& state, EaxDirtyFlags& dst_df);
+    void eax5_context_commit(Eax5State& state, EaxDirtyFlags& dst_df);
+    void eax_context_commit();
+#endif // ALSOFT_EAX
+};
+
+using ContextRef = al::intrusive_ptr<ALCcontext>;
+
+ContextRef GetContextRef(void);
+
+void UpdateContextProps(ALCcontext *context);
+
+
+extern bool TrapALError;
+
+
+#ifdef ALSOFT_EAX
+ALenum AL_APIENTRY EAXSet(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_value,
+    ALuint property_value_size) noexcept;
+
+ALenum AL_APIENTRY EAXGet(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_value,
+    ALuint property_value_size) noexcept;
+#endif // ALSOFT_EAX
+
+#endif /* ALC_CONTEXT_H */
diff --git a/alc/device.cpp b/alc/device.cpp
new file mode 100644 (file)
index 0000000..66b13c5
--- /dev/null
@@ -0,0 +1,93 @@
+
+#include "config.h"
+
+#include "device.h"
+
+#include <numeric>
+#include <stddef.h>
+
+#include "albit.h"
+#include "alconfig.h"
+#include "backends/base.h"
+#include "core/bformatdec.h"
+#include "core/bs2b.h"
+#include "core/front_stablizer.h"
+#include "core/hrtf.h"
+#include "core/logging.h"
+#include "core/mastering.h"
+#include "core/uhjfilter.h"
+
+
+namespace {
+
+using voidp = void*;
+
+} // namespace
+
+
+ALCdevice::ALCdevice(DeviceType type) : DeviceBase{type}
+{ }
+
+ALCdevice::~ALCdevice()
+{
+    TRACE("Freeing device %p\n", voidp{this});
+
+    Backend = nullptr;
+
+    size_t count{std::accumulate(BufferList.cbegin(), BufferList.cend(), size_t{0u},
+        [](size_t cur, const BufferSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<uint>(al::popcount(~sublist.FreeMask)); })};
+    if(count > 0)
+        WARN("%zu Buffer%s not deleted\n", count, (count==1)?"":"s");
+
+    count = std::accumulate(EffectList.cbegin(), EffectList.cend(), size_t{0u},
+        [](size_t cur, const EffectSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<uint>(al::popcount(~sublist.FreeMask)); });
+    if(count > 0)
+        WARN("%zu Effect%s not deleted\n", count, (count==1)?"":"s");
+
+    count = std::accumulate(FilterList.cbegin(), FilterList.cend(), size_t{0u},
+        [](size_t cur, const FilterSubList &sublist) noexcept -> size_t
+        { return cur + static_cast<uint>(al::popcount(~sublist.FreeMask)); });
+    if(count > 0)
+        WARN("%zu Filter%s not deleted\n", count, (count==1)?"":"s");
+}
+
+void ALCdevice::enumerateHrtfs()
+{
+    mHrtfList = EnumerateHrtf(configValue<std::string>(nullptr, "hrtf-paths"));
+    if(auto defhrtfopt = configValue<std::string>(nullptr, "default-hrtf"))
+    {
+        auto iter = std::find(mHrtfList.begin(), mHrtfList.end(), *defhrtfopt);
+        if(iter == mHrtfList.end())
+            WARN("Failed to find default HRTF \"%s\"\n", defhrtfopt->c_str());
+        else if(iter != mHrtfList.begin())
+            std::rotate(mHrtfList.begin(), iter, iter+1);
+    }
+}
+
+auto ALCdevice::getOutputMode1() const noexcept -> OutputMode1
+{
+    if(mContexts.load(std::memory_order_relaxed)->empty())
+        return OutputMode1::Any;
+
+    switch(FmtChans)
+    {
+    case DevFmtMono: return OutputMode1::Mono;
+    case DevFmtStereo:
+        if(mHrtf)
+            return OutputMode1::Hrtf;
+        else if(mUhjEncoder)
+            return OutputMode1::Uhj2;
+        return OutputMode1::StereoBasic;
+    case DevFmtQuad: return OutputMode1::Quad;
+    case DevFmtX51: return OutputMode1::X51;
+    case DevFmtX61: return OutputMode1::X61;
+    case DevFmtX71: return OutputMode1::X71;
+    case DevFmtX714:
+    case DevFmtX3D71:
+    case DevFmtAmbi3D:
+        break;
+    }
+    return OutputMode1::Any;
+}
diff --git a/alc/device.h b/alc/device.h
new file mode 100644 (file)
index 0000000..ef50f53
--- /dev/null
@@ -0,0 +1,165 @@
+#ifndef ALC_DEVICE_H
+#define ALC_DEVICE_H
+
+#include <atomic>
+#include <memory>
+#include <mutex>
+#include <stdint.h>
+#include <string>
+#include <utility>
+
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "inprogext.h"
+#include "intrusive_ptr.h"
+#include "vector.h"
+
+#ifdef ALSOFT_EAX
+#include "al/eax/x_ram.h"
+#endif // ALSOFT_EAX
+
+struct ALbuffer;
+struct ALeffect;
+struct ALfilter;
+struct BackendBase;
+
+using uint = unsigned int;
+
+
+struct BufferSubList {
+    uint64_t FreeMask{~0_u64};
+    ALbuffer *Buffers{nullptr}; /* 64 */
+
+    BufferSubList() noexcept = default;
+    BufferSubList(const BufferSubList&) = delete;
+    BufferSubList(BufferSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Buffers{rhs.Buffers}
+    { rhs.FreeMask = ~0_u64; rhs.Buffers = nullptr; }
+    ~BufferSubList();
+
+    BufferSubList& operator=(const BufferSubList&) = delete;
+    BufferSubList& operator=(BufferSubList&& rhs) noexcept
+    { std::swap(FreeMask, rhs.FreeMask); std::swap(Buffers, rhs.Buffers); return *this; }
+};
+
+struct EffectSubList {
+    uint64_t FreeMask{~0_u64};
+    ALeffect *Effects{nullptr}; /* 64 */
+
+    EffectSubList() noexcept = default;
+    EffectSubList(const EffectSubList&) = delete;
+    EffectSubList(EffectSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Effects{rhs.Effects}
+    { rhs.FreeMask = ~0_u64; rhs.Effects = nullptr; }
+    ~EffectSubList();
+
+    EffectSubList& operator=(const EffectSubList&) = delete;
+    EffectSubList& operator=(EffectSubList&& rhs) noexcept
+    { std::swap(FreeMask, rhs.FreeMask); std::swap(Effects, rhs.Effects); return *this; }
+};
+
+struct FilterSubList {
+    uint64_t FreeMask{~0_u64};
+    ALfilter *Filters{nullptr}; /* 64 */
+
+    FilterSubList() noexcept = default;
+    FilterSubList(const FilterSubList&) = delete;
+    FilterSubList(FilterSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Filters{rhs.Filters}
+    { rhs.FreeMask = ~0_u64; rhs.Filters = nullptr; }
+    ~FilterSubList();
+
+    FilterSubList& operator=(const FilterSubList&) = delete;
+    FilterSubList& operator=(FilterSubList&& rhs) noexcept
+    { std::swap(FreeMask, rhs.FreeMask); std::swap(Filters, rhs.Filters); return *this; }
+};
+
+
+struct ALCdevice : public al::intrusive_ref<ALCdevice>, DeviceBase {
+    /* This lock protects the device state (format, update size, etc) from
+     * being from being changed in multiple threads, or being accessed while
+     * being changed. It's also used to serialize calls to the backend.
+     */
+    std::mutex StateLock;
+    std::unique_ptr<BackendBase> Backend;
+
+    ALCuint NumMonoSources{};
+    ALCuint NumStereoSources{};
+
+    // Maximum number of sources that can be created
+    uint SourcesMax{};
+    // Maximum number of slots that can be created
+    uint AuxiliaryEffectSlotMax{};
+
+    std::string mHrtfName;
+    al::vector<std::string> mHrtfList;
+    ALCenum mHrtfStatus{ALC_FALSE};
+
+    enum class OutputMode1 : ALCenum {
+        Any = ALC_ANY_SOFT,
+        Mono = ALC_MONO_SOFT,
+        Stereo = ALC_STEREO_SOFT,
+        StereoBasic = ALC_STEREO_BASIC_SOFT,
+        Uhj2 = ALC_STEREO_UHJ_SOFT,
+        Hrtf = ALC_STEREO_HRTF_SOFT,
+        Quad = ALC_QUAD_SOFT,
+        X51 = ALC_SURROUND_5_1_SOFT,
+        X61 = ALC_SURROUND_6_1_SOFT,
+        X71 = ALC_SURROUND_7_1_SOFT
+    };
+    OutputMode1 getOutputMode1() const noexcept;
+
+    using OutputMode = OutputMode1;
+
+    std::atomic<ALCenum> LastError{ALC_NO_ERROR};
+
+    // Map of Buffers for this device
+    std::mutex BufferLock;
+    al::vector<BufferSubList> BufferList;
+
+    // Map of Effects for this device
+    std::mutex EffectLock;
+    al::vector<EffectSubList> EffectList;
+
+    // Map of Filters for this device
+    std::mutex FilterLock;
+    al::vector<FilterSubList> FilterList;
+
+#ifdef ALSOFT_EAX
+    ALuint eax_x_ram_free_size{eax_x_ram_max_size};
+#endif // ALSOFT_EAX
+
+
+    ALCdevice(DeviceType type);
+    ~ALCdevice();
+
+    void enumerateHrtfs();
+
+    bool getConfigValueBool(const char *block, const char *key, bool def)
+    { return GetConfigValueBool(DeviceName.c_str(), block, key, def); }
+
+    template<typename T>
+    inline al::optional<T> configValue(const char *block, const char *key) = delete;
+
+    DEF_NEWDEL(ALCdevice)
+};
+
+template<>
+inline al::optional<std::string> ALCdevice::configValue(const char *block, const char *key)
+{ return ConfigValueStr(DeviceName.c_str(), block, key); }
+template<>
+inline al::optional<int> ALCdevice::configValue(const char *block, const char *key)
+{ return ConfigValueInt(DeviceName.c_str(), block, key); }
+template<>
+inline al::optional<uint> ALCdevice::configValue(const char *block, const char *key)
+{ return ConfigValueUInt(DeviceName.c_str(), block, key); }
+template<>
+inline al::optional<float> ALCdevice::configValue(const char *block, const char *key)
+{ return ConfigValueFloat(DeviceName.c_str(), block, key); }
+template<>
+inline al::optional<bool> ALCdevice::configValue(const char *block, const char *key)
+{ return ConfigValueBool(DeviceName.c_str(), block, key); }
+
+#endif
diff --git a/alc/effects/autowah.cpp b/alc/effects/autowah.cpp
new file mode 100644 (file)
index 0000000..4f874ef
--- /dev/null
@@ -0,0 +1,235 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2018 by Raul Herraiz.
+ * 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 <algorithm>
+#include <array>
+#include <cstdlib>
+#include <iterator>
+#include <utility>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/mixer.h"
+#include "intrusive_ptr.h"
+
+
+namespace {
+
+constexpr float GainScale{31621.0f};
+constexpr float MinFreq{20.0f};
+constexpr float MaxFreq{2500.0f};
+constexpr float QFactor{5.0f};
+
+struct AutowahState final : public EffectState {
+    /* Effect parameters */
+    float mAttackRate;
+    float mReleaseRate;
+    float mResonanceGain;
+    float mPeakGain;
+    float mFreqMinNorm;
+    float mBandwidthNorm;
+    float mEnvDelay;
+
+    /* Filter components derived from the envelope. */
+    struct {
+        float cos_w0;
+        float alpha;
+    } mEnv[BufferLineSize];
+
+    struct {
+        uint mTargetChannel{InvalidChannelIndex};
+
+        /* Effect filters' history. */
+        struct {
+            float z1, z2;
+        } mFilter;
+
+        /* Effect gains for each output channel */
+        float mCurrentGain;
+        float mTargetGain;
+    } mChans[MaxAmbiChannels];
+
+    /* Effects buffers */
+    alignas(16) float mBufferOut[BufferLineSize];
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(AutowahState)
+};
+
+void AutowahState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    /* (Re-)initializing parameters and clear the buffers. */
+
+    mAttackRate    = 1.0f;
+    mReleaseRate   = 1.0f;
+    mResonanceGain = 10.0f;
+    mPeakGain      = 4.5f;
+    mFreqMinNorm   = 4.5e-4f;
+    mBandwidthNorm = 0.05f;
+    mEnvDelay      = 0.0f;
+
+    for(auto &e : mEnv)
+    {
+        e.cos_w0 = 0.0f;
+        e.alpha = 0.0f;
+    }
+
+    for(auto &chan : mChans)
+    {
+        chan.mTargetChannel = InvalidChannelIndex;
+        chan.mFilter.z1 = 0.0f;
+        chan.mFilter.z2 = 0.0f;
+        chan.mCurrentGain = 0.0f;
+    }
+}
+
+void AutowahState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *device{context->mDevice};
+    const auto frequency = static_cast<float>(device->Frequency);
+
+    const float ReleaseTime{clampf(props->Autowah.ReleaseTime, 0.001f, 1.0f)};
+
+    mAttackRate    = std::exp(-1.0f / (props->Autowah.AttackTime*frequency));
+    mReleaseRate   = std::exp(-1.0f / (ReleaseTime*frequency));
+    /* 0-20dB Resonance Peak gain */
+    mResonanceGain = std::sqrt(std::log10(props->Autowah.Resonance)*10.0f / 3.0f);
+    mPeakGain      = 1.0f - std::log10(props->Autowah.PeakGain / GainScale);
+    mFreqMinNorm   = MinFreq / frequency;
+    mBandwidthNorm = (MaxFreq-MinFreq) / frequency;
+
+    mOutTarget = target.Main->Buffer;
+    auto set_channel = [this](size_t idx, uint outchan, float outgain)
+    {
+        mChans[idx].mTargetChannel = outchan;
+        mChans[idx].mTargetGain = outgain;
+    };
+    target.Main->setAmbiMixParams(slot->Wet, slot->Gain, set_channel);
+}
+
+void AutowahState::process(const size_t samplesToDo,
+    const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    const float attack_rate{mAttackRate};
+    const float release_rate{mReleaseRate};
+    const float res_gain{mResonanceGain};
+    const float peak_gain{mPeakGain};
+    const float freq_min{mFreqMinNorm};
+    const float bandwidth{mBandwidthNorm};
+
+    float env_delay{mEnvDelay};
+    for(size_t i{0u};i < samplesToDo;i++)
+    {
+        float w0, sample, a;
+
+        /* Envelope follower described on the book: Audio Effects, Theory,
+         * Implementation and Application.
+         */
+        sample = peak_gain * std::fabs(samplesIn[0][i]);
+        a = (sample > env_delay) ? attack_rate : release_rate;
+        env_delay = lerpf(sample, env_delay, a);
+
+        /* Calculate the cos and alpha components for this sample's filter. */
+        w0 = minf((bandwidth*env_delay + freq_min), 0.46f) * (al::numbers::pi_v<float>*2.0f);
+        mEnv[i].cos_w0 = std::cos(w0);
+        mEnv[i].alpha = std::sin(w0)/(2.0f * QFactor);
+    }
+    mEnvDelay = env_delay;
+
+    auto chandata = std::begin(mChans);
+    for(const auto &insamples : samplesIn)
+    {
+        const size_t outidx{chandata->mTargetChannel};
+        if(outidx == InvalidChannelIndex)
+        {
+            ++chandata;
+            continue;
+        }
+
+        /* This effectively inlines BiquadFilter_setParams for a peaking
+         * filter and BiquadFilter_processC. The alpha and cosine components
+         * for the filter coefficients were previously calculated with the
+         * envelope. Because the filter changes for each sample, the
+         * coefficients are transient and don't need to be held.
+         */
+        float z1{chandata->mFilter.z1};
+        float z2{chandata->mFilter.z2};
+
+        for(size_t i{0u};i < samplesToDo;i++)
+        {
+            const float alpha{mEnv[i].alpha};
+            const float cos_w0{mEnv[i].cos_w0};
+            float input, output;
+            float a[3], b[3];
+
+            b[0] =  1.0f + alpha*res_gain;
+            b[1] = -2.0f * cos_w0;
+            b[2] =  1.0f - alpha*res_gain;
+            a[0] =  1.0f + alpha/res_gain;
+            a[1] = -2.0f * cos_w0;
+            a[2] =  1.0f - alpha/res_gain;
+
+            input = insamples[i];
+            output = input*(b[0]/a[0]) + z1;
+            z1 = input*(b[1]/a[0]) - output*(a[1]/a[0]) + z2;
+            z2 = input*(b[2]/a[0]) - output*(a[2]/a[0]);
+            mBufferOut[i] = output;
+        }
+        chandata->mFilter.z1 = z1;
+        chandata->mFilter.z2 = z2;
+
+        /* Now, mix the processed sound data to the output. */
+        MixSamples({mBufferOut, samplesToDo}, samplesOut[outidx].data(), chandata->mCurrentGain,
+            chandata->mTargetGain, samplesToDo);
+        ++chandata;
+    }
+}
+
+
+struct AutowahStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new AutowahState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *AutowahStateFactory_getFactory()
+{
+    static AutowahStateFactory AutowahFactory{};
+    return &AutowahFactory;
+}
diff --git a/alc/effects/base.h b/alc/effects/base.h
new file mode 100644 (file)
index 0000000..9569585
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef EFFECTS_BASE_H
+#define EFFECTS_BASE_H
+
+#include "core/effects/base.h"
+
+
+EffectStateFactory *NullStateFactory_getFactory(void);
+EffectStateFactory *ReverbStateFactory_getFactory(void);
+EffectStateFactory *StdReverbStateFactory_getFactory(void);
+EffectStateFactory *AutowahStateFactory_getFactory(void);
+EffectStateFactory *ChorusStateFactory_getFactory(void);
+EffectStateFactory *CompressorStateFactory_getFactory(void);
+EffectStateFactory *DistortionStateFactory_getFactory(void);
+EffectStateFactory *EchoStateFactory_getFactory(void);
+EffectStateFactory *EqualizerStateFactory_getFactory(void);
+EffectStateFactory *FlangerStateFactory_getFactory(void);
+EffectStateFactory *FshifterStateFactory_getFactory(void);
+EffectStateFactory *ModulatorStateFactory_getFactory(void);
+EffectStateFactory *PshifterStateFactory_getFactory(void);
+EffectStateFactory* VmorpherStateFactory_getFactory(void);
+
+EffectStateFactory *DedicatedStateFactory_getFactory(void);
+
+EffectStateFactory *ConvolutionStateFactory_getFactory(void);
+
+#endif /* EFFECTS_BASE_H */
diff --git a/alc/effects/chorus.cpp b/alc/effects/chorus.cpp
new file mode 100644 (file)
index 0000000..10ccf9f
--- /dev/null
@@ -0,0 +1,330 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2013 by Mike Gorchak
+ * 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 <algorithm>
+#include <array>
+#include <climits>
+#include <cstdlib>
+#include <iterator>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/mixer.h"
+#include "core/mixer/defs.h"
+#include "core/resampler_limits.h"
+#include "intrusive_ptr.h"
+#include "opthelpers.h"
+#include "vector.h"
+
+
+namespace {
+
+using uint = unsigned int;
+
+struct ChorusState final : public EffectState {
+    al::vector<float,16> mDelayBuffer;
+    uint mOffset{0};
+
+    uint mLfoOffset{0};
+    uint mLfoRange{1};
+    float mLfoScale{0.0f};
+    uint mLfoDisp{0};
+
+    /* Calculated delays to apply to the left and right outputs. */
+    uint mModDelays[2][BufferLineSize];
+
+    /* Temp storage for the modulated left and right outputs. */
+    alignas(16) float mBuffer[2][BufferLineSize];
+
+    /* Gains for left and right outputs. */
+    struct {
+        float Current[MaxAmbiChannels]{};
+        float Target[MaxAmbiChannels]{};
+    } mGains[2];
+
+    /* effect parameters */
+    ChorusWaveform mWaveform{};
+    int mDelay{0};
+    float mDepth{0.0f};
+    float mFeedback{0.0f};
+
+    void calcTriangleDelays(const size_t todo);
+    void calcSinusoidDelays(const size_t todo);
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(ChorusState)
+};
+
+void ChorusState::deviceUpdate(const DeviceBase *Device, const BufferStorage*)
+{
+    constexpr float max_delay{maxf(ChorusMaxDelay, FlangerMaxDelay)};
+
+    const auto frequency = static_cast<float>(Device->Frequency);
+    const size_t maxlen{NextPowerOf2(float2uint(max_delay*2.0f*frequency) + 1u)};
+    if(maxlen != mDelayBuffer.size())
+        decltype(mDelayBuffer)(maxlen).swap(mDelayBuffer);
+
+    std::fill(mDelayBuffer.begin(), mDelayBuffer.end(), 0.0f);
+    for(auto &e : mGains)
+    {
+        std::fill(std::begin(e.Current), std::end(e.Current), 0.0f);
+        std::fill(std::begin(e.Target), std::end(e.Target), 0.0f);
+    }
+}
+
+void ChorusState::update(const ContextBase *Context, const EffectSlot *Slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    constexpr int mindelay{(MaxResamplerPadding>>1) << MixerFracBits};
+
+    /* The LFO depth is scaled to be relative to the sample delay. Clamp the
+     * delay and depth to allow enough padding for resampling.
+     */
+    const DeviceBase *device{Context->mDevice};
+    const auto frequency = static_cast<float>(device->Frequency);
+
+    mWaveform = props->Chorus.Waveform;
+
+    mDelay = maxi(float2int(props->Chorus.Delay*frequency*MixerFracOne + 0.5f), mindelay);
+    mDepth = minf(props->Chorus.Depth * static_cast<float>(mDelay),
+        static_cast<float>(mDelay - mindelay));
+
+    mFeedback = props->Chorus.Feedback;
+
+    /* Gains for left and right sides */
+    static constexpr auto inv_sqrt2 = static_cast<float>(1.0 / al::numbers::sqrt2);
+    static constexpr auto lcoeffs_pw = CalcDirectionCoeffs({-1.0f, 0.0f, 0.0f});
+    static constexpr auto rcoeffs_pw = CalcDirectionCoeffs({ 1.0f, 0.0f, 0.0f});
+    static constexpr auto lcoeffs_nrml = CalcDirectionCoeffs({-inv_sqrt2, 0.0f, inv_sqrt2});
+    static constexpr auto rcoeffs_nrml = CalcDirectionCoeffs({ inv_sqrt2, 0.0f, inv_sqrt2});
+    auto &lcoeffs = (device->mRenderMode != RenderMode::Pairwise) ? lcoeffs_nrml : lcoeffs_pw;
+    auto &rcoeffs = (device->mRenderMode != RenderMode::Pairwise) ? rcoeffs_nrml : rcoeffs_pw;
+
+    mOutTarget = target.Main->Buffer;
+    ComputePanGains(target.Main, lcoeffs.data(), Slot->Gain, mGains[0].Target);
+    ComputePanGains(target.Main, rcoeffs.data(), Slot->Gain, mGains[1].Target);
+
+    float rate{props->Chorus.Rate};
+    if(!(rate > 0.0f))
+    {
+        mLfoOffset = 0;
+        mLfoRange = 1;
+        mLfoScale = 0.0f;
+        mLfoDisp = 0;
+    }
+    else
+    {
+        /* Calculate LFO coefficient (number of samples per cycle). Limit the
+         * max range to avoid overflow when calculating the displacement.
+         */
+        uint lfo_range{float2uint(minf(frequency/rate + 0.5f, float{INT_MAX/360 - 180}))};
+
+        mLfoOffset = mLfoOffset * lfo_range / mLfoRange;
+        mLfoRange = lfo_range;
+        switch(mWaveform)
+        {
+        case ChorusWaveform::Triangle:
+            mLfoScale = 4.0f / static_cast<float>(mLfoRange);
+            break;
+        case ChorusWaveform::Sinusoid:
+            mLfoScale = al::numbers::pi_v<float>*2.0f / static_cast<float>(mLfoRange);
+            break;
+        }
+
+        /* Calculate lfo phase displacement */
+        int phase{props->Chorus.Phase};
+        if(phase < 0) phase = 360 + phase;
+        mLfoDisp = (mLfoRange*static_cast<uint>(phase) + 180) / 360;
+    }
+}
+
+
+void ChorusState::calcTriangleDelays(const size_t todo)
+{
+    const uint lfo_range{mLfoRange};
+    const float lfo_scale{mLfoScale};
+    const float depth{mDepth};
+    const int delay{mDelay};
+
+    ASSUME(lfo_range > 0);
+    ASSUME(todo > 0);
+
+    auto gen_lfo = [lfo_scale,depth,delay](const uint offset) -> uint
+    {
+        const float offset_norm{static_cast<float>(offset) * lfo_scale};
+        return static_cast<uint>(fastf2i((1.0f-std::abs(2.0f-offset_norm)) * depth) + delay);
+    };
+
+    uint offset{mLfoOffset};
+    for(size_t i{0};i < todo;)
+    {
+        size_t rem{minz(todo-i, lfo_range-offset)};
+        do {
+            mModDelays[0][i++] = gen_lfo(offset++);
+        } while(--rem);
+        if(offset == lfo_range)
+            offset = 0;
+    }
+
+    offset = (mLfoOffset+mLfoDisp) % lfo_range;
+    for(size_t i{0};i < todo;)
+    {
+        size_t rem{minz(todo-i, lfo_range-offset)};
+        do {
+            mModDelays[1][i++] = gen_lfo(offset++);
+        } while(--rem);
+        if(offset == lfo_range)
+            offset = 0;
+    }
+
+    mLfoOffset = static_cast<uint>(mLfoOffset+todo) % lfo_range;
+}
+
+void ChorusState::calcSinusoidDelays(const size_t todo)
+{
+    const uint lfo_range{mLfoRange};
+    const float lfo_scale{mLfoScale};
+    const float depth{mDepth};
+    const int delay{mDelay};
+
+    ASSUME(lfo_range > 0);
+    ASSUME(todo > 0);
+
+    auto gen_lfo = [lfo_scale,depth,delay](const uint offset) -> uint
+    {
+        const float offset_norm{static_cast<float>(offset) * lfo_scale};
+        return static_cast<uint>(fastf2i(std::sin(offset_norm)*depth) + delay);
+    };
+
+    uint offset{mLfoOffset};
+    for(size_t i{0};i < todo;)
+    {
+        size_t rem{minz(todo-i, lfo_range-offset)};
+        do {
+            mModDelays[0][i++] = gen_lfo(offset++);
+        } while(--rem);
+        if(offset == lfo_range)
+            offset = 0;
+    }
+
+    offset = (mLfoOffset+mLfoDisp) % lfo_range;
+    for(size_t i{0};i < todo;)
+    {
+        size_t rem{minz(todo-i, lfo_range-offset)};
+        do {
+            mModDelays[1][i++] = gen_lfo(offset++);
+        } while(--rem);
+        if(offset == lfo_range)
+            offset = 0;
+    }
+
+    mLfoOffset = static_cast<uint>(mLfoOffset+todo) % lfo_range;
+}
+
+void ChorusState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    const size_t bufmask{mDelayBuffer.size()-1};
+    const float feedback{mFeedback};
+    const uint avgdelay{(static_cast<uint>(mDelay) + MixerFracHalf) >> MixerFracBits};
+    float *RESTRICT delaybuf{mDelayBuffer.data()};
+    uint offset{mOffset};
+
+    if(mWaveform == ChorusWaveform::Sinusoid)
+        calcSinusoidDelays(samplesToDo);
+    else /*if(mWaveform == ChorusWaveform::Triangle)*/
+        calcTriangleDelays(samplesToDo);
+
+    const uint *RESTRICT ldelays{mModDelays[0]};
+    const uint *RESTRICT rdelays{mModDelays[1]};
+    float *RESTRICT lbuffer{al::assume_aligned<16>(mBuffer[0])};
+    float *RESTRICT rbuffer{al::assume_aligned<16>(mBuffer[1])};
+    for(size_t i{0u};i < samplesToDo;++i)
+    {
+        // Feed the buffer's input first (necessary for delays < 1).
+        delaybuf[offset&bufmask] = samplesIn[0][i];
+
+        // Tap for the left output.
+        uint delay{offset - (ldelays[i]>>MixerFracBits)};
+        float mu{static_cast<float>(ldelays[i]&MixerFracMask) * (1.0f/MixerFracOne)};
+        lbuffer[i] = cubic(delaybuf[(delay+1) & bufmask], delaybuf[(delay  ) & bufmask],
+            delaybuf[(delay-1) & bufmask], delaybuf[(delay-2) & bufmask], mu);
+
+        // Tap for the right output.
+        delay = offset - (rdelays[i]>>MixerFracBits);
+        mu = static_cast<float>(rdelays[i]&MixerFracMask) * (1.0f/MixerFracOne);
+        rbuffer[i] = cubic(delaybuf[(delay+1) & bufmask], delaybuf[(delay  ) & bufmask],
+            delaybuf[(delay-1) & bufmask], delaybuf[(delay-2) & bufmask], mu);
+
+        // Accumulate feedback from the average delay of the taps.
+        delaybuf[offset&bufmask] += delaybuf[(offset-avgdelay) & bufmask] * feedback;
+        ++offset;
+    }
+
+    MixSamples({lbuffer, samplesToDo}, samplesOut, mGains[0].Current, mGains[0].Target,
+        samplesToDo, 0);
+    MixSamples({rbuffer, samplesToDo}, samplesOut, mGains[1].Current, mGains[1].Target,
+        samplesToDo, 0);
+
+    mOffset = offset;
+}
+
+
+struct ChorusStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new ChorusState{}}; }
+};
+
+
+/* Flanger is basically a chorus with a really short delay. They can both use
+ * the same processing functions, so piggyback flanger on the chorus functions.
+ */
+struct FlangerStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new ChorusState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *ChorusStateFactory_getFactory()
+{
+    static ChorusStateFactory ChorusFactory{};
+    return &ChorusFactory;
+}
+
+EffectStateFactory *FlangerStateFactory_getFactory()
+{
+    static FlangerStateFactory FlangerFactory{};
+    return &FlangerFactory;
+}
diff --git a/alc/effects/compressor.cpp b/alc/effects/compressor.cpp
new file mode 100644 (file)
index 0000000..0a7ed67
--- /dev/null
@@ -0,0 +1,201 @@
+/**
+ * This file is part of the OpenAL Soft cross platform audio library
+ *
+ * Copyright (C) 2013 by Anis A. Hireche
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Spherical-Harmonic-Transform nor the names of its
+ *   contributors may be used to endorse or promote products derived from
+ *   this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#include <array>
+#include <cstdlib>
+#include <iterator>
+#include <utility>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/mixer.h"
+#include "core/mixer/defs.h"
+#include "intrusive_ptr.h"
+
+struct ContextBase;
+
+
+namespace {
+
+#define AMP_ENVELOPE_MIN  0.5f
+#define AMP_ENVELOPE_MAX  2.0f
+
+#define ATTACK_TIME  0.1f /* 100ms to rise from min to max */
+#define RELEASE_TIME 0.2f /* 200ms to drop from max to min */
+
+
+struct CompressorState final : public EffectState {
+    /* Effect gains for each channel */
+    struct {
+        uint mTarget{InvalidChannelIndex};
+        float mGain{1.0f};
+    } mChans[MaxAmbiChannels];
+
+    /* Effect parameters */
+    bool mEnabled{true};
+    float mAttackMult{1.0f};
+    float mReleaseMult{1.0f};
+    float mEnvFollower{1.0f};
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(CompressorState)
+};
+
+void CompressorState::deviceUpdate(const DeviceBase *device, const BufferStorage*)
+{
+    /* Number of samples to do a full attack and release (non-integer sample
+     * counts are okay).
+     */
+    const float attackCount{static_cast<float>(device->Frequency) * ATTACK_TIME};
+    const float releaseCount{static_cast<float>(device->Frequency) * RELEASE_TIME};
+
+    /* Calculate per-sample multipliers to attack and release at the desired
+     * rates.
+     */
+    mAttackMult  = std::pow(AMP_ENVELOPE_MAX/AMP_ENVELOPE_MIN, 1.0f/attackCount);
+    mReleaseMult = std::pow(AMP_ENVELOPE_MIN/AMP_ENVELOPE_MAX, 1.0f/releaseCount);
+}
+
+void CompressorState::update(const ContextBase*, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    mEnabled = props->Compressor.OnOff;
+
+    mOutTarget = target.Main->Buffer;
+    auto set_channel = [this](size_t idx, uint outchan, float outgain)
+    {
+        mChans[idx].mTarget = outchan;
+        mChans[idx].mGain = outgain;
+    };
+    target.Main->setAmbiMixParams(slot->Wet, slot->Gain, set_channel);
+}
+
+void CompressorState::process(const size_t samplesToDo,
+    const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    for(size_t base{0u};base < samplesToDo;)
+    {
+        float gains[256];
+        const size_t td{minz(256, samplesToDo-base)};
+
+        /* Generate the per-sample gains from the signal envelope. */
+        float env{mEnvFollower};
+        if(mEnabled)
+        {
+            for(size_t i{0u};i < td;++i)
+            {
+                /* Clamp the absolute amplitude to the defined envelope limits,
+                 * then attack or release the envelope to reach it.
+                 */
+                const float amplitude{clampf(std::fabs(samplesIn[0][base+i]), AMP_ENVELOPE_MIN,
+                    AMP_ENVELOPE_MAX)};
+                if(amplitude > env)
+                    env = minf(env*mAttackMult, amplitude);
+                else if(amplitude < env)
+                    env = maxf(env*mReleaseMult, amplitude);
+
+                /* Apply the reciprocal of the envelope to normalize the volume
+                 * (compress the dynamic range).
+                 */
+                gains[i] = 1.0f / env;
+            }
+        }
+        else
+        {
+            /* Same as above, except the amplitude is forced to 1. This helps
+             * ensure smooth gain changes when the compressor is turned on and
+             * off.
+             */
+            for(size_t i{0u};i < td;++i)
+            {
+                const float amplitude{1.0f};
+                if(amplitude > env)
+                    env = minf(env*mAttackMult, amplitude);
+                else if(amplitude < env)
+                    env = maxf(env*mReleaseMult, amplitude);
+
+                gains[i] = 1.0f / env;
+            }
+        }
+        mEnvFollower = env;
+
+        /* Now compress the signal amplitude to output. */
+        auto chan = std::cbegin(mChans);
+        for(const auto &input : samplesIn)
+        {
+            const size_t outidx{chan->mTarget};
+            if(outidx != InvalidChannelIndex)
+            {
+                const float *RESTRICT src{input.data() + base};
+                float *RESTRICT dst{samplesOut[outidx].data() + base};
+                const float gain{chan->mGain};
+                if(!(std::fabs(gain) > GainSilenceThreshold))
+                {
+                    for(size_t i{0u};i < td;i++)
+                        dst[i] += src[i] * gains[i] * gain;
+                }
+            }
+            ++chan;
+        }
+
+        base += td;
+    }
+}
+
+
+struct CompressorStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new CompressorState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *CompressorStateFactory_getFactory()
+{
+    static CompressorStateFactory CompressorFactory{};
+    return &CompressorFactory;
+}
diff --git a/alc/effects/convolution.cpp b/alc/effects/convolution.cpp
new file mode 100644 (file)
index 0000000..7f36c41
--- /dev/null
@@ -0,0 +1,636 @@
+
+#include "config.h"
+
+#include <algorithm>
+#include <array>
+#include <complex>
+#include <cstddef>
+#include <functional>
+#include <iterator>
+#include <memory>
+#include <stdint.h>
+#include <utility>
+
+#ifdef HAVE_SSE_INTRINSICS
+#include <xmmintrin.h>
+#elif defined(HAVE_NEON)
+#include <arm_neon.h>
+#endif
+
+#include "albyte.h"
+#include "alcomplex.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "base.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/buffer_storage.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/filters/splitter.h"
+#include "core/fmt_traits.h"
+#include "core/mixer.h"
+#include "intrusive_ptr.h"
+#include "polyphase_resampler.h"
+#include "vector.h"
+
+
+namespace {
+
+/* Convolution reverb is implemented using a segmented overlap-add method. The
+ * impulse response is broken up into multiple segments of 128 samples, and
+ * each segment has an FFT applied with a 256-sample buffer (the latter half
+ * left silent) to get its frequency-domain response. The resulting response
+ * has its positive/non-mirrored frequencies saved (129 bins) in each segment.
+ *
+ * Input samples are similarly broken up into 128-sample segments, with an FFT
+ * applied to each new incoming segment to get its 129 bins. A history of FFT'd
+ * input segments is maintained, equal to the length of the impulse response.
+ *
+ * To apply the reverberation, each impulse response segment is convolved with
+ * its paired input segment (using complex multiplies, far cheaper than FIRs),
+ * accumulating into a 256-bin FFT buffer. The input history is then shifted to
+ * align with later impulse response segments for next time.
+ *
+ * An inverse FFT is then applied to the accumulated FFT buffer to get a 256-
+ * sample time-domain response for output, which is split in two halves. The
+ * first half is the 128-sample output, and the second half is a 128-sample
+ * (really, 127) delayed extension, which gets added to the output next time.
+ * Convolving two time-domain responses of lengths N and M results in a time-
+ * domain signal of length N+M-1, and this holds true regardless of the
+ * convolution being applied in the frequency domain, so these "overflow"
+ * samples need to be accounted for.
+ *
+ * To avoid a delay with gathering enough input samples to apply an FFT with,
+ * the first segment is applied directly in the time-domain as the samples come
+ * in. Once enough have been retrieved, the FFT is applied on the input and
+ * it's paired with the remaining (FFT'd) filter segments for processing.
+ */
+
+
+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);
+    /* FIXME: Handle ADPCM decoding here. */
+    case FmtIMA4:
+    case FmtMSADPCM:
+        std::fill_n(dst, samples, 0.0f);
+        break;
+    }
+#undef HANDLE_FMT
+}
+
+
+inline auto& GetAmbiScales(AmbiScaling scaletype) noexcept
+{
+    switch(scaletype)
+    {
+    case AmbiScaling::FuMa: return AmbiScale::FromFuMa();
+    case AmbiScaling::SN3D: return AmbiScale::FromSN3D();
+    case AmbiScaling::UHJ: return AmbiScale::FromUHJ();
+    case AmbiScaling::N3D: break;
+    }
+    return AmbiScale::FromN3D();
+}
+
+inline auto& GetAmbiLayout(AmbiLayout layouttype) noexcept
+{
+    if(layouttype == AmbiLayout::FuMa) return AmbiIndex::FromFuMa();
+    return AmbiIndex::FromACN();
+}
+
+inline auto& GetAmbi2DLayout(AmbiLayout layouttype) noexcept
+{
+    if(layouttype == AmbiLayout::FuMa) return AmbiIndex::FromFuMa2D();
+    return AmbiIndex::FromACN2D();
+}
+
+
+struct ChanMap {
+    Channel channel;
+    float angle;
+    float elevation;
+};
+
+constexpr float Deg2Rad(float x) noexcept
+{ return static_cast<float>(al::numbers::pi / 180.0 * x); }
+
+
+using complex_f = std::complex<float>;
+
+constexpr size_t ConvolveUpdateSize{256};
+constexpr size_t ConvolveUpdateSamples{ConvolveUpdateSize / 2};
+
+
+void apply_fir(al::span<float> dst, const float *RESTRICT src, const float *RESTRICT filter)
+{
+#ifdef HAVE_SSE_INTRINSICS
+    for(float &output : dst)
+    {
+        __m128 r4{_mm_setzero_ps()};
+        for(size_t j{0};j < ConvolveUpdateSamples;j+=4)
+        {
+            const __m128 coeffs{_mm_load_ps(&filter[j])};
+            const __m128 s{_mm_loadu_ps(&src[j])};
+
+            r4 = _mm_add_ps(r4, _mm_mul_ps(s, coeffs));
+        }
+        r4 = _mm_add_ps(r4, _mm_shuffle_ps(r4, r4, _MM_SHUFFLE(0, 1, 2, 3)));
+        r4 = _mm_add_ps(r4, _mm_movehl_ps(r4, r4));
+        output = _mm_cvtss_f32(r4);
+
+        ++src;
+    }
+
+#elif defined(HAVE_NEON)
+
+    for(float &output : dst)
+    {
+        float32x4_t r4{vdupq_n_f32(0.0f)};
+        for(size_t j{0};j < ConvolveUpdateSamples;j+=4)
+            r4 = vmlaq_f32(r4, vld1q_f32(&src[j]), vld1q_f32(&filter[j]));
+        r4 = vaddq_f32(r4, vrev64q_f32(r4));
+        output = vget_lane_f32(vadd_f32(vget_low_f32(r4), vget_high_f32(r4)), 0);
+
+        ++src;
+    }
+
+#else
+
+    for(float &output : dst)
+    {
+        float ret{0.0f};
+        for(size_t j{0};j < ConvolveUpdateSamples;++j)
+            ret += src[j] * filter[j];
+        output = ret;
+        ++src;
+    }
+#endif
+}
+
+struct ConvolutionState final : public EffectState {
+    FmtChannels mChannels{};
+    AmbiLayout mAmbiLayout{};
+    AmbiScaling mAmbiScaling{};
+    uint mAmbiOrder{};
+
+    size_t mFifoPos{0};
+    std::array<float,ConvolveUpdateSamples*2> mInput{};
+    al::vector<std::array<float,ConvolveUpdateSamples>,16> mFilter;
+    al::vector<std::array<float,ConvolveUpdateSamples*2>,16> mOutput;
+
+    alignas(16) std::array<complex_f,ConvolveUpdateSize> mFftBuffer{};
+
+    size_t mCurrentSegment{0};
+    size_t mNumConvolveSegs{0};
+
+    struct ChannelData {
+        alignas(16) FloatBufferLine mBuffer{};
+        float mHfScale{}, mLfScale{};
+        BandSplitter mFilter{};
+        float Current[MAX_OUTPUT_CHANNELS]{};
+        float Target[MAX_OUTPUT_CHANNELS]{};
+    };
+    using ChannelDataArray = al::FlexArray<ChannelData>;
+    std::unique_ptr<ChannelDataArray> mChans;
+    std::unique_ptr<complex_f[]> mComplexData;
+
+
+    ConvolutionState() = default;
+    ~ConvolutionState() override = default;
+
+    void NormalMix(const al::span<FloatBufferLine> samplesOut, const size_t samplesToDo);
+    void UpsampleMix(const al::span<FloatBufferLine> samplesOut, const size_t samplesToDo);
+    void (ConvolutionState::*mMix)(const al::span<FloatBufferLine>,const size_t)
+    {&ConvolutionState::NormalMix};
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(ConvolutionState)
+};
+
+void ConvolutionState::NormalMix(const al::span<FloatBufferLine> samplesOut,
+    const size_t samplesToDo)
+{
+    for(auto &chan : *mChans)
+        MixSamples({chan.mBuffer.data(), samplesToDo}, samplesOut, chan.Current, chan.Target,
+            samplesToDo, 0);
+}
+
+void ConvolutionState::UpsampleMix(const al::span<FloatBufferLine> samplesOut,
+    const size_t samplesToDo)
+{
+    for(auto &chan : *mChans)
+    {
+        const al::span<float> src{chan.mBuffer.data(), samplesToDo};
+        chan.mFilter.processScale(src, chan.mHfScale, chan.mLfScale);
+        MixSamples(src, samplesOut, chan.Current, chan.Target, samplesToDo, 0);
+    }
+}
+
+
+void ConvolutionState::deviceUpdate(const DeviceBase *device, const BufferStorage *buffer)
+{
+    using UhjDecoderType = UhjDecoder<512>;
+    static constexpr auto DecoderPadding = UhjDecoderType::sInputPadding;
+
+    constexpr uint MaxConvolveAmbiOrder{1u};
+
+    mFifoPos = 0;
+    mInput.fill(0.0f);
+    decltype(mFilter){}.swap(mFilter);
+    decltype(mOutput){}.swap(mOutput);
+    mFftBuffer.fill(complex_f{});
+
+    mCurrentSegment = 0;
+    mNumConvolveSegs = 0;
+
+    mChans = nullptr;
+    mComplexData = nullptr;
+
+    /* An empty buffer doesn't need a convolution filter. */
+    if(!buffer || buffer->mSampleLen < 1) return;
+
+    mChannels = buffer->mChannels;
+    mAmbiLayout = IsUHJ(mChannels) ? AmbiLayout::FuMa : buffer->mAmbiLayout;
+    mAmbiScaling = IsUHJ(mChannels) ? AmbiScaling::UHJ : buffer->mAmbiScaling;
+    mAmbiOrder = minu(buffer->mAmbiOrder, MaxConvolveAmbiOrder);
+
+    constexpr size_t m{ConvolveUpdateSize/2 + 1};
+    const auto bytesPerSample = BytesFromFmt(buffer->mType);
+    const auto realChannels = buffer->channelsFromFmt();
+    const auto numChannels = (mChannels == FmtUHJ2) ? 3u : ChannelsFromFmt(mChannels, mAmbiOrder);
+
+    mChans = ChannelDataArray::Create(numChannels);
+
+    /* The impulse response needs to have the same sample rate as the input and
+     * output. The bsinc24 resampler is decent, but there is high-frequency
+     * attenuation that some people may be able to pick up on. Since this is
+     * called very infrequently, go ahead and use the polyphase resampler.
+     */
+    PPhaseResampler resampler;
+    if(device->Frequency != buffer->mSampleRate)
+        resampler.init(buffer->mSampleRate, device->Frequency);
+    const auto resampledCount = static_cast<uint>(
+        (uint64_t{buffer->mSampleLen}*device->Frequency+(buffer->mSampleRate-1)) /
+        buffer->mSampleRate);
+
+    const BandSplitter splitter{device->mXOverFreq / static_cast<float>(device->Frequency)};
+    for(auto &e : *mChans)
+        e.mFilter = splitter;
+
+    mFilter.resize(numChannels, {});
+    mOutput.resize(numChannels, {});
+
+    /* Calculate the number of segments needed to hold the impulse response and
+     * the input history (rounded up), and allocate them. Exclude one segment
+     * which gets applied as a time-domain FIR filter. Make sure at least one
+     * segment is allocated to simplify handling.
+     */
+    mNumConvolveSegs = (resampledCount+(ConvolveUpdateSamples-1)) / ConvolveUpdateSamples;
+    mNumConvolveSegs = maxz(mNumConvolveSegs, 2) - 1;
+
+    const size_t complex_length{mNumConvolveSegs * m * (numChannels+1)};
+    mComplexData = std::make_unique<complex_f[]>(complex_length);
+    std::fill_n(mComplexData.get(), complex_length, complex_f{});
+
+    /* Load the samples from the buffer. */
+    const size_t srclinelength{RoundUp(buffer->mSampleLen+DecoderPadding, 16)};
+    auto srcsamples = std::make_unique<float[]>(srclinelength * numChannels);
+    std::fill_n(srcsamples.get(), srclinelength * numChannels, 0.0f);
+    for(size_t c{0};c < numChannels && c < realChannels;++c)
+        LoadSamples(srcsamples.get() + srclinelength*c, buffer->mData.data() + bytesPerSample*c,
+            realChannels, buffer->mType, buffer->mSampleLen);
+
+    if(IsUHJ(mChannels))
+    {
+        auto decoder = std::make_unique<UhjDecoderType>();
+        std::array<float*,4> samples{};
+        for(size_t c{0};c < numChannels;++c)
+            samples[c] = srcsamples.get() + srclinelength*c;
+        decoder->decode({samples.data(), numChannels}, buffer->mSampleLen, buffer->mSampleLen);
+    }
+
+    auto ressamples = std::make_unique<double[]>(buffer->mSampleLen +
+        (resampler ? resampledCount : 0));
+    complex_f *filteriter = mComplexData.get() + mNumConvolveSegs*m;
+    for(size_t c{0};c < numChannels;++c)
+    {
+        /* Resample to match the device. */
+        if(resampler)
+        {
+            std::copy_n(srcsamples.get() + srclinelength*c, buffer->mSampleLen,
+                ressamples.get() + resampledCount);
+            resampler.process(buffer->mSampleLen, ressamples.get()+resampledCount,
+                resampledCount, ressamples.get());
+        }
+        else
+            std::copy_n(srcsamples.get() + srclinelength*c, buffer->mSampleLen, ressamples.get());
+
+        /* Store the first segment's samples in reverse in the time-domain, to
+         * apply as a FIR filter.
+         */
+        const size_t first_size{minz(resampledCount, ConvolveUpdateSamples)};
+        std::transform(ressamples.get(), ressamples.get()+first_size, mFilter[c].rbegin(),
+            [](const double d) noexcept -> float { return static_cast<float>(d); });
+
+        auto fftbuffer = std::vector<std::complex<double>>(ConvolveUpdateSize);
+        size_t done{first_size};
+        for(size_t s{0};s < mNumConvolveSegs;++s)
+        {
+            const size_t todo{minz(resampledCount-done, ConvolveUpdateSamples)};
+
+            auto iter = std::copy_n(&ressamples[done], todo, fftbuffer.begin());
+            done += todo;
+            std::fill(iter, fftbuffer.end(), std::complex<double>{});
+
+            forward_fft(al::as_span(fftbuffer));
+            filteriter = std::copy_n(fftbuffer.cbegin(), m, filteriter);
+        }
+    }
+}
+
+
+void ConvolutionState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps* /*props*/, const EffectTarget target)
+{
+    /* NOTE: Stereo and Rear are slightly different from normal mixing (as
+     * defined in alu.cpp). These are 45 degrees from center, rather than the
+     * 30 degrees used there.
+     *
+     * TODO: LFE is not mixed to output. This will require each buffer channel
+     * to have its own output target since the main mixing buffer won't have an
+     * LFE channel (due to being B-Format).
+     */
+    static constexpr ChanMap MonoMap[1]{
+        { FrontCenter, 0.0f, 0.0f }
+    }, StereoMap[2]{
+        { FrontLeft,  Deg2Rad(-45.0f), Deg2Rad(0.0f) },
+        { FrontRight, Deg2Rad( 45.0f), Deg2Rad(0.0f) }
+    }, RearMap[2]{
+        { BackLeft,  Deg2Rad(-135.0f), Deg2Rad(0.0f) },
+        { BackRight, Deg2Rad( 135.0f), Deg2Rad(0.0f) }
+    }, QuadMap[4]{
+        { FrontLeft,  Deg2Rad( -45.0f), Deg2Rad(0.0f) },
+        { FrontRight, Deg2Rad(  45.0f), Deg2Rad(0.0f) },
+        { BackLeft,   Deg2Rad(-135.0f), Deg2Rad(0.0f) },
+        { BackRight,  Deg2Rad( 135.0f), Deg2Rad(0.0f) }
+    }, X51Map[6]{
+        { FrontLeft,   Deg2Rad( -30.0f), Deg2Rad(0.0f) },
+        { FrontRight,  Deg2Rad(  30.0f), Deg2Rad(0.0f) },
+        { FrontCenter, Deg2Rad(   0.0f), Deg2Rad(0.0f) },
+        { LFE, 0.0f, 0.0f },
+        { SideLeft,    Deg2Rad(-110.0f), Deg2Rad(0.0f) },
+        { SideRight,   Deg2Rad( 110.0f), Deg2Rad(0.0f) }
+    }, X61Map[7]{
+        { FrontLeft,   Deg2Rad(-30.0f), Deg2Rad(0.0f) },
+        { FrontRight,  Deg2Rad( 30.0f), Deg2Rad(0.0f) },
+        { FrontCenter, Deg2Rad(  0.0f), Deg2Rad(0.0f) },
+        { LFE, 0.0f, 0.0f },
+        { BackCenter,  Deg2Rad(180.0f), Deg2Rad(0.0f) },
+        { SideLeft,    Deg2Rad(-90.0f), Deg2Rad(0.0f) },
+        { SideRight,   Deg2Rad( 90.0f), Deg2Rad(0.0f) }
+    }, X71Map[8]{
+        { FrontLeft,   Deg2Rad( -30.0f), Deg2Rad(0.0f) },
+        { FrontRight,  Deg2Rad(  30.0f), Deg2Rad(0.0f) },
+        { FrontCenter, Deg2Rad(   0.0f), Deg2Rad(0.0f) },
+        { LFE, 0.0f, 0.0f },
+        { BackLeft,    Deg2Rad(-150.0f), Deg2Rad(0.0f) },
+        { BackRight,   Deg2Rad( 150.0f), Deg2Rad(0.0f) },
+        { SideLeft,    Deg2Rad( -90.0f), Deg2Rad(0.0f) },
+        { SideRight,   Deg2Rad(  90.0f), Deg2Rad(0.0f) }
+    };
+
+    if(mNumConvolveSegs < 1) UNLIKELY
+        return;
+
+    mMix = &ConvolutionState::NormalMix;
+
+    for(auto &chan : *mChans)
+        std::fill(std::begin(chan.Target), std::end(chan.Target), 0.0f);
+    const float gain{slot->Gain};
+    if(IsAmbisonic(mChannels))
+    {
+        DeviceBase *device{context->mDevice};
+        if(mChannels == FmtUHJ2 && !device->mUhjEncoder)
+        {
+            mMix = &ConvolutionState::UpsampleMix;
+            (*mChans)[0].mHfScale = 1.0f;
+            (*mChans)[0].mLfScale = DecoderBase::sWLFScale;
+            (*mChans)[1].mHfScale = 1.0f;
+            (*mChans)[1].mLfScale = DecoderBase::sXYLFScale;
+            (*mChans)[2].mHfScale = 1.0f;
+            (*mChans)[2].mLfScale = DecoderBase::sXYLFScale;
+        }
+        else if(device->mAmbiOrder > mAmbiOrder)
+        {
+            mMix = &ConvolutionState::UpsampleMix;
+            const auto scales = AmbiScale::GetHFOrderScales(mAmbiOrder, device->mAmbiOrder,
+                device->m2DMixing);
+            (*mChans)[0].mHfScale = scales[0];
+            (*mChans)[0].mLfScale = 1.0f;
+            for(size_t i{1};i < mChans->size();++i)
+            {
+                (*mChans)[i].mHfScale = scales[1];
+                (*mChans)[i].mLfScale = 1.0f;
+            }
+        }
+        mOutTarget = target.Main->Buffer;
+
+        auto&& scales = GetAmbiScales(mAmbiScaling);
+        const uint8_t *index_map{Is2DAmbisonic(mChannels) ?
+            GetAmbi2DLayout(mAmbiLayout).data() :
+            GetAmbiLayout(mAmbiLayout).data()};
+
+        std::array<float,MaxAmbiChannels> coeffs{};
+        for(size_t c{0u};c < mChans->size();++c)
+        {
+            const size_t acn{index_map[c]};
+            coeffs[acn] = scales[acn];
+            ComputePanGains(target.Main, coeffs.data(), gain, (*mChans)[c].Target);
+            coeffs[acn] = 0.0f;
+        }
+    }
+    else
+    {
+        DeviceBase *device{context->mDevice};
+        al::span<const ChanMap> chanmap{};
+        switch(mChannels)
+        {
+        case FmtMono: chanmap = MonoMap; break;
+        case FmtSuperStereo:
+        case FmtStereo: chanmap = StereoMap; break;
+        case FmtRear: chanmap = RearMap; break;
+        case FmtQuad: chanmap = QuadMap; break;
+        case FmtX51: chanmap = X51Map; break;
+        case FmtX61: chanmap = X61Map; break;
+        case FmtX71: chanmap = X71Map; break;
+        case FmtBFormat2D:
+        case FmtBFormat3D:
+        case FmtUHJ2:
+        case FmtUHJ3:
+        case FmtUHJ4:
+            break;
+        }
+
+        mOutTarget = target.Main->Buffer;
+        if(device->mRenderMode == RenderMode::Pairwise)
+        {
+            auto ScaleAzimuthFront = [](float azimuth, float scale) -> float
+            {
+                constexpr float half_pi{al::numbers::pi_v<float>*0.5f};
+                const float abs_azi{std::fabs(azimuth)};
+                if(!(abs_azi >= half_pi))
+                    return std::copysign(minf(abs_azi*scale, half_pi), azimuth);
+                return azimuth;
+            };
+
+            for(size_t i{0};i < chanmap.size();++i)
+            {
+                if(chanmap[i].channel == LFE) continue;
+                const auto coeffs = CalcAngleCoeffs(ScaleAzimuthFront(chanmap[i].angle, 2.0f),
+                    chanmap[i].elevation, 0.0f);
+                ComputePanGains(target.Main, coeffs.data(), gain, (*mChans)[i].Target);
+            }
+        }
+        else for(size_t i{0};i < chanmap.size();++i)
+        {
+            if(chanmap[i].channel == LFE) continue;
+            const auto coeffs = CalcAngleCoeffs(chanmap[i].angle, chanmap[i].elevation, 0.0f);
+            ComputePanGains(target.Main, coeffs.data(), gain, (*mChans)[i].Target);
+        }
+    }
+}
+
+void ConvolutionState::process(const size_t samplesToDo,
+    const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    if(mNumConvolveSegs < 1) UNLIKELY
+        return;
+
+    constexpr size_t m{ConvolveUpdateSize/2 + 1};
+    size_t curseg{mCurrentSegment};
+    auto &chans = *mChans;
+
+    for(size_t base{0u};base < samplesToDo;)
+    {
+        const size_t todo{minz(ConvolveUpdateSamples-mFifoPos, samplesToDo-base)};
+
+        std::copy_n(samplesIn[0].begin() + base, todo,
+            mInput.begin()+ConvolveUpdateSamples+mFifoPos);
+
+        /* Apply the FIR for the newly retrieved input samples, and combine it
+         * with the inverse FFT'd output samples.
+         */
+        for(size_t c{0};c < chans.size();++c)
+        {
+            auto buf_iter = chans[c].mBuffer.begin() + base;
+            apply_fir({buf_iter, todo}, mInput.data()+1 + mFifoPos, mFilter[c].data());
+
+            auto fifo_iter = mOutput[c].begin() + mFifoPos;
+            std::transform(fifo_iter, fifo_iter+todo, buf_iter, buf_iter, std::plus<>{});
+        }
+
+        mFifoPos += todo;
+        base += todo;
+
+        /* Check whether the input buffer is filled with new samples. */
+        if(mFifoPos < ConvolveUpdateSamples) break;
+        mFifoPos = 0;
+
+        /* Move the newest input to the front for the next iteration's history. */
+        std::copy(mInput.cbegin()+ConvolveUpdateSamples, mInput.cend(), mInput.begin());
+
+        /* Calculate the frequency domain response and add the relevant
+         * frequency bins to the FFT history.
+         */
+        auto fftiter = std::copy_n(mInput.cbegin(), ConvolveUpdateSamples, mFftBuffer.begin());
+        std::fill(fftiter, mFftBuffer.end(), complex_f{});
+        forward_fft(al::as_span(mFftBuffer));
+
+        std::copy_n(mFftBuffer.cbegin(), m, &mComplexData[curseg*m]);
+
+        const complex_f *RESTRICT filter{mComplexData.get() + mNumConvolveSegs*m};
+        for(size_t c{0};c < chans.size();++c)
+        {
+            std::fill_n(mFftBuffer.begin(), m, complex_f{});
+
+            /* Convolve each input segment with its IR filter counterpart
+             * (aligned in time).
+             */
+            const complex_f *RESTRICT input{&mComplexData[curseg*m]};
+            for(size_t s{curseg};s < mNumConvolveSegs;++s)
+            {
+                for(size_t i{0};i < m;++i,++input,++filter)
+                    mFftBuffer[i] += *input * *filter;
+            }
+            input = mComplexData.get();
+            for(size_t s{0};s < curseg;++s)
+            {
+                for(size_t i{0};i < m;++i,++input,++filter)
+                    mFftBuffer[i] += *input * *filter;
+            }
+
+            /* Reconstruct the mirrored/negative frequencies to do a proper
+             * inverse FFT.
+             */
+            for(size_t i{m};i < ConvolveUpdateSize;++i)
+                mFftBuffer[i] = std::conj(mFftBuffer[ConvolveUpdateSize-i]);
+
+            /* Apply iFFT to get the 256 (really 255) samples for output. The
+             * 128 output samples are combined with the last output's 127
+             * second-half samples (and this output's second half is
+             * subsequently saved for next time).
+             */
+            inverse_fft(al::as_span(mFftBuffer));
+
+            /* The iFFT'd response is scaled up by the number of bins, so apply
+             * the inverse to normalize the output.
+             */
+            for(size_t i{0};i < ConvolveUpdateSamples;++i)
+                mOutput[c][i] =
+                    (mFftBuffer[i].real()+mOutput[c][ConvolveUpdateSamples+i]) *
+                    (1.0f/float{ConvolveUpdateSize});
+            for(size_t i{0};i < ConvolveUpdateSamples;++i)
+                mOutput[c][ConvolveUpdateSamples+i] = mFftBuffer[ConvolveUpdateSamples+i].real();
+        }
+
+        /* Shift the input history. */
+        curseg = curseg ? (curseg-1) : (mNumConvolveSegs-1);
+    }
+    mCurrentSegment = curseg;
+
+    /* Finally, mix to the output. */
+    (this->*mMix)(samplesOut, samplesToDo);
+}
+
+
+struct ConvolutionStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new ConvolutionState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *ConvolutionStateFactory_getFactory()
+{
+    static ConvolutionStateFactory ConvolutionFactory{};
+    return &ConvolutionFactory;
+}
diff --git a/alc/effects/dedicated.cpp b/alc/effects/dedicated.cpp
new file mode 100644 (file)
index 0000000..047e676
--- /dev/null
@@ -0,0 +1,123 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2011 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 <algorithm>
+#include <array>
+#include <cstdlib>
+#include <iterator>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alspan.h"
+#include "core/bufferline.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/mixer.h"
+#include "intrusive_ptr.h"
+
+struct ContextBase;
+
+
+namespace {
+
+using uint = unsigned int;
+
+struct DedicatedState final : public EffectState {
+    /* The "dedicated" effect can output to the real output, so should have
+     * gains for all possible output channels and not just the main ambisonic
+     * buffer.
+     */
+    float mCurrentGains[MAX_OUTPUT_CHANNELS];
+    float mTargetGains[MAX_OUTPUT_CHANNELS];
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(DedicatedState)
+};
+
+void DedicatedState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    std::fill(std::begin(mCurrentGains), std::end(mCurrentGains), 0.0f);
+}
+
+void DedicatedState::update(const ContextBase*, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    std::fill(std::begin(mTargetGains), std::end(mTargetGains), 0.0f);
+
+    const float Gain{slot->Gain * props->Dedicated.Gain};
+
+    if(slot->EffectType == EffectSlotType::DedicatedLFE)
+    {
+        const uint idx{target.RealOut ? target.RealOut->ChannelIndex[LFE] : InvalidChannelIndex};
+        if(idx != InvalidChannelIndex)
+        {
+            mOutTarget = target.RealOut->Buffer;
+            mTargetGains[idx] = Gain;
+        }
+    }
+    else if(slot->EffectType == EffectSlotType::DedicatedDialog)
+    {
+        /* Dialog goes to the front-center speaker if it exists, otherwise it
+         * plays from the front-center location. */
+        const uint idx{target.RealOut ? target.RealOut->ChannelIndex[FrontCenter]
+            : InvalidChannelIndex};
+        if(idx != InvalidChannelIndex)
+        {
+            mOutTarget = target.RealOut->Buffer;
+            mTargetGains[idx] = Gain;
+        }
+        else
+        {
+            static constexpr auto coeffs = CalcDirectionCoeffs({0.0f, 0.0f, -1.0f});
+
+            mOutTarget = target.Main->Buffer;
+            ComputePanGains(target.Main, coeffs.data(), Gain, mTargetGains);
+        }
+    }
+}
+
+void DedicatedState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    MixSamples({samplesIn[0].data(), samplesToDo}, samplesOut, mCurrentGains, mTargetGains,
+        samplesToDo, 0);
+}
+
+
+struct DedicatedStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new DedicatedState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *DedicatedStateFactory_getFactory()
+{
+    static DedicatedStateFactory DedicatedFactory{};
+    return &DedicatedFactory;
+}
diff --git a/alc/effects/distortion.cpp b/alc/effects/distortion.cpp
new file mode 100644 (file)
index 0000000..b4e2167
--- /dev/null
@@ -0,0 +1,178 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2013 by Mike Gorchak
+ * 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 <algorithm>
+#include <array>
+#include <cstdlib>
+#include <iterator>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/filters/biquad.h"
+#include "core/mixer.h"
+#include "core/mixer/defs.h"
+#include "intrusive_ptr.h"
+
+
+namespace {
+
+struct DistortionState final : public EffectState {
+    /* Effect gains for each channel */
+    float mGain[MaxAmbiChannels]{};
+
+    /* Effect parameters */
+    BiquadFilter mLowpass;
+    BiquadFilter mBandpass;
+    float mAttenuation{};
+    float mEdgeCoeff{};
+
+    alignas(16) float mBuffer[2][BufferLineSize]{};
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(DistortionState)
+};
+
+void DistortionState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    mLowpass.clear();
+    mBandpass.clear();
+}
+
+void DistortionState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *device{context->mDevice};
+
+    /* Store waveshaper edge settings. */
+    const float edge{minf(std::sin(al::numbers::pi_v<float>*0.5f * props->Distortion.Edge),
+        0.99f)};
+    mEdgeCoeff = 2.0f * edge / (1.0f-edge);
+
+    float cutoff{props->Distortion.LowpassCutoff};
+    /* Bandwidth value is constant in octaves. */
+    float bandwidth{(cutoff / 2.0f) / (cutoff * 0.67f)};
+    /* Divide normalized frequency by the amount of oversampling done during
+     * processing.
+     */
+    auto frequency = static_cast<float>(device->Frequency);
+    mLowpass.setParamsFromBandwidth(BiquadType::LowPass, cutoff/frequency/4.0f, 1.0f, bandwidth);
+
+    cutoff = props->Distortion.EQCenter;
+    /* Convert bandwidth in Hz to octaves. */
+    bandwidth = props->Distortion.EQBandwidth / (cutoff * 0.67f);
+    mBandpass.setParamsFromBandwidth(BiquadType::BandPass, cutoff/frequency/4.0f, 1.0f, bandwidth);
+
+    static constexpr auto coeffs = CalcDirectionCoeffs({0.0f, 0.0f, -1.0f});
+
+    mOutTarget = target.Main->Buffer;
+    ComputePanGains(target.Main, coeffs.data(), slot->Gain*props->Distortion.Gain, mGain);
+}
+
+void DistortionState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    const float fc{mEdgeCoeff};
+    for(size_t base{0u};base < samplesToDo;)
+    {
+        /* Perform 4x oversampling to avoid aliasing. Oversampling greatly
+         * improves distortion quality and allows to implement lowpass and
+         * bandpass filters using high frequencies, at which classic IIR
+         * filters became unstable.
+         */
+        size_t todo{minz(BufferLineSize, (samplesToDo-base) * 4)};
+
+        /* Fill oversample buffer using zero stuffing. Multiply the sample by
+         * the amount of oversampling to maintain the signal's power.
+         */
+        for(size_t i{0u};i < todo;i++)
+            mBuffer[0][i] = !(i&3) ? samplesIn[0][(i>>2)+base] * 4.0f : 0.0f;
+
+        /* First step, do lowpass filtering of original signal. Additionally
+         * perform buffer interpolation and lowpass cutoff for oversampling
+         * (which is fortunately first step of distortion). So combine three
+         * operations into the one.
+         */
+        mLowpass.process({mBuffer[0], todo}, mBuffer[1]);
+
+        /* Second step, do distortion using waveshaper function to emulate
+         * signal processing during tube overdriving. Three steps of
+         * waveshaping are intended to modify waveform without boost/clipping/
+         * attenuation process.
+         */
+        auto proc_sample = [fc](float smp) -> float
+        {
+            smp = (1.0f + fc) * smp/(1.0f + fc*std::abs(smp));
+            smp = (1.0f + fc) * smp/(1.0f + fc*std::abs(smp)) * -1.0f;
+            smp = (1.0f + fc) * smp/(1.0f + fc*std::abs(smp));
+            return smp;
+        };
+        std::transform(std::begin(mBuffer[1]), std::begin(mBuffer[1])+todo, std::begin(mBuffer[0]),
+            proc_sample);
+
+        /* Third step, do bandpass filtering of distorted signal. */
+        mBandpass.process({mBuffer[0], todo}, mBuffer[1]);
+
+        todo >>= 2;
+        const float *outgains{mGain};
+        for(FloatBufferLine &output : samplesOut)
+        {
+            /* Fourth step, final, do attenuation and perform decimation,
+             * storing only one sample out of four.
+             */
+            const float gain{*(outgains++)};
+            if(!(std::fabs(gain) > GainSilenceThreshold))
+                continue;
+
+            for(size_t i{0u};i < todo;i++)
+                output[base+i] += gain * mBuffer[1][i*4];
+        }
+
+        base += todo;
+    }
+}
+
+
+struct DistortionStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new DistortionState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *DistortionStateFactory_getFactory()
+{
+    static DistortionStateFactory DistortionFactory{};
+    return &DistortionFactory;
+}
diff --git a/alc/effects/echo.cpp b/alc/effects/echo.cpp
new file mode 100644 (file)
index 0000000..a69529d
--- /dev/null
@@ -0,0 +1,180 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2009 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 <algorithm>
+#include <array>
+#include <cstdlib>
+#include <iterator>
+#include <tuple>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/filters/biquad.h"
+#include "core/mixer.h"
+#include "intrusive_ptr.h"
+#include "opthelpers.h"
+#include "vector.h"
+
+
+namespace {
+
+using uint = unsigned int;
+
+constexpr float LowpassFreqRef{5000.0f};
+
+struct EchoState final : public EffectState {
+    al::vector<float,16> mSampleBuffer;
+
+    // The echo is two tap. The delay is the number of samples from before the
+    // current offset
+    struct {
+        size_t delay{0u};
+    } mTap[2];
+    size_t mOffset{0u};
+
+    /* The panning gains for the two taps */
+    struct {
+        float Current[MaxAmbiChannels]{};
+        float Target[MaxAmbiChannels]{};
+    } mGains[2];
+
+    BiquadFilter mFilter;
+    float mFeedGain{0.0f};
+
+    alignas(16) float mTempBuffer[2][BufferLineSize];
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(EchoState)
+};
+
+void EchoState::deviceUpdate(const DeviceBase *Device, const BufferStorage*)
+{
+    const auto frequency = static_cast<float>(Device->Frequency);
+
+    // Use the next power of 2 for the buffer length, so the tap offsets can be
+    // wrapped using a mask instead of a modulo
+    const uint maxlen{NextPowerOf2(float2uint(EchoMaxDelay*frequency + 0.5f) +
+        float2uint(EchoMaxLRDelay*frequency + 0.5f))};
+    if(maxlen != mSampleBuffer.size())
+        al::vector<float,16>(maxlen).swap(mSampleBuffer);
+
+    std::fill(mSampleBuffer.begin(), mSampleBuffer.end(), 0.0f);
+    for(auto &e : mGains)
+    {
+        std::fill(std::begin(e.Current), std::end(e.Current), 0.0f);
+        std::fill(std::begin(e.Target), std::end(e.Target), 0.0f);
+    }
+}
+
+void EchoState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *device{context->mDevice};
+    const auto frequency = static_cast<float>(device->Frequency);
+
+    mTap[0].delay = maxu(float2uint(props->Echo.Delay*frequency + 0.5f), 1);
+    mTap[1].delay = float2uint(props->Echo.LRDelay*frequency + 0.5f) + mTap[0].delay;
+
+    const float gainhf{maxf(1.0f - props->Echo.Damping, 0.0625f)}; /* Limit -24dB */
+    mFilter.setParamsFromSlope(BiquadType::HighShelf, LowpassFreqRef/frequency, gainhf, 1.0f);
+
+    mFeedGain = props->Echo.Feedback;
+
+    /* Convert echo spread (where 0 = center, +/-1 = sides) to angle. */
+    const float angle{std::asin(props->Echo.Spread)};
+
+    const auto coeffs0 = CalcAngleCoeffs(-angle, 0.0f, 0.0f);
+    const auto coeffs1 = CalcAngleCoeffs( angle, 0.0f, 0.0f);
+
+    mOutTarget = target.Main->Buffer;
+    ComputePanGains(target.Main, coeffs0.data(), slot->Gain, mGains[0].Target);
+    ComputePanGains(target.Main, coeffs1.data(), slot->Gain, mGains[1].Target);
+}
+
+void EchoState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    const size_t mask{mSampleBuffer.size()-1};
+    float *RESTRICT delaybuf{mSampleBuffer.data()};
+    size_t offset{mOffset};
+    size_t tap1{offset - mTap[0].delay};
+    size_t tap2{offset - mTap[1].delay};
+    float z1, z2;
+
+    ASSUME(samplesToDo > 0);
+
+    const BiquadFilter filter{mFilter};
+    std::tie(z1, z2) = mFilter.getComponents();
+    for(size_t i{0u};i < samplesToDo;)
+    {
+        offset &= mask;
+        tap1 &= mask;
+        tap2 &= mask;
+
+        size_t td{minz(mask+1 - maxz(offset, maxz(tap1, tap2)), samplesToDo-i)};
+        do {
+            /* Feed the delay buffer's input first. */
+            delaybuf[offset] = samplesIn[0][i];
+
+            /* Get delayed output from the first and second taps. Use the
+             * second tap for feedback.
+             */
+            mTempBuffer[0][i] = delaybuf[tap1++];
+            mTempBuffer[1][i] = delaybuf[tap2++];
+            const float feedb{mTempBuffer[1][i++]};
+
+            /* Add feedback to the delay buffer with damping and attenuation. */
+            delaybuf[offset++] += filter.processOne(feedb, z1, z2) * mFeedGain;
+        } while(--td);
+    }
+    mFilter.setComponents(z1, z2);
+    mOffset = offset;
+
+    for(size_t c{0};c < 2;c++)
+        MixSamples({mTempBuffer[c], samplesToDo}, samplesOut, mGains[c].Current, mGains[c].Target,
+            samplesToDo, 0);
+}
+
+
+struct EchoStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new EchoState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *EchoStateFactory_getFactory()
+{
+    static EchoStateFactory EchoFactory{};
+    return &EchoFactory;
+}
diff --git a/alc/effects/equalizer.cpp b/alc/effects/equalizer.cpp
new file mode 100644 (file)
index 0000000..50bec4a
--- /dev/null
@@ -0,0 +1,204 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2013 by Mike Gorchak
+ * 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 <algorithm>
+#include <array>
+#include <cstdlib>
+#include <functional>
+#include <iterator>
+#include <utility>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alspan.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/filters/biquad.h"
+#include "core/mixer.h"
+#include "intrusive_ptr.h"
+
+
+namespace {
+
+/*  The document  "Effects Extension Guide.pdf"  says that low and high  *
+ *  frequencies are cutoff frequencies. This is not fully correct, they  *
+ *  are corner frequencies for low and high shelf filters. If they were  *
+ *  just cutoff frequencies, there would be no need in cutoff frequency  *
+ *  gains, which are present.  Documentation for  "Creative Proteus X2"  *
+ *  software describes  4-band equalizer functionality in a much better  *
+ *  way.  This equalizer seems  to be a predecessor  of  OpenAL  4-band  *
+ *  equalizer.  With low and high  shelf filters  we are able to cutoff  *
+ *  frequencies below and/or above corner frequencies using attenuation  *
+ *  gains (below 1.0) and amplify all low and/or high frequencies using  *
+ *  gains above 1.0.                                                     *
+ *                                                                       *
+ *     Low-shelf       Low Mid Band      High Mid Band     High-shelf    *
+ *      corner            center             center          corner      *
+ *     frequency        frequency          frequency       frequency     *
+ *    50Hz..800Hz     200Hz..3000Hz      1000Hz..8000Hz  4000Hz..16000Hz *
+ *                                                                       *
+ *          |               |                  |               |         *
+ *          |               |                  |               |         *
+ *   B -----+            /--+--\            /--+--\            +-----    *
+ *   O      |\          |   |   |          |   |   |          /|         *
+ *   O      | \        -    |    -        -    |    -        / |         *
+ *   S +    |  \      |     |     |      |     |     |      /  |         *
+ *   T      |   |    |      |      |    |      |      |    |   |         *
+ * ---------+---------------+------------------+---------------+-------- *
+ *   C      |   |    |      |      |    |      |      |    |   |         *
+ *   U -    |  /      |     |     |      |     |     |      \  |         *
+ *   T      | /        -    |    -        -    |    -        \ |         *
+ *   O      |/          |   |   |          |   |   |          \|         *
+ *   F -----+            \--+--/            \--+--/            +-----    *
+ *   F      |               |                  |               |         *
+ *          |               |                  |               |         *
+ *                                                                       *
+ * Gains vary from 0.126 up to 7.943, which means from -18dB attenuation *
+ * up to +18dB amplification. Band width varies from 0.01 up to 1.0 in   *
+ * octaves for two mid bands.                                            *
+ *                                                                       *
+ * Implementation is based on the "Cookbook formulae for audio EQ biquad *
+ * filter coefficients" by Robert Bristow-Johnson                        *
+ * http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt                   */
+
+
+struct EqualizerState final : public EffectState {
+    struct {
+        uint mTargetChannel{InvalidChannelIndex};
+
+        /* Effect parameters */
+        BiquadFilter mFilter[4];
+
+        /* Effect gains for each channel */
+        float mCurrentGain{};
+        float mTargetGain{};
+    } mChans[MaxAmbiChannels];
+
+    alignas(16) FloatBufferLine mSampleBuffer{};
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(EqualizerState)
+};
+
+void EqualizerState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    for(auto &e : mChans)
+    {
+        e.mTargetChannel = InvalidChannelIndex;
+        std::for_each(std::begin(e.mFilter), std::end(e.mFilter),
+            std::mem_fn(&BiquadFilter::clear));
+        e.mCurrentGain = 0.0f;
+    }
+}
+
+void EqualizerState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *device{context->mDevice};
+    auto frequency = static_cast<float>(device->Frequency);
+    float gain, f0norm;
+
+    /* Calculate coefficients for the each type of filter. Note that the shelf
+     * and peaking filters' gain is for the centerpoint of the transition band,
+     * while the effect property gains are for the shelf/peak itself. So the
+     * property gains need their dB halved (sqrt of linear gain) for the
+     * shelf/peak to reach the provided gain.
+     */
+    gain = std::sqrt(props->Equalizer.LowGain);
+    f0norm = props->Equalizer.LowCutoff / frequency;
+    mChans[0].mFilter[0].setParamsFromSlope(BiquadType::LowShelf, f0norm, gain, 0.75f);
+
+    gain = std::sqrt(props->Equalizer.Mid1Gain);
+    f0norm = props->Equalizer.Mid1Center / frequency;
+    mChans[0].mFilter[1].setParamsFromBandwidth(BiquadType::Peaking, f0norm, gain,
+        props->Equalizer.Mid1Width);
+
+    gain = std::sqrt(props->Equalizer.Mid2Gain);
+    f0norm = props->Equalizer.Mid2Center / frequency;
+    mChans[0].mFilter[2].setParamsFromBandwidth(BiquadType::Peaking, f0norm, gain,
+        props->Equalizer.Mid2Width);
+
+    gain = std::sqrt(props->Equalizer.HighGain);
+    f0norm = props->Equalizer.HighCutoff / frequency;
+    mChans[0].mFilter[3].setParamsFromSlope(BiquadType::HighShelf, f0norm, gain, 0.75f);
+
+    /* Copy the filter coefficients for the other input channels. */
+    for(size_t i{1u};i < slot->Wet.Buffer.size();++i)
+    {
+        mChans[i].mFilter[0].copyParamsFrom(mChans[0].mFilter[0]);
+        mChans[i].mFilter[1].copyParamsFrom(mChans[0].mFilter[1]);
+        mChans[i].mFilter[2].copyParamsFrom(mChans[0].mFilter[2]);
+        mChans[i].mFilter[3].copyParamsFrom(mChans[0].mFilter[3]);
+    }
+
+    mOutTarget = target.Main->Buffer;
+    auto set_channel = [this](size_t idx, uint outchan, float outgain)
+    {
+        mChans[idx].mTargetChannel = outchan;
+        mChans[idx].mTargetGain = outgain;
+    };
+    target.Main->setAmbiMixParams(slot->Wet, slot->Gain, set_channel);
+}
+
+void EqualizerState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    const al::span<float> buffer{mSampleBuffer.data(), samplesToDo};
+    auto chan = std::begin(mChans);
+    for(const auto &input : samplesIn)
+    {
+        const size_t outidx{chan->mTargetChannel};
+        if(outidx != InvalidChannelIndex)
+        {
+            const al::span<const float> inbuf{input.data(), samplesToDo};
+            DualBiquad{chan->mFilter[0], chan->mFilter[1]}.process(inbuf, buffer.begin());
+            DualBiquad{chan->mFilter[2], chan->mFilter[3]}.process(buffer, buffer.begin());
+
+            MixSamples(buffer, samplesOut[outidx].data(), chan->mCurrentGain, chan->mTargetGain,
+                samplesToDo);
+        }
+        ++chan;
+    }
+}
+
+
+struct EqualizerStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new EqualizerState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *EqualizerStateFactory_getFactory()
+{
+    static EqualizerStateFactory EqualizerFactory{};
+    return &EqualizerFactory;
+}
diff --git a/alc/effects/fshifter.cpp b/alc/effects/fshifter.cpp
new file mode 100644 (file)
index 0000000..3e6a738
--- /dev/null
@@ -0,0 +1,255 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2018 by Raul Herraiz.
+ * 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 <algorithm>
+#include <array>
+#include <cmath>
+#include <complex>
+#include <cstdlib>
+#include <iterator>
+
+#include "alc/effects/base.h"
+#include "alcomplex.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/mixer.h"
+#include "core/mixer/defs.h"
+#include "intrusive_ptr.h"
+
+
+namespace {
+
+using uint = unsigned int;
+using complex_d = std::complex<double>;
+
+constexpr size_t HilSize{1024};
+constexpr size_t HilHalfSize{HilSize >> 1};
+constexpr size_t OversampleFactor{4};
+
+static_assert(HilSize%OversampleFactor == 0, "Factor must be a clean divisor of the size");
+constexpr size_t HilStep{HilSize / OversampleFactor};
+
+/* Define a Hann window, used to filter the HIL input and output. */
+struct Windower {
+    alignas(16) std::array<double,HilSize> mData;
+
+    Windower()
+    {
+        /* Create lookup table of the Hann window for the desired size. */
+        for(size_t i{0};i < HilHalfSize;i++)
+        {
+            constexpr double scale{al::numbers::pi / double{HilSize}};
+            const double val{std::sin((static_cast<double>(i)+0.5) * scale)};
+            mData[i] = mData[HilSize-1-i] = val * val;
+        }
+    }
+};
+const Windower gWindow{};
+
+
+struct FshifterState final : public EffectState {
+    /* Effect parameters */
+    size_t mCount{};
+    size_t mPos{};
+    std::array<uint,2> mPhaseStep{};
+    std::array<uint,2> mPhase{};
+    std::array<double,2> mSign{};
+
+    /* Effects buffers */
+    std::array<double,HilSize> mInFIFO{};
+    std::array<complex_d,HilStep> mOutFIFO{};
+    std::array<complex_d,HilSize> mOutputAccum{};
+    std::array<complex_d,HilSize> mAnalytic{};
+    std::array<complex_d,BufferLineSize> mOutdata{};
+
+    alignas(16) FloatBufferLine mBufferOut{};
+
+    /* Effect gains for each output channel */
+    struct {
+        float Current[MaxAmbiChannels]{};
+        float Target[MaxAmbiChannels]{};
+    } mGains[2];
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(FshifterState)
+};
+
+void FshifterState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    /* (Re-)initializing parameters and clear the buffers. */
+    mCount = 0;
+    mPos = HilSize - HilStep;
+
+    mPhaseStep.fill(0u);
+    mPhase.fill(0u);
+    mSign.fill(1.0);
+    mInFIFO.fill(0.0);
+    mOutFIFO.fill(complex_d{});
+    mOutputAccum.fill(complex_d{});
+    mAnalytic.fill(complex_d{});
+
+    for(auto &gain : mGains)
+    {
+        std::fill(std::begin(gain.Current), std::end(gain.Current), 0.0f);
+        std::fill(std::begin(gain.Target), std::end(gain.Target), 0.0f);
+    }
+}
+
+void FshifterState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *device{context->mDevice};
+
+    const float step{props->Fshifter.Frequency / static_cast<float>(device->Frequency)};
+    mPhaseStep[0] = mPhaseStep[1] = fastf2u(minf(step, 1.0f) * MixerFracOne);
+
+    switch(props->Fshifter.LeftDirection)
+    {
+    case FShifterDirection::Down:
+        mSign[0] = -1.0;
+        break;
+    case FShifterDirection::Up:
+        mSign[0] = 1.0;
+        break;
+    case FShifterDirection::Off:
+        mPhase[0]     = 0;
+        mPhaseStep[0] = 0;
+        break;
+    }
+
+    switch(props->Fshifter.RightDirection)
+    {
+    case FShifterDirection::Down:
+        mSign[1] = -1.0;
+        break;
+    case FShifterDirection::Up:
+        mSign[1] = 1.0;
+        break;
+    case FShifterDirection::Off:
+        mPhase[1]     = 0;
+        mPhaseStep[1] = 0;
+        break;
+    }
+
+    static constexpr auto inv_sqrt2 = static_cast<float>(1.0 / al::numbers::sqrt2);
+    static constexpr auto lcoeffs_pw = CalcDirectionCoeffs({-1.0f, 0.0f, 0.0f});
+    static constexpr auto rcoeffs_pw = CalcDirectionCoeffs({ 1.0f, 0.0f, 0.0f});
+    static constexpr auto lcoeffs_nrml = CalcDirectionCoeffs({-inv_sqrt2, 0.0f, inv_sqrt2});
+    static constexpr auto rcoeffs_nrml = CalcDirectionCoeffs({ inv_sqrt2, 0.0f, inv_sqrt2});
+    auto &lcoeffs = (device->mRenderMode != RenderMode::Pairwise) ? lcoeffs_nrml : lcoeffs_pw;
+    auto &rcoeffs = (device->mRenderMode != RenderMode::Pairwise) ? rcoeffs_nrml : rcoeffs_pw;
+
+    mOutTarget = target.Main->Buffer;
+    ComputePanGains(target.Main, lcoeffs.data(), slot->Gain, mGains[0].Target);
+    ComputePanGains(target.Main, rcoeffs.data(), slot->Gain, mGains[1].Target);
+}
+
+void FshifterState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    for(size_t base{0u};base < samplesToDo;)
+    {
+        size_t todo{minz(HilStep-mCount, samplesToDo-base)};
+
+        /* Fill FIFO buffer with samples data */
+        const size_t pos{mPos};
+        size_t count{mCount};
+        do {
+            mInFIFO[pos+count] = samplesIn[0][base];
+            mOutdata[base] = mOutFIFO[count];
+            ++base; ++count;
+        } while(--todo);
+        mCount = count;
+
+        /* Check whether FIFO buffer is filled */
+        if(mCount < HilStep) break;
+        mCount = 0;
+        mPos = (mPos+HilStep) & (HilSize-1);
+
+        /* Real signal windowing and store in Analytic buffer */
+        for(size_t src{mPos}, k{0u};src < HilSize;++src,++k)
+            mAnalytic[k] = mInFIFO[src]*gWindow.mData[k];
+        for(size_t src{0u}, k{HilSize-mPos};src < mPos;++src,++k)
+            mAnalytic[k] = mInFIFO[src]*gWindow.mData[k];
+
+        /* Processing signal by Discrete Hilbert Transform (analytical signal). */
+        complex_hilbert(mAnalytic);
+
+        /* Windowing and add to output accumulator */
+        for(size_t dst{mPos}, k{0u};dst < HilSize;++dst,++k)
+            mOutputAccum[dst] += 2.0/OversampleFactor*gWindow.mData[k]*mAnalytic[k];
+        for(size_t dst{0u}, k{HilSize-mPos};dst < mPos;++dst,++k)
+            mOutputAccum[dst] += 2.0/OversampleFactor*gWindow.mData[k]*mAnalytic[k];
+
+        /* Copy out the accumulated result, then clear for the next iteration. */
+        std::copy_n(mOutputAccum.cbegin() + mPos, HilStep, mOutFIFO.begin());
+        std::fill_n(mOutputAccum.begin() + mPos, HilStep, complex_d{});
+    }
+
+    /* Process frequency shifter using the analytic signal obtained. */
+    float *RESTRICT BufferOut{al::assume_aligned<16>(mBufferOut.data())};
+    for(size_t c{0};c < 2;++c)
+    {
+        const uint phase_step{mPhaseStep[c]};
+        uint phase_idx{mPhase[c]};
+        for(size_t k{0};k < samplesToDo;++k)
+        {
+            const double phase{phase_idx * (al::numbers::pi*2.0 / MixerFracOne)};
+            BufferOut[k] = static_cast<float>(mOutdata[k].real()*std::cos(phase) +
+                mOutdata[k].imag()*std::sin(phase)*mSign[c]);
+
+            phase_idx += phase_step;
+            phase_idx &= MixerFracMask;
+        }
+        mPhase[c] = phase_idx;
+
+        /* Now, mix the processed sound data to the output. */
+        MixSamples({BufferOut, samplesToDo}, samplesOut, mGains[c].Current, mGains[c].Target,
+            maxz(samplesToDo, 512), 0);
+    }
+}
+
+
+struct FshifterStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new FshifterState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *FshifterStateFactory_getFactory()
+{
+    static FshifterStateFactory FshifterFactory{};
+    return &FshifterFactory;
+}
diff --git a/alc/effects/modulator.cpp b/alc/effects/modulator.cpp
new file mode 100644 (file)
index 0000000..14ee500
--- /dev/null
@@ -0,0 +1,193 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2009 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 <algorithm>
+#include <array>
+#include <cstdlib>
+#include <iterator>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/filters/biquad.h"
+#include "core/mixer.h"
+#include "intrusive_ptr.h"
+
+
+namespace {
+
+using uint = unsigned int;
+
+#define MAX_UPDATE_SAMPLES 128
+
+#define WAVEFORM_FRACBITS  24
+#define WAVEFORM_FRACONE   (1<<WAVEFORM_FRACBITS)
+#define WAVEFORM_FRACMASK  (WAVEFORM_FRACONE-1)
+
+inline float Sin(uint index)
+{
+    constexpr float scale{al::numbers::pi_v<float>*2.0f / WAVEFORM_FRACONE};
+    return std::sin(static_cast<float>(index) * scale);
+}
+
+inline float Saw(uint index)
+{ return static_cast<float>(index)*(2.0f/WAVEFORM_FRACONE) - 1.0f; }
+
+inline float Square(uint index)
+{ return static_cast<float>(static_cast<int>((index>>(WAVEFORM_FRACBITS-2))&2) - 1); }
+
+inline float One(uint) { return 1.0f; }
+
+template<float (&func)(uint)>
+void Modulate(float *RESTRICT dst, uint index, const uint step, size_t todo)
+{
+    for(size_t i{0u};i < todo;i++)
+    {
+        index += step;
+        index &= WAVEFORM_FRACMASK;
+        dst[i] = func(index);
+    }
+}
+
+
+struct ModulatorState final : public EffectState {
+    void (*mGetSamples)(float*RESTRICT, uint, const uint, size_t){};
+
+    uint mIndex{0};
+    uint mStep{1};
+
+    struct {
+        uint mTargetChannel{InvalidChannelIndex};
+
+        BiquadFilter mFilter;
+
+        float mCurrentGain{};
+        float mTargetGain{};
+    } mChans[MaxAmbiChannels];
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(ModulatorState)
+};
+
+void ModulatorState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    for(auto &e : mChans)
+    {
+        e.mTargetChannel = InvalidChannelIndex;
+        e.mFilter.clear();
+        e.mCurrentGain = 0.0f;
+    }
+}
+
+void ModulatorState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *device{context->mDevice};
+
+    const float step{props->Modulator.Frequency / static_cast<float>(device->Frequency)};
+    mStep = fastf2u(clampf(step*WAVEFORM_FRACONE, 0.0f, float{WAVEFORM_FRACONE-1}));
+
+    if(mStep == 0)
+        mGetSamples = Modulate<One>;
+    else if(props->Modulator.Waveform == ModulatorWaveform::Sinusoid)
+        mGetSamples = Modulate<Sin>;
+    else if(props->Modulator.Waveform == ModulatorWaveform::Sawtooth)
+        mGetSamples = Modulate<Saw>;
+    else /*if(props->Modulator.Waveform == ModulatorWaveform::Square)*/
+        mGetSamples = Modulate<Square>;
+
+    float f0norm{props->Modulator.HighPassCutoff / static_cast<float>(device->Frequency)};
+    f0norm = clampf(f0norm, 1.0f/512.0f, 0.49f);
+    /* Bandwidth value is constant in octaves. */
+    mChans[0].mFilter.setParamsFromBandwidth(BiquadType::HighPass, f0norm, 1.0f, 0.75f);
+    for(size_t i{1u};i < slot->Wet.Buffer.size();++i)
+        mChans[i].mFilter.copyParamsFrom(mChans[0].mFilter);
+
+    mOutTarget = target.Main->Buffer;
+    auto set_channel = [this](size_t idx, uint outchan, float outgain)
+    {
+        mChans[idx].mTargetChannel = outchan;
+        mChans[idx].mTargetGain = outgain;
+    };
+    target.Main->setAmbiMixParams(slot->Wet, slot->Gain, set_channel);
+}
+
+void ModulatorState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    for(size_t base{0u};base < samplesToDo;)
+    {
+        alignas(16) float modsamples[MAX_UPDATE_SAMPLES];
+        const size_t td{minz(MAX_UPDATE_SAMPLES, samplesToDo-base)};
+
+        mGetSamples(modsamples, mIndex, mStep, td);
+        mIndex += static_cast<uint>(mStep * td);
+        mIndex &= WAVEFORM_FRACMASK;
+
+        auto chandata = std::begin(mChans);
+        for(const auto &input : samplesIn)
+        {
+            const size_t outidx{chandata->mTargetChannel};
+            if(outidx != InvalidChannelIndex)
+            {
+                alignas(16) float temps[MAX_UPDATE_SAMPLES];
+
+                chandata->mFilter.process({&input[base], td}, temps);
+                for(size_t i{0u};i < td;i++)
+                    temps[i] *= modsamples[i];
+
+                MixSamples({temps, td}, samplesOut[outidx].data()+base, chandata->mCurrentGain,
+                    chandata->mTargetGain, samplesToDo-base);
+            }
+            ++chandata;
+        }
+
+        base += td;
+    }
+}
+
+
+struct ModulatorStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new ModulatorState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *ModulatorStateFactory_getFactory()
+{
+    static ModulatorStateFactory ModulatorFactory{};
+    return &ModulatorFactory;
+}
diff --git a/alc/effects/null.cpp b/alc/effects/null.cpp
new file mode 100644 (file)
index 0000000..1f9ae67
--- /dev/null
@@ -0,0 +1,84 @@
+
+#include "config.h"
+
+#include <stddef.h>
+
+#include "almalloc.h"
+#include "alspan.h"
+#include "base.h"
+#include "core/bufferline.h"
+#include "intrusive_ptr.h"
+
+struct ContextBase;
+struct DeviceBase;
+struct EffectSlot;
+
+
+namespace {
+
+struct NullState final : public EffectState {
+    NullState();
+    ~NullState() override;
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(NullState)
+};
+
+/* This constructs the effect state. It's called when the object is first
+ * created.
+ */
+NullState::NullState() = default;
+
+/* This destructs the effect state. It's called only when the effect instance
+ * is no longer used.
+ */
+NullState::~NullState() = default;
+
+/* This updates the device-dependant effect state. This is called on state
+ * initialization and any time the device parameters (e.g. playback frequency,
+ * format) have been changed. Will always be followed by a call to the update
+ * method, if successful.
+ */
+void NullState::deviceUpdate(const DeviceBase* /*device*/, const BufferStorage* /*buffer*/)
+{
+}
+
+/* This updates the effect state with new properties. This is called any time
+ * the effect is (re)loaded into a slot.
+ */
+void NullState::update(const ContextBase* /*context*/, const EffectSlot* /*slot*/,
+    const EffectProps* /*props*/, const EffectTarget /*target*/)
+{
+}
+
+/* This processes the effect state, for the given number of samples from the
+ * input to the output buffer. The result should be added to the output buffer,
+ * not replace it.
+ */
+void NullState::process(const size_t/*samplesToDo*/,
+    const al::span<const FloatBufferLine> /*samplesIn*/,
+    const al::span<FloatBufferLine> /*samplesOut*/)
+{
+}
+
+
+struct NullStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override;
+};
+
+/* Creates EffectState objects of the appropriate type. */
+al::intrusive_ptr<EffectState> NullStateFactory::create()
+{ return al::intrusive_ptr<EffectState>{new NullState{}}; }
+
+} // namespace
+
+EffectStateFactory *NullStateFactory_getFactory()
+{
+    static NullStateFactory NullFactory{};
+    return &NullFactory;
+}
diff --git a/alc/effects/pshifter.cpp b/alc/effects/pshifter.cpp
new file mode 100644 (file)
index 0000000..426a226
--- /dev/null
@@ -0,0 +1,307 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2018 by Raul Herraiz.
+ * 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 <algorithm>
+#include <array>
+#include <cmath>
+#include <complex>
+#include <cstdlib>
+#include <iterator>
+
+#include "alc/effects/base.h"
+#include "alcomplex.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/bufferline.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/mixer.h"
+#include "core/mixer/defs.h"
+#include "intrusive_ptr.h"
+
+struct ContextBase;
+
+
+namespace {
+
+using uint = unsigned int;
+using complex_f = std::complex<float>;
+
+constexpr size_t StftSize{1024};
+constexpr size_t StftHalfSize{StftSize >> 1};
+constexpr size_t OversampleFactor{8};
+
+static_assert(StftSize%OversampleFactor == 0, "Factor must be a clean divisor of the size");
+constexpr size_t StftStep{StftSize / OversampleFactor};
+
+/* Define a Hann window, used to filter the STFT input and output. */
+struct Windower {
+    alignas(16) std::array<float,StftSize> mData;
+
+    Windower()
+    {
+        /* Create lookup table of the Hann window for the desired size. */
+        for(size_t i{0};i < StftHalfSize;i++)
+        {
+            constexpr double scale{al::numbers::pi / double{StftSize}};
+            const double val{std::sin((static_cast<double>(i)+0.5) * scale)};
+            mData[i] = mData[StftSize-1-i] = static_cast<float>(val * val);
+        }
+    }
+};
+const Windower gWindow{};
+
+
+struct FrequencyBin {
+    float Magnitude;
+    float FreqBin;
+};
+
+
+struct PshifterState final : public EffectState {
+    /* Effect parameters */
+    size_t mCount;
+    size_t mPos;
+    uint mPitchShiftI;
+    float mPitchShift;
+
+    /* Effects buffers */
+    std::array<float,StftSize> mFIFO;
+    std::array<float,StftHalfSize+1> mLastPhase;
+    std::array<float,StftHalfSize+1> mSumPhase;
+    std::array<float,StftSize> mOutputAccum;
+
+    std::array<complex_f,StftSize> mFftBuffer;
+
+    std::array<FrequencyBin,StftHalfSize+1> mAnalysisBuffer;
+    std::array<FrequencyBin,StftHalfSize+1> mSynthesisBuffer;
+
+    alignas(16) FloatBufferLine mBufferOut;
+
+    /* Effect gains for each output channel */
+    float mCurrentGains[MaxAmbiChannels];
+    float mTargetGains[MaxAmbiChannels];
+
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(PshifterState)
+};
+
+void PshifterState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    /* (Re-)initializing parameters and clear the buffers. */
+    mCount       = 0;
+    mPos         = StftSize - StftStep;
+    mPitchShiftI = MixerFracOne;
+    mPitchShift  = 1.0f;
+
+    mFIFO.fill(0.0f);
+    mLastPhase.fill(0.0f);
+    mSumPhase.fill(0.0f);
+    mOutputAccum.fill(0.0f);
+    mFftBuffer.fill(complex_f{});
+    mAnalysisBuffer.fill(FrequencyBin{});
+    mSynthesisBuffer.fill(FrequencyBin{});
+
+    std::fill(std::begin(mCurrentGains), std::end(mCurrentGains), 0.0f);
+    std::fill(std::begin(mTargetGains),  std::end(mTargetGains),  0.0f);
+}
+
+void PshifterState::update(const ContextBase*, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const int tune{props->Pshifter.CoarseTune*100 + props->Pshifter.FineTune};
+    const float pitch{std::pow(2.0f, static_cast<float>(tune) / 1200.0f)};
+    mPitchShiftI = clampu(fastf2u(pitch*MixerFracOne), MixerFracHalf, MixerFracOne*2);
+    mPitchShift  = static_cast<float>(mPitchShiftI) * float{1.0f/MixerFracOne};
+
+    static constexpr auto coeffs = CalcDirectionCoeffs({0.0f, 0.0f, -1.0f});
+
+    mOutTarget = target.Main->Buffer;
+    ComputePanGains(target.Main, coeffs.data(), slot->Gain, mTargetGains);
+}
+
+void PshifterState::process(const size_t samplesToDo,
+    const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    /* Pitch shifter engine based on the work of Stephan Bernsee.
+     * http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/
+     */
+
+    /* Cycle offset per update expected of each frequency bin (bin 0 is none,
+     * bin 1 is x1, bin 2 is x2, etc).
+     */
+    constexpr float expected_cycles{al::numbers::pi_v<float>*2.0f / OversampleFactor};
+
+    for(size_t base{0u};base < samplesToDo;)
+    {
+        const size_t todo{minz(StftStep-mCount, samplesToDo-base)};
+
+        /* Retrieve the output samples from the FIFO and fill in the new input
+         * samples.
+         */
+        auto fifo_iter = mFIFO.begin()+mPos + mCount;
+        std::copy_n(fifo_iter, todo, mBufferOut.begin()+base);
+
+        std::copy_n(samplesIn[0].begin()+base, todo, fifo_iter);
+        mCount += todo;
+        base += todo;
+
+        /* Check whether FIFO buffer is filled with new samples. */
+        if(mCount < StftStep) break;
+        mCount = 0;
+        mPos = (mPos+StftStep) & (mFIFO.size()-1);
+
+        /* Time-domain signal windowing, store in FftBuffer, and apply a
+         * forward FFT to get the frequency-domain signal.
+         */
+        for(size_t src{mPos}, k{0u};src < StftSize;++src,++k)
+            mFftBuffer[k] = mFIFO[src] * gWindow.mData[k];
+        for(size_t src{0u}, k{StftSize-mPos};src < mPos;++src,++k)
+            mFftBuffer[k] = mFIFO[src] * gWindow.mData[k];
+        forward_fft(al::as_span(mFftBuffer));
+
+        /* Analyze the obtained data. Since the real FFT is symmetric, only
+         * StftHalfSize+1 samples are needed.
+         */
+        for(size_t k{0u};k < StftHalfSize+1;k++)
+        {
+            const float magnitude{std::abs(mFftBuffer[k])};
+            const float phase{std::arg(mFftBuffer[k])};
+
+            /* Compute the phase difference from the last update and subtract
+             * the expected phase difference for this bin.
+             *
+             * When oversampling, the expected per-update offset increments by
+             * 1/OversampleFactor for every frequency bin. So, the offset wraps
+             * every 'OversampleFactor' bin.
+             */
+            const auto bin_offset = static_cast<float>(k % OversampleFactor);
+            float tmp{(phase - mLastPhase[k]) - bin_offset*expected_cycles};
+            /* Store the actual phase for the next update. */
+            mLastPhase[k] = phase;
+
+            /* Normalize from pi, and wrap the delta between -1 and +1. */
+            tmp *= al::numbers::inv_pi_v<float>;
+            int qpd{float2int(tmp)};
+            tmp -= static_cast<float>(qpd + (qpd%2));
+
+            /* Get deviation from bin frequency (-0.5 to +0.5), and account for
+             * oversampling.
+             */
+            tmp *= 0.5f * OversampleFactor;
+
+            /* Compute the k-th partials' frequency bin target and store the
+             * magnitude and frequency bin in the analysis buffer. We don't
+             * need the "true frequency" since it's a linear relationship with
+             * the bin.
+             */
+            mAnalysisBuffer[k].Magnitude = magnitude;
+            mAnalysisBuffer[k].FreqBin = static_cast<float>(k) + tmp;
+        }
+
+        /* Shift the frequency bins according to the pitch adjustment,
+         * accumulating the magnitudes of overlapping frequency bins.
+         */
+        std::fill(mSynthesisBuffer.begin(), mSynthesisBuffer.end(), FrequencyBin{});
+
+        constexpr size_t bin_limit{((StftHalfSize+1)<<MixerFracBits) - MixerFracHalf - 1};
+        const size_t bin_count{minz(StftHalfSize+1, bin_limit/mPitchShiftI + 1)};
+        for(size_t k{0u};k < bin_count;k++)
+        {
+            const size_t j{(k*mPitchShiftI + MixerFracHalf) >> MixerFracBits};
+
+            /* If more than two bins end up together, use the target frequency
+             * bin for the one with the dominant magnitude. There might be a
+             * better way to handle this, but it's better than last-index-wins.
+             */
+            if(mAnalysisBuffer[k].Magnitude > mSynthesisBuffer[j].Magnitude)
+                mSynthesisBuffer[j].FreqBin = mAnalysisBuffer[k].FreqBin * mPitchShift;
+            mSynthesisBuffer[j].Magnitude += mAnalysisBuffer[k].Magnitude;
+        }
+
+        /* Reconstruct the frequency-domain signal from the adjusted frequency
+         * bins.
+         */
+        for(size_t k{0u};k < StftHalfSize+1;k++)
+        {
+            /* Calculate the actual delta phase for this bin's target frequency
+             * bin, and accumulate it to get the actual bin phase.
+             */
+            float tmp{mSumPhase[k] + mSynthesisBuffer[k].FreqBin*expected_cycles};
+
+            /* Wrap between -pi and +pi for the sum. If mSumPhase is left to
+             * grow indefinitely, it will lose precision and produce less exact
+             * phase over time.
+             */
+            tmp *= al::numbers::inv_pi_v<float>;
+            int qpd{float2int(tmp)};
+            tmp -= static_cast<float>(qpd + (qpd%2));
+            mSumPhase[k] = tmp * al::numbers::pi_v<float>;
+
+            mFftBuffer[k] = std::polar(mSynthesisBuffer[k].Magnitude, mSumPhase[k]);
+        }
+        for(size_t k{StftHalfSize+1};k < StftSize;++k)
+            mFftBuffer[k] = std::conj(mFftBuffer[StftSize-k]);
+
+        /* Apply an inverse FFT to get the time-domain signal, and accumulate
+         * for the output with windowing.
+         */
+        inverse_fft(al::as_span(mFftBuffer));
+
+        static constexpr float scale{3.0f / OversampleFactor / StftSize};
+        for(size_t dst{mPos}, k{0u};dst < StftSize;++dst,++k)
+            mOutputAccum[dst] += gWindow.mData[k]*mFftBuffer[k].real() * scale;
+        for(size_t dst{0u}, k{StftSize-mPos};dst < mPos;++dst,++k)
+            mOutputAccum[dst] += gWindow.mData[k]*mFftBuffer[k].real() * scale;
+
+        /* Copy out the accumulated result, then clear for the next iteration. */
+        std::copy_n(mOutputAccum.begin() + mPos, StftStep, mFIFO.begin() + mPos);
+        std::fill_n(mOutputAccum.begin() + mPos, StftStep, 0.0f);
+    }
+
+    /* Now, mix the processed sound data to the output. */
+    MixSamples({mBufferOut.data(), samplesToDo}, samplesOut, mCurrentGains, mTargetGains,
+        maxz(samplesToDo, 512), 0);
+}
+
+
+struct PshifterStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new PshifterState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *PshifterStateFactory_getFactory()
+{
+    static PshifterStateFactory PshifterFactory{};
+    return &PshifterFactory;
+}
diff --git a/alc/effects/reverb.cpp b/alc/effects/reverb.cpp
new file mode 100644 (file)
index 0000000..3875bed
--- /dev/null
@@ -0,0 +1,1770 @@
+/**
+ * Ambisonic reverb engine for the OpenAL cross platform audio library
+ * Copyright (C) 2008-2017 by Chris Robinson and Christopher Fitzgerald.
+ * 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 <algorithm>
+#include <array>
+#include <cstdio>
+#include <functional>
+#include <iterator>
+#include <numeric>
+#include <stdint.h>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/filters/biquad.h"
+#include "core/filters/splitter.h"
+#include "core/mixer.h"
+#include "core/mixer/defs.h"
+#include "intrusive_ptr.h"
+#include "opthelpers.h"
+#include "vecmat.h"
+#include "vector.h"
+
+/* This is a user config option for modifying the overall output of the reverb
+ * effect.
+ */
+float ReverbBoost = 1.0f;
+
+namespace {
+
+using uint = unsigned int;
+
+constexpr float MaxModulationTime{4.0f};
+constexpr float DefaultModulationTime{0.25f};
+
+#define MOD_FRACBITS 24
+#define MOD_FRACONE  (1<<MOD_FRACBITS)
+#define MOD_FRACMASK (MOD_FRACONE-1)
+
+
+struct CubicFilter {
+    static constexpr size_t sTableBits{8};
+    static constexpr size_t sTableSteps{1 << sTableBits};
+    static constexpr size_t sTableMask{sTableSteps - 1};
+
+    float mFilter[sTableSteps*2 + 1]{};
+
+    constexpr CubicFilter()
+    {
+        /* This creates a lookup table for a cubic spline filter, with 256
+         * steps between samples. Only half the coefficients are needed, since
+         * Coeff2 is just Coeff1 in reverse and Coeff3 is just Coeff0 in
+         * reverse.
+         */
+        for(size_t i{0};i < sTableSteps;++i)
+        {
+            const double mu{static_cast<double>(i) / double{sTableSteps}};
+            const double mu2{mu*mu}, mu3{mu2*mu};
+            const double a0{-0.5*mu3 +      mu2 + -0.5*mu};
+            const double a1{ 1.5*mu3 + -2.5*mu2           + 1.0f};
+            mFilter[i] = static_cast<float>(a1);
+            mFilter[sTableSteps+i] = static_cast<float>(a0);
+        }
+    }
+
+    constexpr float getCoeff0(size_t i) const noexcept { return mFilter[sTableSteps+i]; }
+    constexpr float getCoeff1(size_t i) const noexcept { return mFilter[i]; }
+    constexpr float getCoeff2(size_t i) const noexcept { return mFilter[sTableSteps-i]; }
+    constexpr float getCoeff3(size_t i) const noexcept { return mFilter[sTableSteps*2-i]; }
+};
+constexpr CubicFilter gCubicTable;
+
+
+using namespace std::placeholders;
+
+/* Max samples per process iteration. Used to limit the size needed for
+ * temporary buffers. Must be a multiple of 4 for SIMD alignment.
+ */
+constexpr size_t MAX_UPDATE_SAMPLES{256};
+
+/* The number of spatialized lines or channels to process. Four channels allows
+ * for a 3D A-Format response. NOTE: This can't be changed without taking care
+ * of the conversion matrices, and a few places where the length arrays are
+ * assumed to have 4 elements.
+ */
+constexpr size_t NUM_LINES{4u};
+
+
+/* This coefficient is used to define the maximum frequency range controlled by
+ * the modulation depth. The current value of 0.05 will allow it to swing from
+ * 0.95x to 1.05x. This value must be below 1. At 1 it will cause the sampler
+ * to stall on the downswing, and above 1 it will cause it to sample backwards.
+ * The value 0.05 seems be nearest to Creative hardware behavior.
+ */
+constexpr float MODULATION_DEPTH_COEFF{0.05f};
+
+
+/* The B-Format to A-Format conversion matrix. The arrangement of rows is
+ * deliberately chosen to align the resulting lines to their spatial opposites
+ * (0:above front left <-> 3:above back right, 1:below front right <-> 2:below
+ * back left). It's not quite opposite, since the A-Format results in a
+ * tetrahedron, but it's close enough. Should the model be extended to 8-lines
+ * in the future, true opposites can be used.
+ */
+alignas(16) constexpr float B2A[NUM_LINES][NUM_LINES]{
+    { 0.5f,  0.5f,  0.5f,  0.5f },
+    { 0.5f, -0.5f, -0.5f,  0.5f },
+    { 0.5f,  0.5f, -0.5f, -0.5f },
+    { 0.5f, -0.5f,  0.5f, -0.5f }
+};
+
+/* Converts A-Format to B-Format for early reflections. */
+alignas(16) constexpr std::array<std::array<float,NUM_LINES>,NUM_LINES> EarlyA2B{{
+    {{ 0.5f,  0.5f,  0.5f,  0.5f }},
+    {{ 0.5f, -0.5f,  0.5f, -0.5f }},
+    {{ 0.5f, -0.5f, -0.5f,  0.5f }},
+    {{ 0.5f,  0.5f, -0.5f, -0.5f }}
+}};
+
+/* Converts A-Format to B-Format for late reverb. */
+constexpr auto InvSqrt2 = static_cast<float>(1.0/al::numbers::sqrt2);
+alignas(16) constexpr std::array<std::array<float,NUM_LINES>,NUM_LINES> LateA2B{{
+    {{ 0.5f,  0.5f,  0.5f,  0.5f }},
+    {{ InvSqrt2, -InvSqrt2,  0.0f,  0.0f }},
+    {{ 0.0f,  0.0f,  InvSqrt2, -InvSqrt2 }},
+    {{ 0.5f,  0.5f, -0.5f, -0.5f }}
+}};
+
+/* The all-pass and delay lines have a variable length dependent on the
+ * effect's density parameter, which helps alter the perceived environment
+ * size. The size-to-density conversion is a cubed scale:
+ *
+ * density = min(1.0, pow(size, 3.0) / DENSITY_SCALE);
+ *
+ * The line lengths scale linearly with room size, so the inverse density
+ * conversion is needed, taking the cube root of the re-scaled density to
+ * calculate the line length multiplier:
+ *
+ *     length_mult = max(5.0, cbrt(density*DENSITY_SCALE));
+ *
+ * The density scale below will result in a max line multiplier of 50, for an
+ * effective size range of 5m to 50m.
+ */
+constexpr float DENSITY_SCALE{125000.0f};
+
+/* All delay line lengths are specified in seconds.
+ *
+ * To approximate early reflections, we break them up into primary (those
+ * arriving from the same direction as the source) and secondary (those
+ * arriving from the opposite direction).
+ *
+ * The early taps decorrelate the 4-channel signal to approximate an average
+ * room response for the primary reflections after the initial early delay.
+ *
+ * Given an average room dimension (d_a) and the speed of sound (c) we can
+ * calculate the average reflection delay (r_a) regardless of listener and
+ * source positions as:
+ *
+ *     r_a = d_a / c
+ *     c   = 343.3
+ *
+ * This can extended to finding the average difference (r_d) between the
+ * maximum (r_1) and minimum (r_0) reflection delays:
+ *
+ *     r_0 = 2 / 3 r_a
+ *         = r_a - r_d / 2
+ *         = r_d
+ *     r_1 = 4 / 3 r_a
+ *         = r_a + r_d / 2
+ *         = 2 r_d
+ *     r_d = 2 / 3 r_a
+ *         = r_1 - r_0
+ *
+ * As can be determined by integrating the 1D model with a source (s) and
+ * listener (l) positioned across the dimension of length (d_a):
+ *
+ *     r_d = int_(l=0)^d_a (int_(s=0)^d_a |2 d_a - 2 (l + s)| ds) dl / c
+ *
+ * The initial taps (T_(i=0)^N) are then specified by taking a power series
+ * that ranges between r_0 and half of r_1 less r_0:
+ *
+ *     R_i = 2^(i / (2 N - 1)) r_d
+ *         = r_0 + (2^(i / (2 N - 1)) - 1) r_d
+ *         = r_0 + T_i
+ *     T_i = R_i - r_0
+ *         = (2^(i / (2 N - 1)) - 1) r_d
+ *
+ * Assuming an average of 1m, we get the following taps:
+ */
+constexpr std::array<float,NUM_LINES> EARLY_TAP_LENGTHS{{
+    0.0000000e+0f, 2.0213520e-4f, 4.2531060e-4f, 6.7171600e-4f
+}};
+
+/* The early all-pass filter lengths are based on the early tap lengths:
+ *
+ *     A_i = R_i / a
+ *
+ * Where a is the approximate maximum all-pass cycle limit (20).
+ */
+constexpr std::array<float,NUM_LINES> EARLY_ALLPASS_LENGTHS{{
+    9.7096800e-5f, 1.0720356e-4f, 1.1836234e-4f, 1.3068260e-4f
+}};
+
+/* The early delay lines are used to transform the primary reflections into
+ * the secondary reflections.  The A-format is arranged in such a way that
+ * the channels/lines are spatially opposite:
+ *
+ *     C_i is opposite C_(N-i-1)
+ *
+ * The delays of the two opposing reflections (R_i and O_i) from a source
+ * anywhere along a particular dimension always sum to twice its full delay:
+ *
+ *     2 r_a = R_i + O_i
+ *
+ * With that in mind we can determine the delay between the two reflections
+ * and thus specify our early line lengths (L_(i=0)^N) using:
+ *
+ *     O_i = 2 r_a - R_(N-i-1)
+ *     L_i = O_i - R_(N-i-1)
+ *         = 2 (r_a - R_(N-i-1))
+ *         = 2 (r_a - T_(N-i-1) - r_0)
+ *         = 2 r_a (1 - (2 / 3) 2^((N - i - 1) / (2 N - 1)))
+ *
+ * Using an average dimension of 1m, we get:
+ */
+constexpr std::array<float,NUM_LINES> EARLY_LINE_LENGTHS{{
+    5.9850400e-4f, 1.0913150e-3f, 1.5376658e-3f, 1.9419362e-3f
+}};
+
+/* The late all-pass filter lengths are based on the late line lengths:
+ *
+ *     A_i = (5 / 3) L_i / r_1
+ */
+constexpr std::array<float,NUM_LINES> LATE_ALLPASS_LENGTHS{{
+    1.6182800e-4f, 2.0389060e-4f, 2.8159360e-4f, 3.2365600e-4f
+}};
+
+/* The late lines are used to approximate the decaying cycle of recursive
+ * late reflections.
+ *
+ * Splitting the lines in half, we start with the shortest reflection paths
+ * (L_(i=0)^(N/2)):
+ *
+ *     L_i = 2^(i / (N - 1)) r_d
+ *
+ * Then for the opposite (longest) reflection paths (L_(i=N/2)^N):
+ *
+ *     L_i = 2 r_a - L_(i-N/2)
+ *         = 2 r_a - 2^((i - N / 2) / (N - 1)) r_d
+ *
+ * For our 1m average room, we get:
+ */
+constexpr std::array<float,NUM_LINES> LATE_LINE_LENGTHS{{
+    1.9419362e-3f, 2.4466860e-3f, 3.3791220e-3f, 3.8838720e-3f
+}};
+
+
+using ReverbUpdateLine = std::array<float,MAX_UPDATE_SAMPLES>;
+
+struct DelayLineI {
+    /* The delay lines use interleaved samples, with the lengths being powers
+     * of 2 to allow the use of bit-masking instead of a modulus for wrapping.
+     */
+    size_t Mask{0u};
+    union {
+        uintptr_t LineOffset{0u};
+        std::array<float,NUM_LINES> *Line;
+    };
+
+    /* Given the allocated sample buffer, this function updates each delay line
+     * offset.
+     */
+    void realizeLineOffset(std::array<float,NUM_LINES> *sampleBuffer) noexcept
+    { Line = sampleBuffer + LineOffset; }
+
+    /* Calculate the length of a delay line and store its mask and offset. */
+    uint calcLineLength(const float length, const uintptr_t offset, const float frequency,
+        const uint extra)
+    {
+        /* All line lengths are powers of 2, calculated from their lengths in
+         * seconds, rounded up.
+         */
+        uint samples{float2uint(std::ceil(length*frequency))};
+        samples = NextPowerOf2(samples + extra);
+
+        /* All lines share a single sample buffer. */
+        Mask = samples - 1;
+        LineOffset = offset;
+
+        /* Return the sample count for accumulation. */
+        return samples;
+    }
+
+    void write(size_t offset, const size_t c, const float *RESTRICT in, const size_t count) const noexcept
+    {
+        ASSUME(count > 0);
+        for(size_t i{0u};i < count;)
+        {
+            offset &= Mask;
+            size_t td{minz(Mask+1 - offset, count - i)};
+            do {
+                Line[offset++][c] = in[i++];
+            } while(--td);
+        }
+    }
+};
+
+struct VecAllpass {
+    DelayLineI Delay;
+    float Coeff{0.0f};
+    size_t Offset[NUM_LINES]{};
+
+    void process(const al::span<ReverbUpdateLine,NUM_LINES> samples, size_t offset,
+        const float xCoeff, const float yCoeff, const size_t todo);
+};
+
+struct T60Filter {
+    /* Two filters are used to adjust the signal. One to control the low
+     * frequencies, and one to control the high frequencies.
+     */
+    float MidGain{0.0f};
+    BiquadFilter HFFilter, LFFilter;
+
+    void calcCoeffs(const float length, const float lfDecayTime, const float mfDecayTime,
+        const float hfDecayTime, const float lf0norm, const float hf0norm);
+
+    /* Applies the two T60 damping filter sections. */
+    void process(const al::span<float> samples)
+    { DualBiquad{HFFilter, LFFilter}.process(samples, samples.data()); }
+
+    void clear() noexcept { HFFilter.clear(); LFFilter.clear(); }
+};
+
+struct EarlyReflections {
+    /* A Gerzon vector all-pass filter is used to simulate initial diffusion.
+     * The spread from this filter also helps smooth out the reverb tail.
+     */
+    VecAllpass VecAp;
+
+    /* An echo line is used to complete the second half of the early
+     * reflections.
+     */
+    DelayLineI Delay;
+    size_t Offset[NUM_LINES]{};
+    float Coeff[NUM_LINES]{};
+
+    /* The gain for each output channel based on 3D panning. */
+    float CurrentGains[NUM_LINES][MaxAmbiChannels]{};
+    float TargetGains[NUM_LINES][MaxAmbiChannels]{};
+
+    void updateLines(const float density_mult, const float diffusion, const float decayTime,
+        const float frequency);
+};
+
+
+struct Modulation {
+    /* The vibrato time is tracked with an index over a (MOD_FRACONE)
+     * normalized range.
+     */
+    uint Index, Step;
+
+    /* The depth of frequency change, in samples. */
+    float Depth;
+
+    float ModDelays[MAX_UPDATE_SAMPLES];
+
+    void updateModulator(float modTime, float modDepth, float frequency);
+
+    void calcDelays(size_t todo);
+};
+
+struct LateReverb {
+    /* A recursive delay line is used fill in the reverb tail. */
+    DelayLineI Delay;
+    size_t     Offset[NUM_LINES]{};
+
+    /* Attenuation to compensate for the modal density and decay rate of the
+     * late lines.
+     */
+    float DensityGain{0.0f};
+
+    /* T60 decay filters are used to simulate absorption. */
+    T60Filter T60[NUM_LINES];
+
+    Modulation Mod;
+
+    /* A Gerzon vector all-pass filter is used to simulate diffusion. */
+    VecAllpass VecAp;
+
+    /* The gain for each output channel based on 3D panning. */
+    float CurrentGains[NUM_LINES][MaxAmbiChannels]{};
+    float TargetGains[NUM_LINES][MaxAmbiChannels]{};
+
+    void updateLines(const float density_mult, const float diffusion, const float lfDecayTime,
+        const float mfDecayTime, const float hfDecayTime, const float lf0norm,
+        const float hf0norm, const float frequency);
+
+    void clear() noexcept
+    {
+        for(auto &filter : T60)
+            filter.clear();
+    }
+};
+
+struct ReverbPipeline {
+    /* Master effect filters */
+    struct {
+        BiquadFilter Lp;
+        BiquadFilter Hp;
+    } mFilter[NUM_LINES];
+
+    /* Core delay line (early reflections and late reverb tap from this). */
+    DelayLineI mEarlyDelayIn;
+    DelayLineI mLateDelayIn;
+
+    /* Tap points for early reflection delay. */
+    size_t mEarlyDelayTap[NUM_LINES][2]{};
+    float mEarlyDelayCoeff[NUM_LINES]{};
+
+    /* Tap points for late reverb feed and delay. */
+    size_t mLateDelayTap[NUM_LINES][2]{};
+
+    /* Coefficients for the all-pass and line scattering matrices. */
+    float mMixX{0.0f};
+    float mMixY{0.0f};
+
+    EarlyReflections mEarly;
+
+    LateReverb mLate;
+
+    std::array<std::array<BandSplitter,NUM_LINES>,2> mAmbiSplitter;
+
+    size_t mFadeSampleCount{1};
+
+    void updateDelayLine(const float earlyDelay, const float lateDelay, const float density_mult,
+        const float decayTime, const float frequency);
+    void update3DPanning(const float *ReflectionsPan, const float *LateReverbPan,
+        const float earlyGain, const float lateGain, const bool doUpmix, const MixParams *mainMix);
+
+    void processEarly(size_t offset, const size_t samplesToDo,
+        const al::span<ReverbUpdateLine,NUM_LINES> tempSamples,
+        const al::span<FloatBufferLine,NUM_LINES> outSamples);
+    void processLate(size_t offset, const size_t samplesToDo,
+        const al::span<ReverbUpdateLine,NUM_LINES> tempSamples,
+        const al::span<FloatBufferLine,NUM_LINES> outSamples);
+
+    void clear() noexcept
+    {
+        for(auto &filter : mFilter)
+        {
+            filter.Lp.clear();
+            filter.Hp.clear();
+        }
+        mLate.clear();
+        for(auto &filters : mAmbiSplitter)
+        {
+            for(auto &filter : filters)
+                filter.clear();
+        }
+    }
+};
+
+struct ReverbState final : public EffectState {
+    /* All delay lines are allocated as a single buffer to reduce memory
+     * fragmentation and management code.
+     */
+    al::vector<std::array<float,NUM_LINES>,16> mSampleBuffer;
+
+    struct {
+        /* Calculated parameters which indicate if cross-fading is needed after
+         * an update.
+         */
+        float Density{1.0f};
+        float Diffusion{1.0f};
+        float DecayTime{1.49f};
+        float HFDecayTime{0.83f * 1.49f};
+        float LFDecayTime{1.0f * 1.49f};
+        float ModulationTime{0.25f};
+        float ModulationDepth{0.0f};
+        float HFReference{5000.0f};
+        float LFReference{250.0f};
+    } mParams;
+
+    enum PipelineState : uint8_t {
+        DeviceClear,
+        StartFade,
+        Fading,
+        Cleanup,
+        Normal,
+    };
+    PipelineState mPipelineState{DeviceClear};
+    uint8_t mCurrentPipeline{0};
+
+    ReverbPipeline mPipelines[2];
+
+    /* The current write offset for all delay lines. */
+    size_t mOffset{};
+
+    /* Temporary storage used when processing. */
+    union {
+        alignas(16) FloatBufferLine mTempLine{};
+        alignas(16) std::array<ReverbUpdateLine,NUM_LINES> mTempSamples;
+    };
+    alignas(16) std::array<FloatBufferLine,NUM_LINES> mEarlySamples{};
+    alignas(16) std::array<FloatBufferLine,NUM_LINES> mLateSamples{};
+
+    std::array<float,MaxAmbiOrder+1> mOrderScales{};
+
+    bool mUpmixOutput{false};
+
+
+    void MixOutPlain(ReverbPipeline &pipeline, const al::span<FloatBufferLine> samplesOut,
+        const size_t todo)
+    {
+        ASSUME(todo > 0);
+
+        /* When not upsampling, the panning gains convert to B-Format and pan
+         * at the same time.
+         */
+        for(size_t c{0u};c < NUM_LINES;c++)
+        {
+            const al::span<float> tmpspan{mEarlySamples[c].data(), todo};
+            MixSamples(tmpspan, samplesOut, pipeline.mEarly.CurrentGains[c],
+                pipeline.mEarly.TargetGains[c], todo, 0);
+        }
+        for(size_t c{0u};c < NUM_LINES;c++)
+        {
+            const al::span<float> tmpspan{mLateSamples[c].data(), todo};
+            MixSamples(tmpspan, samplesOut, pipeline.mLate.CurrentGains[c],
+                pipeline.mLate.TargetGains[c], todo, 0);
+        }
+    }
+
+    void MixOutAmbiUp(ReverbPipeline &pipeline, const al::span<FloatBufferLine> samplesOut,
+        const size_t todo)
+    {
+        ASSUME(todo > 0);
+
+        auto DoMixRow = [](const al::span<float> OutBuffer, const al::span<const float,4> Gains,
+            const float *InSamples, const size_t InStride)
+        {
+            std::fill(OutBuffer.begin(), OutBuffer.end(), 0.0f);
+            for(const float gain : Gains)
+            {
+                const float *RESTRICT input{al::assume_aligned<16>(InSamples)};
+                InSamples += InStride;
+
+                if(!(std::fabs(gain) > GainSilenceThreshold))
+                    continue;
+
+                auto mix_sample = [gain](const float sample, const float in) noexcept -> float
+                { return sample + in*gain; };
+                std::transform(OutBuffer.begin(), OutBuffer.end(), input, OutBuffer.begin(),
+                    mix_sample);
+            }
+        };
+
+        /* When upsampling, the B-Format conversion needs to be done separately
+         * so the proper HF scaling can be applied to each B-Format channel.
+         * The panning gains then pan and upsample the B-Format channels.
+         */
+        const al::span<float> tmpspan{al::assume_aligned<16>(mTempLine.data()), todo};
+        for(size_t c{0u};c < NUM_LINES;c++)
+        {
+            DoMixRow(tmpspan, EarlyA2B[c], mEarlySamples[0].data(), mEarlySamples[0].size());
+
+            /* Apply scaling to the B-Format's HF response to "upsample" it to
+             * higher-order output.
+             */
+            const float hfscale{(c==0) ? mOrderScales[0] : mOrderScales[1]};
+            pipeline.mAmbiSplitter[0][c].processHfScale(tmpspan, hfscale);
+
+            MixSamples(tmpspan, samplesOut, pipeline.mEarly.CurrentGains[c],
+                pipeline.mEarly.TargetGains[c], todo, 0);
+        }
+        for(size_t c{0u};c < NUM_LINES;c++)
+        {
+            DoMixRow(tmpspan, LateA2B[c], mLateSamples[0].data(), mLateSamples[0].size());
+
+            const float hfscale{(c==0) ? mOrderScales[0] : mOrderScales[1]};
+            pipeline.mAmbiSplitter[1][c].processHfScale(tmpspan, hfscale);
+
+            MixSamples(tmpspan, samplesOut, pipeline.mLate.CurrentGains[c],
+                pipeline.mLate.TargetGains[c], todo, 0);
+        }
+    }
+
+    void mixOut(ReverbPipeline &pipeline, const al::span<FloatBufferLine> samplesOut, const size_t todo)
+    {
+        if(mUpmixOutput)
+            MixOutAmbiUp(pipeline, samplesOut, todo);
+        else
+            MixOutPlain(pipeline, samplesOut, todo);
+    }
+
+    void allocLines(const float frequency);
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    DEF_NEWDEL(ReverbState)
+};
+
+/**************************************
+ *  Device Update                     *
+ **************************************/
+
+inline float CalcDelayLengthMult(float density)
+{ return maxf(5.0f, std::cbrt(density*DENSITY_SCALE)); }
+
+/* Calculates the delay line metrics and allocates the shared sample buffer
+ * for all lines given the sample rate (frequency).
+ */
+void ReverbState::allocLines(const float frequency)
+{
+    /* All delay line lengths are calculated to accomodate the full range of
+     * lengths given their respective paramters.
+     */
+    size_t totalSamples{0u};
+
+    /* Multiplier for the maximum density value, i.e. density=1, which is
+     * actually the least density...
+     */
+    const float multiplier{CalcDelayLengthMult(1.0f)};
+
+    /* The modulator's line length is calculated from the maximum modulation
+     * time and depth coefficient, and halfed for the low-to-high frequency
+     * swing.
+     */
+    constexpr float max_mod_delay{MaxModulationTime*MODULATION_DEPTH_COEFF / 2.0f};
+
+    for(auto &pipeline : mPipelines)
+    {
+        /* The main delay length includes the maximum early reflection delay,
+         * the largest early tap width, the maximum late reverb delay, and the
+         * largest late tap width.  Finally, it must also be extended by the
+         * update size (BufferLineSize) for block processing.
+         */
+        float length{ReverbMaxReflectionsDelay + EARLY_TAP_LENGTHS.back()*multiplier};
+        totalSamples += pipeline.mEarlyDelayIn.calcLineLength(length, totalSamples, frequency,
+            BufferLineSize);
+
+        constexpr float LateLineDiffAvg{(LATE_LINE_LENGTHS.back()-LATE_LINE_LENGTHS.front()) /
+            float{NUM_LINES}};
+        length = ReverbMaxLateReverbDelay + LateLineDiffAvg*multiplier;
+        totalSamples += pipeline.mLateDelayIn.calcLineLength(length, totalSamples, frequency,
+            BufferLineSize);
+
+        /* The early vector all-pass line. */
+        length = EARLY_ALLPASS_LENGTHS.back() * multiplier;
+        totalSamples += pipeline.mEarly.VecAp.Delay.calcLineLength(length, totalSamples, frequency,
+            0);
+
+        /* The early reflection line. */
+        length = EARLY_LINE_LENGTHS.back() * multiplier;
+        totalSamples += pipeline.mEarly.Delay.calcLineLength(length, totalSamples, frequency,
+            MAX_UPDATE_SAMPLES);
+
+        /* The late vector all-pass line. */
+        length = LATE_ALLPASS_LENGTHS.back() * multiplier;
+        totalSamples += pipeline.mLate.VecAp.Delay.calcLineLength(length, totalSamples, frequency,
+            0);
+
+        /* The late delay lines are calculated from the largest maximum density
+         * line length, and the maximum modulation delay. Four additional
+         * samples are needed for resampling the modulator delay.
+         */
+        length = LATE_LINE_LENGTHS.back()*multiplier + max_mod_delay;
+        totalSamples += pipeline.mLate.Delay.calcLineLength(length, totalSamples, frequency, 4);
+    }
+
+    if(totalSamples != mSampleBuffer.size())
+        decltype(mSampleBuffer)(totalSamples).swap(mSampleBuffer);
+
+    /* Clear the sample buffer. */
+    std::fill(mSampleBuffer.begin(), mSampleBuffer.end(), decltype(mSampleBuffer)::value_type{});
+
+    /* Update all delays to reflect the new sample buffer. */
+    for(auto &pipeline : mPipelines)
+    {
+        pipeline.mEarlyDelayIn.realizeLineOffset(mSampleBuffer.data());
+        pipeline.mLateDelayIn.realizeLineOffset(mSampleBuffer.data());
+        pipeline.mEarly.VecAp.Delay.realizeLineOffset(mSampleBuffer.data());
+        pipeline.mEarly.Delay.realizeLineOffset(mSampleBuffer.data());
+        pipeline.mLate.VecAp.Delay.realizeLineOffset(mSampleBuffer.data());
+        pipeline.mLate.Delay.realizeLineOffset(mSampleBuffer.data());
+    }
+}
+
+void ReverbState::deviceUpdate(const DeviceBase *device, const BufferStorage*)
+{
+    const auto frequency = static_cast<float>(device->Frequency);
+
+    /* Allocate the delay lines. */
+    allocLines(frequency);
+
+    for(auto &pipeline : mPipelines)
+    {
+        /* Clear filters and gain coefficients since the delay lines were all just
+        * cleared (if not reallocated).
+        */
+        for(auto &filter : pipeline.mFilter)
+        {
+            filter.Lp.clear();
+            filter.Hp.clear();
+        }
+
+        std::fill(std::begin(pipeline.mEarlyDelayCoeff),std::end(pipeline.mEarlyDelayCoeff), 0.0f);
+        std::fill(std::begin(pipeline.mEarlyDelayCoeff),std::end(pipeline.mEarlyDelayCoeff), 0.0f);
+
+        pipeline.mLate.DensityGain = 0.0f;
+        for(auto &t60 : pipeline.mLate.T60)
+        {
+            t60.MidGain = 0.0f;
+            t60.HFFilter.clear();
+            t60.LFFilter.clear();
+        }
+
+        pipeline.mLate.Mod.Index = 0;
+        pipeline.mLate.Mod.Step = 1;
+        pipeline.mLate.Mod.Depth = 0.0f;
+
+        for(auto &gains : pipeline.mEarly.CurrentGains)
+            std::fill(std::begin(gains), std::end(gains), 0.0f);
+        for(auto &gains : pipeline.mEarly.TargetGains)
+            std::fill(std::begin(gains), std::end(gains), 0.0f);
+        for(auto &gains : pipeline.mLate.CurrentGains)
+            std::fill(std::begin(gains), std::end(gains), 0.0f);
+        for(auto &gains : pipeline.mLate.TargetGains)
+            std::fill(std::begin(gains), std::end(gains), 0.0f);
+    }
+    mPipelineState = DeviceClear;
+
+    /* Reset offset base. */
+    mOffset = 0;
+
+    if(device->mAmbiOrder > 1)
+    {
+        mUpmixOutput = true;
+        mOrderScales = AmbiScale::GetHFOrderScales(1, device->mAmbiOrder, device->m2DMixing);
+    }
+    else
+    {
+        mUpmixOutput = false;
+        mOrderScales.fill(1.0f);
+    }
+    mPipelines[0].mAmbiSplitter[0][0].init(device->mXOverFreq / frequency);
+    for(auto &pipeline : mPipelines)
+    {
+        std::fill(pipeline.mAmbiSplitter[0].begin(), pipeline.mAmbiSplitter[0].end(),
+            pipeline.mAmbiSplitter[0][0]);
+        std::fill(pipeline.mAmbiSplitter[1].begin(), pipeline.mAmbiSplitter[1].end(),
+            pipeline.mAmbiSplitter[0][0]);
+    }
+}
+
+/**************************************
+ *  Effect Update                     *
+ **************************************/
+
+/* Calculate a decay coefficient given the length of each cycle and the time
+ * until the decay reaches -60 dB.
+ */
+inline float CalcDecayCoeff(const float length, const float decayTime)
+{ return std::pow(ReverbDecayGain, length/decayTime); }
+
+/* Calculate a decay length from a coefficient and the time until the decay
+ * reaches -60 dB.
+ */
+inline float CalcDecayLength(const float coeff, const float decayTime)
+{
+    constexpr float log10_decaygain{-3.0f/*std::log10(ReverbDecayGain)*/};
+    return std::log10(coeff) * decayTime / log10_decaygain;
+}
+
+/* Calculate an attenuation to be applied to the input of any echo models to
+ * compensate for modal density and decay time.
+ */
+inline float CalcDensityGain(const float a)
+{
+    /* The energy of a signal can be obtained by finding the area under the
+     * squared signal.  This takes the form of Sum(x_n^2), where x is the
+     * amplitude for the sample n.
+     *
+     * Decaying feedback matches exponential decay of the form Sum(a^n),
+     * where a is the attenuation coefficient, and n is the sample.  The area
+     * under this decay curve can be calculated as:  1 / (1 - a).
+     *
+     * Modifying the above equation to find the area under the squared curve
+     * (for energy) yields:  1 / (1 - a^2).  Input attenuation can then be
+     * calculated by inverting the square root of this approximation,
+     * yielding:  1 / sqrt(1 / (1 - a^2)), simplified to: sqrt(1 - a^2).
+     */
+    return std::sqrt(1.0f - a*a);
+}
+
+/* Calculate the scattering matrix coefficients given a diffusion factor. */
+inline void CalcMatrixCoeffs(const float diffusion, float *x, float *y)
+{
+    /* The matrix is of order 4, so n is sqrt(4 - 1). */
+    constexpr float n{al::numbers::sqrt3_v<float>};
+    const float t{diffusion * std::atan(n)};
+
+    /* Calculate the first mixing matrix coefficient. */
+    *x = std::cos(t);
+    /* Calculate the second mixing matrix coefficient. */
+    *y = std::sin(t) / n;
+}
+
+/* Calculate the limited HF ratio for use with the late reverb low-pass
+ * filters.
+ */
+float CalcLimitedHfRatio(const float hfRatio, const float airAbsorptionGainHF,
+    const float decayTime)
+{
+    /* Find the attenuation due to air absorption in dB (converting delay
+     * time to meters using the speed of sound).  Then reversing the decay
+     * equation, solve for HF ratio.  The delay length is cancelled out of
+     * the equation, so it can be calculated once for all lines.
+     */
+    float limitRatio{1.0f / SpeedOfSoundMetersPerSec /
+        CalcDecayLength(airAbsorptionGainHF, decayTime)};
+
+    /* Using the limit calculated above, apply the upper bound to the HF ratio. */
+    return minf(limitRatio, hfRatio);
+}
+
+
+/* Calculates the 3-band T60 damping coefficients for a particular delay line
+ * of specified length, using a combination of two shelf filter sections given
+ * decay times for each band split at two reference frequencies.
+ */
+void T60Filter::calcCoeffs(const float length, const float lfDecayTime,
+    const float mfDecayTime, const float hfDecayTime, const float lf0norm,
+    const float hf0norm)
+{
+    const float mfGain{CalcDecayCoeff(length, mfDecayTime)};
+    const float lfGain{CalcDecayCoeff(length, lfDecayTime) / mfGain};
+    const float hfGain{CalcDecayCoeff(length, hfDecayTime) / mfGain};
+
+    MidGain = mfGain;
+    LFFilter.setParamsFromSlope(BiquadType::LowShelf, lf0norm, lfGain, 1.0f);
+    HFFilter.setParamsFromSlope(BiquadType::HighShelf, hf0norm, hfGain, 1.0f);
+}
+
+/* Update the early reflection line lengths and gain coefficients. */
+void EarlyReflections::updateLines(const float density_mult, const float diffusion,
+    const float decayTime, const float frequency)
+{
+    /* Calculate the all-pass feed-back/forward coefficient. */
+    VecAp.Coeff = diffusion*diffusion * InvSqrt2;
+
+    for(size_t i{0u};i < NUM_LINES;i++)
+    {
+        /* Calculate the delay length of each all-pass line. */
+        float length{EARLY_ALLPASS_LENGTHS[i] * density_mult};
+        VecAp.Offset[i] = float2uint(length * frequency);
+
+        /* Calculate the delay length of each delay line. */
+        length = EARLY_LINE_LENGTHS[i] * density_mult;
+        Offset[i] = float2uint(length * frequency);
+
+        /* Calculate the gain (coefficient) for each line. */
+        Coeff[i] = CalcDecayCoeff(length, decayTime);
+    }
+}
+
+/* Update the EAX modulation step and depth. Keep in mind that this kind of
+ * vibrato is additive and not multiplicative as one may expect. The downswing
+ * will sound stronger than the upswing.
+ */
+void Modulation::updateModulator(float modTime, float modDepth, float frequency)
+{
+    /* Modulation is calculated in two parts.
+     *
+     * The modulation time effects the sinus rate, altering the speed of
+     * frequency changes. An index is incremented for each sample with an
+     * appropriate step size to generate an LFO, which will vary the feedback
+     * delay over time.
+     */
+    Step = maxu(fastf2u(MOD_FRACONE / (frequency * modTime)), 1);
+
+    /* The modulation depth effects the amount of frequency change over the
+     * range of the sinus. It needs to be scaled by the modulation time so that
+     * a given depth produces a consistent change in frequency over all ranges
+     * of time. Since the depth is applied to a sinus value, it needs to be
+     * halved once for the sinus range and again for the sinus swing in time
+     * (half of it is spent decreasing the frequency, half is spent increasing
+     * it).
+     */
+    if(modTime >= DefaultModulationTime)
+    {
+        /* To cancel the effects of a long period modulation on the late
+         * reverberation, the amount of pitch should be varied (decreased)
+         * according to the modulation time. The natural form is varying
+         * inversely, in fact resulting in an invariant.
+         */
+        Depth = MODULATION_DEPTH_COEFF / 4.0f * DefaultModulationTime * modDepth * frequency;
+    }
+    else
+        Depth = MODULATION_DEPTH_COEFF / 4.0f * modTime * modDepth * frequency;
+}
+
+/* Update the late reverb line lengths and T60 coefficients. */
+void LateReverb::updateLines(const float density_mult, const float diffusion,
+    const float lfDecayTime, const float mfDecayTime, const float hfDecayTime,
+    const float lf0norm, const float hf0norm, const float frequency)
+{
+    /* Scaling factor to convert the normalized reference frequencies from
+     * representing 0...freq to 0...max_reference.
+     */
+    constexpr float MaxHFReference{20000.0f};
+    const float norm_weight_factor{frequency / MaxHFReference};
+
+    const float late_allpass_avg{
+        std::accumulate(LATE_ALLPASS_LENGTHS.begin(), LATE_ALLPASS_LENGTHS.end(), 0.0f) /
+        float{NUM_LINES}};
+
+    /* To compensate for changes in modal density and decay time of the late
+     * reverb signal, the input is attenuated based on the maximal energy of
+     * the outgoing signal.  This approximation is used to keep the apparent
+     * energy of the signal equal for all ranges of density and decay time.
+     *
+     * The average length of the delay lines is used to calculate the
+     * attenuation coefficient.
+     */
+    float length{std::accumulate(LATE_LINE_LENGTHS.begin(), LATE_LINE_LENGTHS.end(), 0.0f) /
+        float{NUM_LINES} + late_allpass_avg};
+    length *= density_mult;
+    /* The density gain calculation uses an average decay time weighted by
+     * approximate bandwidth. This attempts to compensate for losses of energy
+     * that reduce decay time due to scattering into highly attenuated bands.
+     */
+    const float decayTimeWeighted{
+        lf0norm*norm_weight_factor*lfDecayTime +
+        (hf0norm - lf0norm)*norm_weight_factor*mfDecayTime +
+        (1.0f - hf0norm*norm_weight_factor)*hfDecayTime};
+    DensityGain = CalcDensityGain(CalcDecayCoeff(length, decayTimeWeighted));
+
+    /* Calculate the all-pass feed-back/forward coefficient. */
+    VecAp.Coeff = diffusion*diffusion * InvSqrt2;
+
+    for(size_t i{0u};i < NUM_LINES;i++)
+    {
+        /* Calculate the delay length of each all-pass line. */
+        length = LATE_ALLPASS_LENGTHS[i] * density_mult;
+        VecAp.Offset[i] = float2uint(length * frequency);
+
+        /* Calculate the delay length of each feedback delay line. A cubic
+         * resampler is used for modulation on the feedback delay, which
+         * includes one sample of delay. Reduce by one to compensate.
+         */
+        length = LATE_LINE_LENGTHS[i] * density_mult;
+        Offset[i] = maxu(float2uint(length*frequency + 0.5f), 1u) - 1u;
+
+        /* Approximate the absorption that the vector all-pass would exhibit
+         * given the current diffusion so we don't have to process a full T60
+         * filter for each of its four lines. Also include the average
+         * modulation delay (depth is half the max delay in samples).
+         */
+        length += lerpf(LATE_ALLPASS_LENGTHS[i], late_allpass_avg, diffusion)*density_mult +
+            Mod.Depth/frequency;
+
+        /* Calculate the T60 damping coefficients for each line. */
+        T60[i].calcCoeffs(length, lfDecayTime, mfDecayTime, hfDecayTime, lf0norm, hf0norm);
+    }
+}
+
+
+/* Update the offsets for the main effect delay line. */
+void ReverbPipeline::updateDelayLine(const float earlyDelay, const float lateDelay,
+    const float density_mult, const float decayTime, const float frequency)
+{
+    /* Early reflection taps are decorrelated by means of an average room
+     * reflection approximation described above the definition of the taps.
+     * This approximation is linear and so the above density multiplier can
+     * be applied to adjust the width of the taps.  A single-band decay
+     * coefficient is applied to simulate initial attenuation and absorption.
+     *
+     * Late reverb taps are based on the late line lengths to allow a zero-
+     * delay path and offsets that would continue the propagation naturally
+     * into the late lines.
+     */
+    for(size_t i{0u};i < NUM_LINES;i++)
+    {
+        float length{EARLY_TAP_LENGTHS[i]*density_mult};
+        mEarlyDelayTap[i][1] = float2uint((earlyDelay+length) * frequency);
+        mEarlyDelayCoeff[i] = CalcDecayCoeff(length, decayTime);
+
+        length = (LATE_LINE_LENGTHS[i] - LATE_LINE_LENGTHS.front())/float{NUM_LINES}*density_mult +
+            lateDelay;
+        mLateDelayTap[i][1] = float2uint(length * frequency);
+    }
+}
+
+/* Creates a transform matrix given a reverb vector. The vector pans the reverb
+ * reflections toward the given direction, using its magnitude (up to 1) as a
+ * focal strength. This function results in a B-Format transformation matrix
+ * that spatially focuses the signal in the desired direction.
+ */
+std::array<std::array<float,4>,4> GetTransformFromVector(const float *vec)
+{
+    /* Normalize the panning vector according to the N3D scale, which has an
+     * extra sqrt(3) term on the directional components. Converting from OpenAL
+     * to B-Format also requires negating X (ACN 1) and Z (ACN 3). Note however
+     * that the reverb panning vectors use left-handed coordinates, unlike the
+     * rest of OpenAL which use right-handed. This is fixed by negating Z,
+     * which cancels out with the B-Format Z negation.
+     */
+    float norm[3];
+    float mag{std::sqrt(vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2])};
+    if(mag > 1.0f)
+    {
+        norm[0] = vec[0] / mag * -al::numbers::sqrt3_v<float>;
+        norm[1] = vec[1] / mag * al::numbers::sqrt3_v<float>;
+        norm[2] = vec[2] / mag * al::numbers::sqrt3_v<float>;
+        mag = 1.0f;
+    }
+    else
+    {
+        /* If the magnitude is less than or equal to 1, just apply the sqrt(3)
+         * term. There's no need to renormalize the magnitude since it would
+         * just be reapplied in the matrix.
+         */
+        norm[0] = vec[0] * -al::numbers::sqrt3_v<float>;
+        norm[1] = vec[1] * al::numbers::sqrt3_v<float>;
+        norm[2] = vec[2] * al::numbers::sqrt3_v<float>;
+    }
+
+    return std::array<std::array<float,4>,4>{{
+        {{1.0f,   0.0f,    0.0f,   0.0f}},
+        {{norm[0], 1.0f-mag, 0.0f, 0.0f}},
+        {{norm[1], 0.0f, 1.0f-mag, 0.0f}},
+        {{norm[2], 0.0f, 0.0f, 1.0f-mag}}
+    }};
+}
+
+/* Update the early and late 3D panning gains. */
+void ReverbPipeline::update3DPanning(const float *ReflectionsPan, const float *LateReverbPan,
+    const float earlyGain, const float lateGain, const bool doUpmix, const MixParams *mainMix)
+{
+    /* Create matrices that transform a B-Format signal according to the
+     * panning vectors.
+     */
+    const std::array<std::array<float,4>,4> earlymat{GetTransformFromVector(ReflectionsPan)};
+    const std::array<std::array<float,4>,4> latemat{GetTransformFromVector(LateReverbPan)};
+
+    if(doUpmix)
+    {
+        /* When upsampling, combine the early and late transforms with the
+         * first-order upsample matrix. This results in panning gains that
+         * apply the panning transform to first-order B-Format, which is then
+         * upsampled.
+         */
+        auto mult_matrix = [](const al::span<const std::array<float,4>,4> mtx1)
+        {
+            auto&& mtx2 = AmbiScale::FirstOrderUp;
+            std::array<std::array<float,MaxAmbiChannels>,NUM_LINES> res{};
+
+            for(size_t i{0};i < mtx1[0].size();++i)
+            {
+                float *RESTRICT dst{res[i].data()};
+                for(size_t k{0};k < mtx1.size();++k)
+                {
+                    const float *RESTRICT src{mtx2[k].data()};
+                    const float a{mtx1[k][i]};
+                    for(size_t j{0};j < mtx2[0].size();++j)
+                        dst[j] += a * src[j];
+                }
+            }
+
+            return res;
+        };
+        auto earlycoeffs = mult_matrix(earlymat);
+        auto latecoeffs = mult_matrix(latemat);
+
+        for(size_t i{0u};i < NUM_LINES;i++)
+            ComputePanGains(mainMix, earlycoeffs[i].data(), earlyGain, mEarly.TargetGains[i]);
+        for(size_t i{0u};i < NUM_LINES;i++)
+            ComputePanGains(mainMix, latecoeffs[i].data(), lateGain, mLate.TargetGains[i]);
+    }
+    else
+    {
+        /* When not upsampling, combine the early and late A-to-B-Format
+         * conversions with their respective transform. This results panning
+         * gains that convert A-Format to B-Format, which is then panned.
+         */
+        auto mult_matrix = [](const al::span<const std::array<float,NUM_LINES>,4> mtx1,
+            const al::span<const std::array<float,4>,4> mtx2)
+        {
+            std::array<std::array<float,MaxAmbiChannels>,NUM_LINES> res{};
+
+            for(size_t i{0};i < mtx1[0].size();++i)
+            {
+                float *RESTRICT dst{res[i].data()};
+                for(size_t k{0};k < mtx1.size();++k)
+                {
+                    const float a{mtx1[k][i]};
+                    for(size_t j{0};j < mtx2.size();++j)
+                        dst[j] += a * mtx2[j][k];
+                }
+            }
+
+            return res;
+        };
+        auto earlycoeffs = mult_matrix(EarlyA2B, earlymat);
+        auto latecoeffs = mult_matrix(LateA2B, latemat);
+
+        for(size_t i{0u};i < NUM_LINES;i++)
+            ComputePanGains(mainMix, earlycoeffs[i].data(), earlyGain, mEarly.TargetGains[i]);
+        for(size_t i{0u};i < NUM_LINES;i++)
+            ComputePanGains(mainMix, latecoeffs[i].data(), lateGain, mLate.TargetGains[i]);
+    }
+}
+
+void ReverbState::update(const ContextBase *Context, const EffectSlot *Slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *Device{Context->mDevice};
+    const auto frequency = static_cast<float>(Device->Frequency);
+
+    /* If the HF limit parameter is flagged, calculate an appropriate limit
+     * based on the air absorption parameter.
+     */
+    float hfRatio{props->Reverb.DecayHFRatio};
+    if(props->Reverb.DecayHFLimit && props->Reverb.AirAbsorptionGainHF < 1.0f)
+        hfRatio = CalcLimitedHfRatio(hfRatio, props->Reverb.AirAbsorptionGainHF,
+            props->Reverb.DecayTime);
+
+    /* Calculate the LF/HF decay times. */
+    constexpr float MinDecayTime{0.1f}, MaxDecayTime{20.0f};
+    const float lfDecayTime{clampf(props->Reverb.DecayTime*props->Reverb.DecayLFRatio,
+        MinDecayTime, MaxDecayTime)};
+    const float hfDecayTime{clampf(props->Reverb.DecayTime*hfRatio, MinDecayTime, MaxDecayTime)};
+
+    /* Determine if a full update is required. */
+    const bool fullUpdate{mPipelineState == DeviceClear ||
+        /* Density is essentially a master control for the feedback delays, so
+         * changes the offsets of many delay lines.
+         */
+        mParams.Density != props->Reverb.Density ||
+        /* Diffusion and decay times influences the decay rate (gain) of the
+         * late reverb T60 filter.
+         */
+        mParams.Diffusion != props->Reverb.Diffusion ||
+        mParams.DecayTime != props->Reverb.DecayTime ||
+        mParams.HFDecayTime != hfDecayTime ||
+        mParams.LFDecayTime != lfDecayTime ||
+        /* Modulation time and depth both require fading the modulation delay. */
+        mParams.ModulationTime != props->Reverb.ModulationTime ||
+        mParams.ModulationDepth != props->Reverb.ModulationDepth ||
+        /* HF/LF References control the weighting used to calculate the density
+         * gain.
+         */
+        mParams.HFReference != props->Reverb.HFReference ||
+        mParams.LFReference != props->Reverb.LFReference};
+    if(fullUpdate)
+    {
+        mParams.Density = props->Reverb.Density;
+        mParams.Diffusion = props->Reverb.Diffusion;
+        mParams.DecayTime = props->Reverb.DecayTime;
+        mParams.HFDecayTime = hfDecayTime;
+        mParams.LFDecayTime = lfDecayTime;
+        mParams.ModulationTime = props->Reverb.ModulationTime;
+        mParams.ModulationDepth = props->Reverb.ModulationDepth;
+        mParams.HFReference = props->Reverb.HFReference;
+        mParams.LFReference = props->Reverb.LFReference;
+
+        mPipelineState = (mPipelineState != DeviceClear) ? StartFade : Normal;
+        mCurrentPipeline ^= 1;
+    }
+    auto &pipeline = mPipelines[mCurrentPipeline];
+
+    /* Update early and late 3D panning. */
+    mOutTarget = target.Main->Buffer;
+    const float gain{props->Reverb.Gain * Slot->Gain * ReverbBoost};
+    pipeline.update3DPanning(props->Reverb.ReflectionsPan, props->Reverb.LateReverbPan,
+        props->Reverb.ReflectionsGain*gain, props->Reverb.LateReverbGain*gain, mUpmixOutput,
+        target.Main);
+
+    /* Calculate the master filters */
+    float hf0norm{minf(props->Reverb.HFReference/frequency, 0.49f)};
+    pipeline.mFilter[0].Lp.setParamsFromSlope(BiquadType::HighShelf, hf0norm, props->Reverb.GainHF, 1.0f);
+    float lf0norm{minf(props->Reverb.LFReference/frequency, 0.49f)};
+    pipeline.mFilter[0].Hp.setParamsFromSlope(BiquadType::LowShelf, lf0norm, props->Reverb.GainLF, 1.0f);
+    for(size_t i{1u};i < NUM_LINES;i++)
+    {
+        pipeline.mFilter[i].Lp.copyParamsFrom(pipeline.mFilter[0].Lp);
+        pipeline.mFilter[i].Hp.copyParamsFrom(pipeline.mFilter[0].Hp);
+    }
+
+    /* The density-based room size (delay length) multiplier. */
+    const float density_mult{CalcDelayLengthMult(props->Reverb.Density)};
+
+    /* Update the main effect delay and associated taps. */
+    pipeline.updateDelayLine(props->Reverb.ReflectionsDelay, props->Reverb.LateReverbDelay,
+        density_mult, props->Reverb.DecayTime, frequency);
+
+    if(fullUpdate)
+    {
+        /* Update the early lines. */
+        pipeline.mEarly.updateLines(density_mult, props->Reverb.Diffusion, props->Reverb.DecayTime,
+            frequency);
+
+        /* Get the mixing matrix coefficients. */
+        CalcMatrixCoeffs(props->Reverb.Diffusion, &pipeline.mMixX, &pipeline.mMixY);
+
+        /* Update the modulator rate and depth. */
+        pipeline.mLate.Mod.updateModulator(props->Reverb.ModulationTime,
+            props->Reverb.ModulationDepth, frequency);
+
+        /* Update the late lines. */
+        pipeline.mLate.updateLines(density_mult, props->Reverb.Diffusion, lfDecayTime,
+            props->Reverb.DecayTime, hfDecayTime, lf0norm, hf0norm, frequency);
+    }
+
+    const float decaySamples{(props->Reverb.ReflectionsDelay + props->Reverb.LateReverbDelay
+        + props->Reverb.DecayTime) * frequency};
+    pipeline.mFadeSampleCount = static_cast<size_t>(minf(decaySamples, 1'000'000.0f));
+}
+
+
+/**************************************
+ *  Effect Processing                 *
+ **************************************/
+
+/* Applies a scattering matrix to the 4-line (vector) input.  This is used
+ * for both the below vector all-pass model and to perform modal feed-back
+ * delay network (FDN) mixing.
+ *
+ * The matrix is derived from a skew-symmetric matrix to form a 4D rotation
+ * matrix with a single unitary rotational parameter:
+ *
+ *     [  d,  a,  b,  c ]          1 = a^2 + b^2 + c^2 + d^2
+ *     [ -a,  d,  c, -b ]
+ *     [ -b, -c,  d,  a ]
+ *     [ -c,  b, -a,  d ]
+ *
+ * The rotation is constructed from the effect's diffusion parameter,
+ * yielding:
+ *
+ *     1 = x^2 + 3 y^2
+ *
+ * Where a, b, and c are the coefficient y with differing signs, and d is the
+ * coefficient x.  The final matrix is thus:
+ *
+ *     [  x,  y, -y,  y ]          n = sqrt(matrix_order - 1)
+ *     [ -y,  x,  y,  y ]          t = diffusion_parameter * atan(n)
+ *     [  y, -y,  x,  y ]          x = cos(t)
+ *     [ -y, -y, -y,  x ]          y = sin(t) / n
+ *
+ * Any square orthogonal matrix with an order that is a power of two will
+ * work (where ^T is transpose, ^-1 is inverse):
+ *
+ *     M^T = M^-1
+ *
+ * Using that knowledge, finding an appropriate matrix can be accomplished
+ * naively by searching all combinations of:
+ *
+ *     M = D + S - S^T
+ *
+ * Where D is a diagonal matrix (of x), and S is a triangular matrix (of y)
+ * whose combination of signs are being iterated.
+ */
+inline auto VectorPartialScatter(const std::array<float,NUM_LINES> &RESTRICT in,
+    const float xCoeff, const float yCoeff) -> std::array<float,NUM_LINES>
+{
+    return std::array<float,NUM_LINES>{{
+        xCoeff*in[0] + yCoeff*(          in[1] + -in[2] + in[3]),
+        xCoeff*in[1] + yCoeff*(-in[0]          +  in[2] + in[3]),
+        xCoeff*in[2] + yCoeff*( in[0] + -in[1]          + in[3]),
+        xCoeff*in[3] + yCoeff*(-in[0] + -in[1] + -in[2]        )
+    }};
+}
+
+/* Utilizes the above, but reverses the input channels. */
+void VectorScatterRevDelayIn(const DelayLineI delay, size_t offset, const float xCoeff,
+    const float yCoeff, const al::span<const ReverbUpdateLine,NUM_LINES> in, const size_t count)
+{
+    ASSUME(count > 0);
+
+    for(size_t i{0u};i < count;)
+    {
+        offset &= delay.Mask;
+        size_t td{minz(delay.Mask+1 - offset, count-i)};
+        do {
+            std::array<float,NUM_LINES> f;
+            for(size_t j{0u};j < NUM_LINES;j++)
+                f[NUM_LINES-1-j] = in[j][i];
+            ++i;
+
+            delay.Line[offset++] = VectorPartialScatter(f, xCoeff, yCoeff);
+        } while(--td);
+    }
+}
+
+/* This applies a Gerzon multiple-in/multiple-out (MIMO) vector all-pass
+ * filter to the 4-line input.
+ *
+ * It works by vectorizing a regular all-pass filter and replacing the delay
+ * element with a scattering matrix (like the one above) and a diagonal
+ * matrix of delay elements.
+ *
+ * Two static specializations are used for transitional (cross-faded) delay
+ * line processing and non-transitional processing.
+ */
+void VecAllpass::process(const al::span<ReverbUpdateLine,NUM_LINES> samples, size_t offset,
+    const float xCoeff, const float yCoeff, const size_t todo)
+{
+    const DelayLineI delay{Delay};
+    const float feedCoeff{Coeff};
+
+    ASSUME(todo > 0);
+
+    size_t vap_offset[NUM_LINES];
+    for(size_t j{0u};j < NUM_LINES;j++)
+        vap_offset[j] = offset - Offset[j];
+    for(size_t i{0u};i < todo;)
+    {
+        for(size_t j{0u};j < NUM_LINES;j++)
+            vap_offset[j] &= delay.Mask;
+        offset &= delay.Mask;
+
+        size_t maxoff{offset};
+        for(size_t j{0u};j < NUM_LINES;j++)
+            maxoff = maxz(maxoff, vap_offset[j]);
+        size_t td{minz(delay.Mask+1 - maxoff, todo - i)};
+
+        do {
+            std::array<float,NUM_LINES> f;
+            for(size_t j{0u};j < NUM_LINES;j++)
+            {
+                const float input{samples[j][i]};
+                const float out{delay.Line[vap_offset[j]++][j] - feedCoeff*input};
+                f[j] = input + feedCoeff*out;
+
+                samples[j][i] = out;
+            }
+            ++i;
+
+            delay.Line[offset++] = VectorPartialScatter(f, xCoeff, yCoeff);
+        } while(--td);
+    }
+}
+
+/* This generates early reflections.
+ *
+ * This is done by obtaining the primary reflections (those arriving from the
+ * same direction as the source) from the main delay line.  These are
+ * attenuated and all-pass filtered (based on the diffusion parameter).
+ *
+ * The early lines are then fed in reverse (according to the approximately
+ * opposite spatial location of the A-Format lines) to create the secondary
+ * reflections (those arriving from the opposite direction as the source).
+ *
+ * The early response is then completed by combining the primary reflections
+ * with the delayed and attenuated output from the early lines.
+ *
+ * Finally, the early response is reversed, scattered (based on diffusion),
+ * and fed into the late reverb section of the main delay line.
+ */
+void ReverbPipeline::processEarly(size_t offset, const size_t samplesToDo,
+    const al::span<ReverbUpdateLine, NUM_LINES> tempSamples,
+    const al::span<FloatBufferLine, NUM_LINES> outSamples)
+{
+    const DelayLineI early_delay{mEarly.Delay};
+    const DelayLineI in_delay{mEarlyDelayIn};
+    const float mixX{mMixX};
+    const float mixY{mMixY};
+
+    ASSUME(samplesToDo > 0);
+
+    for(size_t base{0};base < samplesToDo;)
+    {
+        const size_t todo{minz(samplesToDo-base, MAX_UPDATE_SAMPLES)};
+
+        /* First, load decorrelated samples from the main delay line as the
+         * primary reflections.
+         */
+        const float fadeStep{1.0f / static_cast<float>(todo)};
+        for(size_t j{0u};j < NUM_LINES;j++)
+        {
+            size_t early_delay_tap0{offset - mEarlyDelayTap[j][0]};
+            size_t early_delay_tap1{offset - mEarlyDelayTap[j][1]};
+            const float coeff{mEarlyDelayCoeff[j]};
+            const float coeffStep{early_delay_tap0 != early_delay_tap1 ? coeff*fadeStep : 0.0f};
+            float fadeCount{0.0f};
+
+            for(size_t i{0u};i < todo;)
+            {
+                early_delay_tap0 &= in_delay.Mask;
+                early_delay_tap1 &= in_delay.Mask;
+                const size_t max_tap{maxz(early_delay_tap0, early_delay_tap1)};
+                size_t td{minz(in_delay.Mask+1 - max_tap, todo-i)};
+                do {
+                    const float fade0{coeff - coeffStep*fadeCount};
+                    const float fade1{coeffStep*fadeCount};
+                    fadeCount += 1.0f;
+                    tempSamples[j][i++] = in_delay.Line[early_delay_tap0++][j]*fade0 +
+                        in_delay.Line[early_delay_tap1++][j]*fade1;
+                } while(--td);
+            }
+
+            mEarlyDelayTap[j][0] = mEarlyDelayTap[j][1];
+        }
+
+        /* Apply a vector all-pass, to help color the initial reflections based
+         * on the diffusion strength.
+         */
+        mEarly.VecAp.process(tempSamples, offset, mixX, mixY, todo);
+
+        /* Apply a delay and bounce to generate secondary reflections, combine
+         * with the primary reflections and write out the result for mixing.
+         */
+        for(size_t j{0u};j < NUM_LINES;j++)
+            early_delay.write(offset, NUM_LINES-1-j, tempSamples[j].data(), todo);
+        for(size_t j{0u};j < NUM_LINES;j++)
+        {
+            size_t feedb_tap{offset - mEarly.Offset[j]};
+            const float feedb_coeff{mEarly.Coeff[j]};
+            float *RESTRICT out{al::assume_aligned<16>(outSamples[j].data() + base)};
+
+            for(size_t i{0u};i < todo;)
+            {
+                feedb_tap &= early_delay.Mask;
+                size_t td{minz(early_delay.Mask+1 - feedb_tap, todo - i)};
+                do {
+                    tempSamples[j][i] += early_delay.Line[feedb_tap++][j]*feedb_coeff;
+                    out[i] = tempSamples[j][i];
+                    ++i;
+                } while(--td);
+            }
+        }
+
+        /* Finally, write the result to the late delay line input for the late
+         * reverb stage to pick up at the appropriate time, applying a scatter
+         * and bounce to improve the initial diffusion in the late reverb.
+         */
+        VectorScatterRevDelayIn(mLateDelayIn, offset, mixX, mixY, tempSamples, todo);
+
+        base += todo;
+        offset += todo;
+    }
+}
+
+void Modulation::calcDelays(size_t todo)
+{
+    constexpr float mod_scale{al::numbers::pi_v<float> * 2.0f / MOD_FRACONE};
+    uint idx{Index};
+    const uint step{Step};
+    const float depth{Depth};
+    for(size_t i{0};i < todo;++i)
+    {
+        idx += step;
+        const float lfo{std::sin(static_cast<float>(idx&MOD_FRACMASK) * mod_scale)};
+        ModDelays[i] = (lfo+1.0f) * depth;
+    }
+    Index = idx;
+}
+
+
+/* This generates the reverb tail using a modified feed-back delay network
+ * (FDN).
+ *
+ * Results from the early reflections are mixed with the output from the
+ * modulated late delay lines.
+ *
+ * The late response is then completed by T60 and all-pass filtering the mix.
+ *
+ * Finally, the lines are reversed (so they feed their opposite directions)
+ * and scattered with the FDN matrix before re-feeding the delay lines.
+ */
+void ReverbPipeline::processLate(size_t offset, const size_t samplesToDo,
+    const al::span<ReverbUpdateLine, NUM_LINES> tempSamples,
+    const al::span<FloatBufferLine, NUM_LINES> outSamples)
+{
+    const DelayLineI late_delay{mLate.Delay};
+    const DelayLineI in_delay{mLateDelayIn};
+    const float mixX{mMixX};
+    const float mixY{mMixY};
+
+    ASSUME(samplesToDo > 0);
+
+    for(size_t base{0};base < samplesToDo;)
+    {
+        const size_t todo{minz(samplesToDo-base, minz(mLate.Offset[0], MAX_UPDATE_SAMPLES))};
+        ASSUME(todo > 0);
+
+        /* First, calculate the modulated delays for the late feedback. */
+        mLate.Mod.calcDelays(todo);
+
+        /* Next, load decorrelated samples from the main and feedback delay
+         * lines. Filter the signal to apply its frequency-dependent decay.
+         */
+        const float fadeStep{1.0f / static_cast<float>(todo)};
+        for(size_t j{0u};j < NUM_LINES;j++)
+        {
+            size_t late_delay_tap0{offset - mLateDelayTap[j][0]};
+            size_t late_delay_tap1{offset - mLateDelayTap[j][1]};
+            size_t late_feedb_tap{offset - mLate.Offset[j]};
+            const float midGain{mLate.T60[j].MidGain};
+            const float densityGain{mLate.DensityGain * midGain};
+            const float densityStep{late_delay_tap0 != late_delay_tap1 ?
+                densityGain*fadeStep : 0.0f};
+            float fadeCount{0.0f};
+
+            for(size_t i{0u};i < todo;)
+            {
+                late_delay_tap0 &= in_delay.Mask;
+                late_delay_tap1 &= in_delay.Mask;
+                size_t td{minz(todo-i, in_delay.Mask+1 - maxz(late_delay_tap0, late_delay_tap1))};
+                do {
+                    /* Calculate the read offset and offset between it and the
+                     * next sample.
+                     */
+                    const float fdelay{mLate.Mod.ModDelays[i]};
+                    const size_t idelay{float2uint(fdelay * float{gCubicTable.sTableSteps})};
+                    const size_t delay{late_feedb_tap - (idelay>>gCubicTable.sTableBits)};
+                    const size_t delayoffset{idelay & gCubicTable.sTableMask};
+                    ++late_feedb_tap;
+
+                    /* Get the samples around by the delayed offset. */
+                    const float out0{late_delay.Line[(delay  ) & late_delay.Mask][j]};
+                    const float out1{late_delay.Line[(delay-1) & late_delay.Mask][j]};
+                    const float out2{late_delay.Line[(delay-2) & late_delay.Mask][j]};
+                    const float out3{late_delay.Line[(delay-3) & late_delay.Mask][j]};
+
+                    /* The output is obtained by interpolating the four samples
+                     * that were acquired above, and combined with the main
+                     * delay tap.
+                     */
+                    const float out{out0*gCubicTable.getCoeff0(delayoffset)
+                        + out1*gCubicTable.getCoeff1(delayoffset)
+                        + out2*gCubicTable.getCoeff2(delayoffset)
+                        + out3*gCubicTable.getCoeff3(delayoffset)};
+                    const float fade0{densityGain - densityStep*fadeCount};
+                    const float fade1{densityStep*fadeCount};
+                    fadeCount += 1.0f;
+                    tempSamples[j][i] = out*midGain +
+                        in_delay.Line[late_delay_tap0++][j]*fade0 +
+                        in_delay.Line[late_delay_tap1++][j]*fade1;
+                    ++i;
+                } while(--td);
+            }
+            mLateDelayTap[j][0] = mLateDelayTap[j][1];
+
+            mLate.T60[j].process({tempSamples[j].data(), todo});
+        }
+
+        /* Apply a vector all-pass to improve micro-surface diffusion, and
+         * write out the results for mixing.
+         */
+        mLate.VecAp.process(tempSamples, offset, mixX, mixY, todo);
+        for(size_t j{0u};j < NUM_LINES;j++)
+            std::copy_n(tempSamples[j].begin(), todo, outSamples[j].begin()+base);
+
+        /* Finally, scatter and bounce the results to refeed the feedback buffer. */
+        VectorScatterRevDelayIn(late_delay, offset, mixX, mixY, tempSamples, todo);
+
+        base += todo;
+        offset += todo;
+    }
+}
+
+void ReverbState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    const size_t offset{mOffset};
+
+    ASSUME(samplesToDo > 0);
+
+    auto &oldpipeline = mPipelines[mCurrentPipeline^1];
+    auto &pipeline = mPipelines[mCurrentPipeline];
+
+    if(mPipelineState >= Fading)
+    {
+        /* Convert B-Format to A-Format for processing. */
+        const size_t numInput{minz(samplesIn.size(), NUM_LINES)};
+        const al::span<float> tmpspan{al::assume_aligned<16>(mTempLine.data()), samplesToDo};
+        for(size_t c{0u};c < NUM_LINES;c++)
+        {
+            std::fill(tmpspan.begin(), tmpspan.end(), 0.0f);
+            for(size_t i{0};i < numInput;++i)
+            {
+                const float gain{B2A[c][i]};
+                const float *RESTRICT input{al::assume_aligned<16>(samplesIn[i].data())};
+
+                auto mix_sample = [gain](const float sample, const float in) noexcept -> float
+                { return sample + in*gain; };
+                std::transform(tmpspan.begin(), tmpspan.end(), input, tmpspan.begin(),
+                    mix_sample);
+            }
+
+            /* Band-pass the incoming samples and feed the initial delay line. */
+            auto&& filter = DualBiquad{pipeline.mFilter[c].Lp, pipeline.mFilter[c].Hp};
+            filter.process(tmpspan, tmpspan.data());
+            pipeline.mEarlyDelayIn.write(offset, c, tmpspan.cbegin(), samplesToDo);
+        }
+        if(mPipelineState == Fading)
+        {
+            /* Give the old pipeline silence if it's still fading out. */
+            for(size_t c{0u};c < NUM_LINES;c++)
+            {
+                std::fill(tmpspan.begin(), tmpspan.end(), 0.0f);
+
+                auto&& filter = DualBiquad{oldpipeline.mFilter[c].Lp, oldpipeline.mFilter[c].Hp};
+                filter.process(tmpspan, tmpspan.data());
+                oldpipeline.mEarlyDelayIn.write(offset, c, tmpspan.cbegin(), samplesToDo);
+            }
+        }
+    }
+    else
+    {
+        /* At the start of a fade, fade in input for the current pipeline, and
+         * fade out input for the old pipeline.
+         */
+        const size_t numInput{minz(samplesIn.size(), NUM_LINES)};
+        const al::span<float> tmpspan{al::assume_aligned<16>(mTempLine.data()), samplesToDo};
+        const float fadeStep{1.0f / static_cast<float>(samplesToDo)};
+
+        for(size_t c{0u};c < NUM_LINES;c++)
+        {
+            std::fill(tmpspan.begin(), tmpspan.end(), 0.0f);
+            for(size_t i{0};i < numInput;++i)
+            {
+                const float gain{B2A[c][i]};
+                const float *RESTRICT input{al::assume_aligned<16>(samplesIn[i].data())};
+
+                auto mix_sample = [gain](const float sample, const float in) noexcept -> float
+                { return sample + in*gain; };
+                std::transform(tmpspan.begin(), tmpspan.end(), input, tmpspan.begin(),
+                    mix_sample);
+            }
+            float stepCount{0.0f};
+            for(float &sample : tmpspan)
+            {
+                stepCount += 1.0f;
+                sample *= stepCount*fadeStep;
+            }
+
+            auto&& filter = DualBiquad{pipeline.mFilter[c].Lp, pipeline.mFilter[c].Hp};
+            filter.process(tmpspan, tmpspan.data());
+            pipeline.mEarlyDelayIn.write(offset, c, tmpspan.cbegin(), samplesToDo);
+        }
+        for(size_t c{0u};c < NUM_LINES;c++)
+        {
+            std::fill(tmpspan.begin(), tmpspan.end(), 0.0f);
+            for(size_t i{0};i < numInput;++i)
+            {
+                const float gain{B2A[c][i]};
+                const float *RESTRICT input{al::assume_aligned<16>(samplesIn[i].data())};
+
+                auto mix_sample = [gain](const float sample, const float in) noexcept -> float
+                { return sample + in*gain; };
+                std::transform(tmpspan.begin(), tmpspan.end(), input, tmpspan.begin(),
+                    mix_sample);
+            }
+            float stepCount{0.0f};
+            for(float &sample : tmpspan)
+            {
+                stepCount += 1.0f;
+                sample *= 1.0f - stepCount*fadeStep;
+            }
+
+            auto&& filter = DualBiquad{oldpipeline.mFilter[c].Lp, oldpipeline.mFilter[c].Hp};
+            filter.process(tmpspan, tmpspan.data());
+            oldpipeline.mEarlyDelayIn.write(offset, c, tmpspan.cbegin(), samplesToDo);
+        }
+        mPipelineState = Fading;
+    }
+
+    /* Process reverb for these samples. and mix them to the output. */
+    pipeline.processEarly(offset, samplesToDo, mTempSamples, mEarlySamples);
+    pipeline.processLate(offset, samplesToDo, mTempSamples, mLateSamples);
+    mixOut(pipeline, samplesOut, samplesToDo);
+
+    if(mPipelineState != Normal)
+    {
+        if(mPipelineState == Cleanup)
+        {
+            size_t numSamples{mSampleBuffer.size()/2};
+            size_t pipelineOffset{numSamples * (mCurrentPipeline^1)};
+            std::fill_n(mSampleBuffer.data()+pipelineOffset, numSamples,
+                decltype(mSampleBuffer)::value_type{});
+
+            oldpipeline.clear();
+            mPipelineState = Normal;
+        }
+        else
+        {
+            /* If this is the final mix for this old pipeline, set the target
+             * gains to 0 to ensure a complete fade out, and set the state to
+             * Cleanup so the next invocation cleans up the delay buffers and
+             * filters.
+             */
+            if(samplesToDo >= oldpipeline.mFadeSampleCount)
+            {
+                for(auto &gains : oldpipeline.mEarly.TargetGains)
+                    std::fill(std::begin(gains), std::end(gains), 0.0f);
+                for(auto &gains : oldpipeline.mLate.TargetGains)
+                    std::fill(std::begin(gains), std::end(gains), 0.0f);
+                oldpipeline.mFadeSampleCount = 0;
+                mPipelineState = Cleanup;
+            }
+            else
+                oldpipeline.mFadeSampleCount -= samplesToDo;
+
+            /* Process the old reverb for these samples. */
+            oldpipeline.processEarly(offset, samplesToDo, mTempSamples, mEarlySamples);
+            oldpipeline.processLate(offset, samplesToDo, mTempSamples, mLateSamples);
+            mixOut(oldpipeline, samplesOut, samplesToDo);
+        }
+    }
+
+    mOffset = offset + samplesToDo;
+}
+
+
+struct ReverbStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new ReverbState{}}; }
+};
+
+struct StdReverbStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new ReverbState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *ReverbStateFactory_getFactory()
+{
+    static ReverbStateFactory ReverbFactory{};
+    return &ReverbFactory;
+}
+
+EffectStateFactory *StdReverbStateFactory_getFactory()
+{
+    static StdReverbStateFactory ReverbFactory{};
+    return &ReverbFactory;
+}
diff --git a/alc/effects/vmorpher.cpp b/alc/effects/vmorpher.cpp
new file mode 100644 (file)
index 0000000..872c7ad
--- /dev/null
@@ -0,0 +1,350 @@
+/**
+ * This file is part of the OpenAL Soft cross platform audio library
+ *
+ * Copyright (C) 2019 by Anis A. Hireche
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Spherical-Harmonic-Transform nor the names of its
+ *   contributors may be used to endorse or promote products derived from
+ *   this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#include <algorithm>
+#include <array>
+#include <cstdlib>
+#include <functional>
+#include <iterator>
+
+#include "alc/effects/base.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/context.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/effectslot.h"
+#include "core/mixer.h"
+#include "intrusive_ptr.h"
+
+
+namespace {
+
+using uint = unsigned int;
+
+#define MAX_UPDATE_SAMPLES 256
+#define NUM_FORMANTS       4
+#define NUM_FILTERS        2
+#define Q_FACTOR           5.0f
+
+#define VOWEL_A_INDEX      0
+#define VOWEL_B_INDEX      1
+
+#define WAVEFORM_FRACBITS  24
+#define WAVEFORM_FRACONE   (1<<WAVEFORM_FRACBITS)
+#define WAVEFORM_FRACMASK  (WAVEFORM_FRACONE-1)
+
+inline float Sin(uint index)
+{
+    constexpr float scale{al::numbers::pi_v<float>*2.0f / WAVEFORM_FRACONE};
+    return std::sin(static_cast<float>(index) * scale)*0.5f + 0.5f;
+}
+
+inline float Saw(uint index)
+{ return static_cast<float>(index) / float{WAVEFORM_FRACONE}; }
+
+inline float Triangle(uint index)
+{ return std::fabs(static_cast<float>(index)*(2.0f/WAVEFORM_FRACONE) - 1.0f); }
+
+inline float Half(uint) { return 0.5f; }
+
+template<float (&func)(uint)>
+void Oscillate(float *RESTRICT dst, uint index, const uint step, size_t todo)
+{
+    for(size_t i{0u};i < todo;i++)
+    {
+        index += step;
+        index &= WAVEFORM_FRACMASK;
+        dst[i] = func(index);
+    }
+}
+
+struct FormantFilter
+{
+    float mCoeff{0.0f};
+    float mGain{1.0f};
+    float mS1{0.0f};
+    float mS2{0.0f};
+
+    FormantFilter() = default;
+    FormantFilter(float f0norm, float gain)
+      : mCoeff{std::tan(al::numbers::pi_v<float> * f0norm)}, mGain{gain}
+    { }
+
+    inline void process(const float *samplesIn, float *samplesOut, const size_t numInput)
+    {
+        /* A state variable filter from a topology-preserving transform.
+         * Based on a talk given by Ivan Cohen: https://www.youtube.com/watch?v=esjHXGPyrhg
+         */
+        const float g{mCoeff};
+        const float gain{mGain};
+        const float h{1.0f / (1.0f + (g/Q_FACTOR) + (g*g))};
+        float s1{mS1};
+        float s2{mS2};
+
+        for(size_t i{0u};i < numInput;i++)
+        {
+            const float H{(samplesIn[i] - (1.0f/Q_FACTOR + g)*s1 - s2)*h};
+            const float B{g*H + s1};
+            const float L{g*B + s2};
+
+            s1 = g*H + B;
+            s2 = g*B + L;
+
+            // Apply peak and accumulate samples.
+            samplesOut[i] += B * gain;
+        }
+        mS1 = s1;
+        mS2 = s2;
+    }
+
+    inline void clear()
+    {
+        mS1 = 0.0f;
+        mS2 = 0.0f;
+    }
+};
+
+
+struct VmorpherState final : public EffectState {
+    struct {
+        uint mTargetChannel{InvalidChannelIndex};
+
+        /* Effect parameters */
+        FormantFilter mFormants[NUM_FILTERS][NUM_FORMANTS];
+
+        /* Effect gains for each channel */
+        float mCurrentGain{};
+        float mTargetGain{};
+    } mChans[MaxAmbiChannels];
+
+    void (*mGetSamples)(float*RESTRICT, uint, const uint, size_t){};
+
+    uint mIndex{0};
+    uint mStep{1};
+
+    /* Effects buffers */
+    alignas(16) float mSampleBufferA[MAX_UPDATE_SAMPLES]{};
+    alignas(16) float mSampleBufferB[MAX_UPDATE_SAMPLES]{};
+    alignas(16) float mLfo[MAX_UPDATE_SAMPLES]{};
+
+    void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) override;
+    void update(const ContextBase *context, const EffectSlot *slot, const EffectProps *props,
+        const EffectTarget target) override;
+    void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) override;
+
+    static std::array<FormantFilter,4> getFiltersByPhoneme(VMorpherPhenome phoneme,
+        float frequency, float pitch);
+
+    DEF_NEWDEL(VmorpherState)
+};
+
+std::array<FormantFilter,4> VmorpherState::getFiltersByPhoneme(VMorpherPhenome phoneme,
+    float frequency, float pitch)
+{
+    /* Using soprano formant set of values to
+     * better match mid-range frequency space.
+     *
+     * See: https://www.classes.cs.uchicago.edu/archive/1999/spring/CS295/Computing_Resources/Csound/CsManual3.48b1.HTML/Appendices/table3.html
+     */
+    switch(phoneme)
+    {
+    case VMorpherPhenome::A:
+        return {{
+            {( 800 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f,   0 / 20.0f); */
+            {(1150 * pitch) / frequency, 0.501187f}, /* std::pow(10.0f,  -6 / 20.0f); */
+            {(2900 * pitch) / frequency, 0.025118f}, /* std::pow(10.0f, -32 / 20.0f); */
+            {(3900 * pitch) / frequency, 0.100000f}  /* std::pow(10.0f, -20 / 20.0f); */
+        }};
+    case VMorpherPhenome::E:
+        return {{
+            {( 350 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f,   0 / 20.0f); */
+            {(2000 * pitch) / frequency, 0.100000f}, /* std::pow(10.0f, -20 / 20.0f); */
+            {(2800 * pitch) / frequency, 0.177827f}, /* std::pow(10.0f, -15 / 20.0f); */
+            {(3600 * pitch) / frequency, 0.009999f}  /* std::pow(10.0f, -40 / 20.0f); */
+        }};
+    case VMorpherPhenome::I:
+        return {{
+            {( 270 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f,   0 / 20.0f); */
+            {(2140 * pitch) / frequency, 0.251188f}, /* std::pow(10.0f, -12 / 20.0f); */
+            {(2950 * pitch) / frequency, 0.050118f}, /* std::pow(10.0f, -26 / 20.0f); */
+            {(3900 * pitch) / frequency, 0.050118f}  /* std::pow(10.0f, -26 / 20.0f); */
+        }};
+    case VMorpherPhenome::O:
+        return {{
+            {( 450 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f,   0 / 20.0f); */
+            {( 800 * pitch) / frequency, 0.281838f}, /* std::pow(10.0f, -11 / 20.0f); */
+            {(2830 * pitch) / frequency, 0.079432f}, /* std::pow(10.0f, -22 / 20.0f); */
+            {(3800 * pitch) / frequency, 0.079432f}  /* std::pow(10.0f, -22 / 20.0f); */
+        }};
+    case VMorpherPhenome::U:
+        return {{
+            {( 325 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f,   0 / 20.0f); */
+            {( 700 * pitch) / frequency, 0.158489f}, /* std::pow(10.0f, -16 / 20.0f); */
+            {(2700 * pitch) / frequency, 0.017782f}, /* std::pow(10.0f, -35 / 20.0f); */
+            {(3800 * pitch) / frequency, 0.009999f}  /* std::pow(10.0f, -40 / 20.0f); */
+        }};
+    default:
+        break;
+    }
+    return {};
+}
+
+
+void VmorpherState::deviceUpdate(const DeviceBase*, const BufferStorage*)
+{
+    for(auto &e : mChans)
+    {
+        e.mTargetChannel = InvalidChannelIndex;
+        std::for_each(std::begin(e.mFormants[VOWEL_A_INDEX]), std::end(e.mFormants[VOWEL_A_INDEX]),
+            std::mem_fn(&FormantFilter::clear));
+        std::for_each(std::begin(e.mFormants[VOWEL_B_INDEX]), std::end(e.mFormants[VOWEL_B_INDEX]),
+            std::mem_fn(&FormantFilter::clear));
+        e.mCurrentGain = 0.0f;
+    }
+}
+
+void VmorpherState::update(const ContextBase *context, const EffectSlot *slot,
+    const EffectProps *props, const EffectTarget target)
+{
+    const DeviceBase *device{context->mDevice};
+    const float frequency{static_cast<float>(device->Frequency)};
+    const float step{props->Vmorpher.Rate / frequency};
+    mStep = fastf2u(clampf(step*WAVEFORM_FRACONE, 0.0f, float{WAVEFORM_FRACONE-1}));
+
+    if(mStep == 0)
+        mGetSamples = Oscillate<Half>;
+    else if(props->Vmorpher.Waveform == VMorpherWaveform::Sinusoid)
+        mGetSamples = Oscillate<Sin>;
+    else if(props->Vmorpher.Waveform == VMorpherWaveform::Triangle)
+        mGetSamples = Oscillate<Triangle>;
+    else /*if(props->Vmorpher.Waveform == VMorpherWaveform::Sawtooth)*/
+        mGetSamples = Oscillate<Saw>;
+
+    const float pitchA{std::pow(2.0f,
+        static_cast<float>(props->Vmorpher.PhonemeACoarseTuning) / 12.0f)};
+    const float pitchB{std::pow(2.0f,
+        static_cast<float>(props->Vmorpher.PhonemeBCoarseTuning) / 12.0f)};
+
+    auto vowelA = getFiltersByPhoneme(props->Vmorpher.PhonemeA, frequency, pitchA);
+    auto vowelB = getFiltersByPhoneme(props->Vmorpher.PhonemeB, frequency, pitchB);
+
+    /* Copy the filter coefficients to the input channels. */
+    for(size_t i{0u};i < slot->Wet.Buffer.size();++i)
+    {
+        std::copy(vowelA.begin(), vowelA.end(), std::begin(mChans[i].mFormants[VOWEL_A_INDEX]));
+        std::copy(vowelB.begin(), vowelB.end(), std::begin(mChans[i].mFormants[VOWEL_B_INDEX]));
+    }
+
+    mOutTarget = target.Main->Buffer;
+    auto set_channel = [this](size_t idx, uint outchan, float outgain)
+    {
+        mChans[idx].mTargetChannel = outchan;
+        mChans[idx].mTargetGain = outgain;
+    };
+    target.Main->setAmbiMixParams(slot->Wet, slot->Gain, set_channel);
+}
+
+void VmorpherState::process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn, const al::span<FloatBufferLine> samplesOut)
+{
+    /* Following the EFX specification for a conformant implementation which describes
+     * the effect as a pair of 4-band formant filters blended together using an LFO.
+     */
+    for(size_t base{0u};base < samplesToDo;)
+    {
+        const size_t td{minz(MAX_UPDATE_SAMPLES, samplesToDo-base)};
+
+        mGetSamples(mLfo, mIndex, mStep, td);
+        mIndex += static_cast<uint>(mStep * td);
+        mIndex &= WAVEFORM_FRACMASK;
+
+        auto chandata = std::begin(mChans);
+        for(const auto &input : samplesIn)
+        {
+            const size_t outidx{chandata->mTargetChannel};
+            if(outidx == InvalidChannelIndex)
+            {
+                ++chandata;
+                continue;
+            }
+
+            auto& vowelA = chandata->mFormants[VOWEL_A_INDEX];
+            auto& vowelB = chandata->mFormants[VOWEL_B_INDEX];
+
+            /* Process first vowel. */
+            std::fill_n(std::begin(mSampleBufferA), td, 0.0f);
+            vowelA[0].process(&input[base], mSampleBufferA, td);
+            vowelA[1].process(&input[base], mSampleBufferA, td);
+            vowelA[2].process(&input[base], mSampleBufferA, td);
+            vowelA[3].process(&input[base], mSampleBufferA, td);
+
+            /* Process second vowel. */
+            std::fill_n(std::begin(mSampleBufferB), td, 0.0f);
+            vowelB[0].process(&input[base], mSampleBufferB, td);
+            vowelB[1].process(&input[base], mSampleBufferB, td);
+            vowelB[2].process(&input[base], mSampleBufferB, td);
+            vowelB[3].process(&input[base], mSampleBufferB, td);
+
+            alignas(16) float blended[MAX_UPDATE_SAMPLES];
+            for(size_t i{0u};i < td;i++)
+                blended[i] = lerpf(mSampleBufferA[i], mSampleBufferB[i], mLfo[i]);
+
+            /* Now, mix the processed sound data to the output. */
+            MixSamples({blended, td}, samplesOut[outidx].data()+base, chandata->mCurrentGain,
+                chandata->mTargetGain, samplesToDo-base);
+            ++chandata;
+        }
+
+        base += td;
+    }
+}
+
+
+struct VmorpherStateFactory final : public EffectStateFactory {
+    al::intrusive_ptr<EffectState> create() override
+    { return al::intrusive_ptr<EffectState>{new VmorpherState{}}; }
+};
+
+} // namespace
+
+EffectStateFactory *VmorpherStateFactory_getFactory()
+{
+    static VmorpherStateFactory VmorpherFactory{};
+    return &VmorpherFactory;
+}
diff --git a/alc/inprogext.h b/alc/inprogext.h
new file mode 100644 (file)
index 0000000..ccb9a4b
--- /dev/null
@@ -0,0 +1,73 @@
+#ifndef INPROGEXT_H
+#define INPROGEXT_H
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef AL_SOFT_map_buffer
+#define AL_SOFT_map_buffer 1
+typedef unsigned int ALbitfieldSOFT;
+#define AL_MAP_READ_BIT_SOFT                     0x00000001
+#define AL_MAP_WRITE_BIT_SOFT                    0x00000002
+#define AL_MAP_PERSISTENT_BIT_SOFT               0x00000004
+#define AL_PRESERVE_DATA_BIT_SOFT                0x00000008
+typedef void (AL_APIENTRY*LPALBUFFERSTORAGESOFT)(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei freq, ALbitfieldSOFT flags);
+typedef void* (AL_APIENTRY*LPALMAPBUFFERSOFT)(ALuint buffer, ALsizei offset, ALsizei length, ALbitfieldSOFT access);
+typedef void (AL_APIENTRY*LPALUNMAPBUFFERSOFT)(ALuint buffer);
+typedef void (AL_APIENTRY*LPALFLUSHMAPPEDBUFFERSOFT)(ALuint buffer, ALsizei offset, ALsizei length);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alBufferStorageSOFT(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei freq, ALbitfieldSOFT flags);
+AL_API void* AL_APIENTRY alMapBufferSOFT(ALuint buffer, ALsizei offset, ALsizei length, ALbitfieldSOFT access);
+AL_API void AL_APIENTRY alUnmapBufferSOFT(ALuint buffer);
+AL_API void AL_APIENTRY alFlushMappedBufferSOFT(ALuint buffer, ALsizei offset, ALsizei length);
+#endif
+#endif
+
+#ifndef AL_SOFT_bformat_hoa
+#define AL_SOFT_bformat_hoa
+#define AL_UNPACK_AMBISONIC_ORDER_SOFT           0x199D
+#endif
+
+#ifndef AL_SOFT_convolution_reverb
+#define AL_SOFT_convolution_reverb
+#define AL_EFFECT_CONVOLUTION_REVERB_SOFT        0xA000
+#define AL_EFFECTSLOT_STATE_SOFT                 0x199D
+typedef void (AL_APIENTRY*LPALAUXILIARYEFFECTSLOTPLAYSOFT)(ALuint slotid);
+typedef void (AL_APIENTRY*LPALAUXILIARYEFFECTSLOTPLAYVSOFT)(ALsizei n, const ALuint *slotids);
+typedef void (AL_APIENTRY*LPALAUXILIARYEFFECTSLOTSTOPSOFT)(ALuint slotid);
+typedef void (AL_APIENTRY*LPALAUXILIARYEFFECTSLOTSTOPVSOFT)(ALsizei n, const ALuint *slotids);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotPlaySOFT(ALuint slotid);
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotPlayvSOFT(ALsizei n, const ALuint *slotids);
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotStopSOFT(ALuint slotid);
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotStopvSOFT(ALsizei n, const ALuint *slotids);
+#endif
+#endif
+
+#ifndef AL_SOFT_hold_on_disconnect
+#define AL_SOFT_hold_on_disconnect
+#define AL_STOP_SOURCES_ON_DISCONNECT_SOFT       0x19AB
+#endif
+
+
+/* Non-standard export. Not part of any extension. */
+AL_API const ALchar* AL_APIENTRY alsoft_get_version(void);
+
+
+/* Functions from abandoned extensions. Only here for binary compatibility. */
+AL_API void AL_APIENTRY alSourceQueueBufferLayersSOFT(ALuint src, ALsizei nb,
+    const ALuint *buffers);
+
+AL_API ALint64SOFT AL_APIENTRY alGetInteger64SOFT(ALenum pname);
+AL_API void AL_APIENTRY alGetInteger64vSOFT(ALenum pname, ALint64SOFT *values);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* INPROGEXT_H */
diff --git a/alc/panning.cpp b/alc/panning.cpp
new file mode 100644 (file)
index 0000000..d118f99
--- /dev/null
@@ -0,0 +1,1152 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 1999-2010 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 <algorithm>
+#include <array>
+#include <cassert>
+#include <chrono>
+#include <cmath>
+#include <cstdio>
+#include <cstring>
+#include <functional>
+#include <iterator>
+#include <memory>
+#include <new>
+#include <numeric>
+#include <string>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "al/auxeffectslot.h"
+#include "albit.h"
+#include "alconfig.h"
+#include "alc/context.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "alu.h"
+#include "core/ambdec.h"
+#include "core/ambidefs.h"
+#include "core/bformatdec.h"
+#include "core/bs2b.h"
+#include "core/devformat.h"
+#include "core/front_stablizer.h"
+#include "core/hrtf.h"
+#include "core/logging.h"
+#include "core/uhjfilter.h"
+#include "device.h"
+#include "opthelpers.h"
+
+
+namespace {
+
+using namespace std::placeholders;
+using std::chrono::seconds;
+using std::chrono::nanoseconds;
+
+inline const char *GetLabelFromChannel(Channel channel)
+{
+    switch(channel)
+    {
+        case FrontLeft: return "front-left";
+        case FrontRight: return "front-right";
+        case FrontCenter: return "front-center";
+        case LFE: return "lfe";
+        case BackLeft: return "back-left";
+        case BackRight: return "back-right";
+        case BackCenter: return "back-center";
+        case SideLeft: return "side-left";
+        case SideRight: return "side-right";
+
+        case TopFrontLeft: return "top-front-left";
+        case TopFrontCenter: return "top-front-center";
+        case TopFrontRight: return "top-front-right";
+        case TopCenter: return "top-center";
+        case TopBackLeft: return "top-back-left";
+        case TopBackCenter: return "top-back-center";
+        case TopBackRight: return "top-back-right";
+
+        case Aux0: return "Aux0";
+        case Aux1: return "Aux1";
+        case Aux2: return "Aux2";
+        case Aux3: return "Aux3";
+        case Aux4: return "Aux4";
+        case Aux5: return "Aux5";
+        case Aux6: return "Aux6";
+        case Aux7: return "Aux7";
+        case Aux8: return "Aux8";
+        case Aux9: return "Aux9";
+        case Aux10: return "Aux10";
+        case Aux11: return "Aux11";
+        case Aux12: return "Aux12";
+        case Aux13: return "Aux13";
+        case Aux14: return "Aux14";
+        case Aux15: return "Aux15";
+
+        case MaxChannels: break;
+    }
+    return "(unknown)";
+}
+
+
+std::unique_ptr<FrontStablizer> CreateStablizer(const size_t outchans, const uint srate)
+{
+    auto stablizer = FrontStablizer::Create(outchans);
+
+    /* Initialize band-splitting filter for the mid signal, with a crossover at
+     * 5khz (could be higher).
+     */
+    stablizer->MidFilter.init(5000.0f / static_cast<float>(srate));
+    for(auto &filter : stablizer->ChannelFilters)
+        filter = stablizer->MidFilter;
+
+    return stablizer;
+}
+
+void AllocChannels(ALCdevice *device, const size_t main_chans, const size_t real_chans)
+{
+    TRACE("Channel config, Main: %zu, Real: %zu\n", main_chans, real_chans);
+
+    /* Allocate extra channels for any post-filter output. */
+    const size_t num_chans{main_chans + real_chans};
+
+    TRACE("Allocating %zu channels, %zu bytes\n", num_chans,
+        num_chans*sizeof(device->MixBuffer[0]));
+    device->MixBuffer.resize(num_chans);
+    al::span<FloatBufferLine> buffer{device->MixBuffer};
+
+    device->Dry.Buffer = buffer.first(main_chans);
+    buffer = buffer.subspan(main_chans);
+    if(real_chans != 0)
+    {
+        device->RealOut.Buffer = buffer.first(real_chans);
+        buffer = buffer.subspan(real_chans);
+    }
+    else
+        device->RealOut.Buffer = device->Dry.Buffer;
+}
+
+
+using ChannelCoeffs = std::array<float,MaxAmbiChannels>;
+enum DecoderMode : bool {
+    SingleBand = false,
+    DualBand = true
+};
+
+template<DecoderMode Mode, size_t N>
+struct DecoderConfig;
+
+template<size_t N>
+struct DecoderConfig<SingleBand, N> {
+    uint8_t mOrder{};
+    bool mIs3D{};
+    std::array<Channel,N> mChannels{};
+    DevAmbiScaling mScaling{};
+    std::array<float,MaxAmbiOrder+1> mOrderGain{};
+    std::array<ChannelCoeffs,N> mCoeffs{};
+};
+
+template<size_t N>
+struct DecoderConfig<DualBand, N> {
+    uint8_t mOrder{};
+    bool mIs3D{};
+    std::array<Channel,N> mChannels{};
+    DevAmbiScaling mScaling{};
+    std::array<float,MaxAmbiOrder+1> mOrderGain{};
+    std::array<ChannelCoeffs,N> mCoeffs{};
+    std::array<float,MaxAmbiOrder+1> mOrderGainLF{};
+    std::array<ChannelCoeffs,N> mCoeffsLF{};
+};
+
+template<>
+struct DecoderConfig<DualBand, 0> {
+    uint8_t mOrder{};
+    bool mIs3D{};
+    al::span<const Channel> mChannels;
+    DevAmbiScaling mScaling{};
+    al::span<const float> mOrderGain;
+    al::span<const ChannelCoeffs> mCoeffs;
+    al::span<const float> mOrderGainLF;
+    al::span<const ChannelCoeffs> mCoeffsLF;
+
+    template<size_t N>
+    DecoderConfig& operator=(const DecoderConfig<SingleBand,N> &rhs) noexcept
+    {
+        mOrder = rhs.mOrder;
+        mIs3D = rhs.mIs3D;
+        mChannels = rhs.mChannels;
+        mScaling = rhs.mScaling;
+        mOrderGain = rhs.mOrderGain;
+        mCoeffs = rhs.mCoeffs;
+        mOrderGainLF = {};
+        mCoeffsLF = {};
+        return *this;
+    }
+
+    template<size_t N>
+    DecoderConfig& operator=(const DecoderConfig<DualBand,N> &rhs) noexcept
+    {
+        mOrder = rhs.mOrder;
+        mIs3D = rhs.mIs3D;
+        mChannels = rhs.mChannels;
+        mScaling = rhs.mScaling;
+        mOrderGain = rhs.mOrderGain;
+        mCoeffs = rhs.mCoeffs;
+        mOrderGainLF = rhs.mOrderGainLF;
+        mCoeffsLF = rhs.mCoeffsLF;
+        return *this;
+    }
+
+    explicit operator bool() const noexcept { return !mChannels.empty(); }
+};
+using DecoderView = DecoderConfig<DualBand, 0>;
+
+
+void InitNearFieldCtrl(ALCdevice *device, float ctrl_dist, uint order, bool is3d)
+{
+    static const uint chans_per_order2d[MaxAmbiOrder+1]{ 1, 2, 2, 2 };
+    static const uint chans_per_order3d[MaxAmbiOrder+1]{ 1, 3, 5, 7 };
+
+    /* NFC is only used when AvgSpeakerDist is greater than 0. */
+    if(!device->getConfigValueBool("decoder", "nfc", false) || !(ctrl_dist > 0.0f))
+        return;
+
+    device->AvgSpeakerDist = clampf(ctrl_dist, 0.1f, 10.0f);
+    TRACE("Using near-field reference distance: %.2f meters\n", device->AvgSpeakerDist);
+
+    const float w1{SpeedOfSoundMetersPerSec /
+        (device->AvgSpeakerDist * static_cast<float>(device->Frequency))};
+    device->mNFCtrlFilter.init(w1);
+
+    auto iter = std::copy_n(is3d ? chans_per_order3d : chans_per_order2d, order+1u,
+        std::begin(device->NumChannelsPerOrder));
+    std::fill(iter, std::end(device->NumChannelsPerOrder), 0u);
+}
+
+void InitDistanceComp(ALCdevice *device, const al::span<const Channel> channels,
+    const al::span<const float,MAX_OUTPUT_CHANNELS> dists)
+{
+    const float maxdist{std::accumulate(std::begin(dists), std::end(dists), 0.0f, maxf)};
+
+    if(!device->getConfigValueBool("decoder", "distance-comp", true) || !(maxdist > 0.0f))
+        return;
+
+    const auto distSampleScale = static_cast<float>(device->Frequency) / SpeedOfSoundMetersPerSec;
+    std::vector<DistanceComp::ChanData> ChanDelay;
+    ChanDelay.reserve(device->RealOut.Buffer.size());
+    size_t total{0u};
+    for(size_t chidx{0};chidx < channels.size();++chidx)
+    {
+        const Channel ch{channels[chidx]};
+        const uint idx{device->RealOut.ChannelIndex[ch]};
+        if(idx == InvalidChannelIndex)
+            continue;
+
+        const float distance{dists[chidx]};
+
+        /* Distance compensation only delays in steps of the sample rate. This
+         * is a bit less accurate since the delay time falls to the nearest
+         * sample time, but it's far simpler as it doesn't have to deal with
+         * phase offsets. This means at 48khz, for instance, the distance delay
+         * will be in steps of about 7 millimeters.
+         */
+        float delay{std::floor((maxdist - distance)*distSampleScale + 0.5f)};
+        if(delay > float{DistanceComp::MaxDelay-1})
+        {
+            ERR("Delay for channel %u (%s) exceeds buffer length (%f > %d)\n", idx,
+                GetLabelFromChannel(ch), delay, DistanceComp::MaxDelay-1);
+            delay = float{DistanceComp::MaxDelay-1};
+        }
+
+        ChanDelay.resize(maxz(ChanDelay.size(), idx+1));
+        ChanDelay[idx].Length = static_cast<uint>(delay);
+        ChanDelay[idx].Gain = distance / maxdist;
+        TRACE("Channel %s distance comp: %u samples, %f gain\n", GetLabelFromChannel(ch),
+            ChanDelay[idx].Length, ChanDelay[idx].Gain);
+
+        /* Round up to the next 4th sample, so each channel buffer starts
+         * 16-byte aligned.
+         */
+        total += RoundUp(ChanDelay[idx].Length, 4);
+    }
+
+    if(total > 0)
+    {
+        auto chandelays = DistanceComp::Create(total);
+
+        ChanDelay[0].Buffer = chandelays->mSamples.data();
+        auto set_bufptr = [](const DistanceComp::ChanData &last, const DistanceComp::ChanData &cur)
+            -> DistanceComp::ChanData
+        {
+            DistanceComp::ChanData ret{cur};
+            ret.Buffer = last.Buffer + RoundUp(last.Length, 4);
+            return ret;
+        };
+        std::partial_sum(ChanDelay.begin(), ChanDelay.end(), chandelays->mChannels.begin(),
+            set_bufptr);
+        device->ChannelDelays = std::move(chandelays);
+    }
+}
+
+
+inline auto& GetAmbiScales(DevAmbiScaling scaletype) noexcept
+{
+    if(scaletype == DevAmbiScaling::FuMa) return AmbiScale::FromFuMa();
+    if(scaletype == DevAmbiScaling::SN3D) return AmbiScale::FromSN3D();
+    return AmbiScale::FromN3D();
+}
+
+inline auto& GetAmbiLayout(DevAmbiLayout layouttype) noexcept
+{
+    if(layouttype == DevAmbiLayout::FuMa) return AmbiIndex::FromFuMa();
+    return AmbiIndex::FromACN();
+}
+
+
+DecoderView MakeDecoderView(ALCdevice *device, const AmbDecConf *conf,
+    DecoderConfig<DualBand, MAX_OUTPUT_CHANNELS> &decoder)
+{
+    DecoderView ret{};
+
+    decoder.mOrder = (conf->ChanMask > Ambi3OrderMask) ? uint8_t{4} :
+        (conf->ChanMask > Ambi2OrderMask) ? uint8_t{3} :
+        (conf->ChanMask > Ambi1OrderMask) ? uint8_t{2} : uint8_t{1};
+    decoder.mIs3D = (conf->ChanMask&AmbiPeriphonicMask) != 0;
+
+    switch(conf->CoeffScale)
+    {
+    case AmbDecScale::Unset: ASSUME(false); break;
+    case AmbDecScale::N3D: decoder.mScaling = DevAmbiScaling::N3D; break;
+    case AmbDecScale::SN3D: decoder.mScaling = DevAmbiScaling::SN3D; break;
+    case AmbDecScale::FuMa: decoder.mScaling = DevAmbiScaling::FuMa; break;
+    }
+
+    std::copy_n(std::begin(conf->HFOrderGain),
+        std::min(al::size(conf->HFOrderGain), al::size(decoder.mOrderGain)),
+        std::begin(decoder.mOrderGain));
+    std::copy_n(std::begin(conf->LFOrderGain),
+        std::min(al::size(conf->LFOrderGain), al::size(decoder.mOrderGainLF)),
+        std::begin(decoder.mOrderGainLF));
+
+    const auto num_coeffs = decoder.mIs3D ? AmbiChannelsFromOrder(decoder.mOrder)
+        : Ambi2DChannelsFromOrder(decoder.mOrder);
+    const auto idx_map = decoder.mIs3D ? AmbiIndex::FromACN().data()
+        : AmbiIndex::FromACN2D().data();
+    const auto hfmatrix = conf->HFMatrix;
+    const auto lfmatrix = conf->LFMatrix;
+
+    uint chan_count{0};
+    using const_speaker_span = al::span<const AmbDecConf::SpeakerConf>;
+    for(auto &speaker : const_speaker_span{conf->Speakers.get(), conf->NumSpeakers})
+    {
+        /* NOTE: AmbDec does not define any standard speaker names, however
+         * for this to work we have to by able to find the output channel
+         * the speaker definition corresponds to. Therefore, OpenAL Soft
+         * requires these channel labels to be recognized:
+         *
+         * LF = Front left
+         * RF = Front right
+         * LS = Side left
+         * RS = Side right
+         * LB = Back left
+         * RB = Back right
+         * CE = Front center
+         * CB = Back center
+         * LFT = Top front left
+         * RFT = Top front right
+         * LBT = Top back left
+         * RBT = Top back right
+         *
+         * Additionally, surround51 will acknowledge back speakers for side
+         * channels, to avoid issues with an ambdec expecting 5.1 to use the
+         * back channels.
+         */
+        Channel ch{};
+        if(speaker.Name == "LF")
+            ch = FrontLeft;
+        else if(speaker.Name == "RF")
+            ch = FrontRight;
+        else if(speaker.Name == "CE")
+            ch = FrontCenter;
+        else if(speaker.Name == "LS")
+            ch = SideLeft;
+        else if(speaker.Name == "RS")
+            ch = SideRight;
+        else if(speaker.Name == "LB")
+            ch = (device->FmtChans == DevFmtX51) ? SideLeft : BackLeft;
+        else if(speaker.Name == "RB")
+            ch = (device->FmtChans == DevFmtX51) ? SideRight : BackRight;
+        else if(speaker.Name == "CB")
+            ch = BackCenter;
+        else if(speaker.Name == "LFT")
+            ch = TopFrontLeft;
+        else if(speaker.Name == "RFT")
+            ch = TopFrontRight;
+        else if(speaker.Name == "LBT")
+            ch = TopBackLeft;
+        else if(speaker.Name == "RBT")
+            ch = TopBackRight;
+        else
+        {
+            int idx{};
+            char c{};
+            if(sscanf(speaker.Name.c_str(), "AUX%d%c", &idx, &c) != 1 || idx < 0
+                || idx >= MaxChannels-Aux0)
+            {
+                ERR("AmbDec speaker label \"%s\" not recognized\n", speaker.Name.c_str());
+                continue;
+            }
+            ch = static_cast<Channel>(Aux0+idx);
+        }
+
+        decoder.mChannels[chan_count] = ch;
+        for(size_t dst{0};dst < num_coeffs;++dst)
+        {
+            const size_t src{idx_map[dst]};
+            decoder.mCoeffs[chan_count][dst] = hfmatrix[chan_count][src];
+        }
+        if(conf->FreqBands > 1)
+        {
+            for(size_t dst{0};dst < num_coeffs;++dst)
+            {
+                const size_t src{idx_map[dst]};
+                decoder.mCoeffsLF[chan_count][dst] = lfmatrix[chan_count][src];
+            }
+        }
+        ++chan_count;
+    }
+
+    if(chan_count > 0)
+    {
+        ret.mOrder = decoder.mOrder;
+        ret.mIs3D = decoder.mIs3D;
+        ret.mScaling = decoder.mScaling;
+        ret.mChannels = {decoder.mChannels.data(), chan_count};
+        ret.mOrderGain = decoder.mOrderGain;
+        ret.mCoeffs = {decoder.mCoeffs.data(), chan_count};
+        if(conf->FreqBands > 1)
+        {
+            ret.mOrderGainLF = decoder.mOrderGainLF;
+            ret.mCoeffsLF = {decoder.mCoeffsLF.data(), chan_count};
+        }
+    }
+    return ret;
+}
+
+constexpr DecoderConfig<SingleBand, 1> MonoConfig{
+    0, false, {{FrontCenter}},
+    DevAmbiScaling::N3D,
+    {{1.0f}},
+    {{ {{1.0f}} }}
+};
+constexpr DecoderConfig<SingleBand, 2> StereoConfig{
+    1, false, {{FrontLeft, FrontRight}},
+    DevAmbiScaling::N3D,
+    {{1.0f, 1.0f}},
+    {{
+        {{5.00000000e-1f,  2.88675135e-1f,  5.52305643e-2f}},
+        {{5.00000000e-1f, -2.88675135e-1f,  5.52305643e-2f}},
+    }}
+};
+constexpr DecoderConfig<DualBand, 4> QuadConfig{
+    1, false, {{BackLeft, FrontLeft, FrontRight, BackRight}},
+    DevAmbiScaling::N3D,
+    /*HF*/{{1.41421356e+0f, 1.00000000e+0f}},
+    {{
+        {{2.50000000e-1f,  2.04124145e-1f, -2.04124145e-1f}},
+        {{2.50000000e-1f,  2.04124145e-1f,  2.04124145e-1f}},
+        {{2.50000000e-1f, -2.04124145e-1f,  2.04124145e-1f}},
+        {{2.50000000e-1f, -2.04124145e-1f, -2.04124145e-1f}},
+    }},
+    /*LF*/{{1.00000000e+0f, 1.00000000e+0f}},
+    {{
+        {{2.50000000e-1f,  2.04124145e-1f, -2.04124145e-1f}},
+        {{2.50000000e-1f,  2.04124145e-1f,  2.04124145e-1f}},
+        {{2.50000000e-1f, -2.04124145e-1f,  2.04124145e-1f}},
+        {{2.50000000e-1f, -2.04124145e-1f, -2.04124145e-1f}},
+    }}
+};
+constexpr DecoderConfig<DualBand, 5> X51Config{
+    2, false, {{SideLeft, FrontLeft, FrontCenter, FrontRight, SideRight}},
+    DevAmbiScaling::FuMa,
+    /*HF*/{{1.00000000e+0f, 1.00000000e+0f, 1.00000000e+0f}},
+    {{
+        {{5.67316000e-1f,  4.22920000e-1f, -3.15495000e-1f, -6.34490000e-2f, -2.92380000e-2f}},
+        {{3.68584000e-1f,  2.72349000e-1f,  3.21616000e-1f,  1.92645000e-1f,  4.82600000e-2f}},
+        {{1.83579000e-1f,  0.00000000e+0f,  1.99588000e-1f,  0.00000000e+0f,  9.62820000e-2f}},
+        {{3.68584000e-1f, -2.72349000e-1f,  3.21616000e-1f, -1.92645000e-1f,  4.82600000e-2f}},
+        {{5.67316000e-1f, -4.22920000e-1f, -3.15495000e-1f,  6.34490000e-2f, -2.92380000e-2f}},
+    }},
+    /*LF*/{{1.00000000e+0f, 1.00000000e+0f, 1.00000000e+0f}},
+    {{
+        {{4.90109850e-1f,  3.77305010e-1f, -3.73106990e-1f, -1.25914530e-1f,  1.45133000e-2f}},
+        {{1.49085730e-1f,  3.03561680e-1f,  1.53290060e-1f,  2.45112480e-1f, -1.50753130e-1f}},
+        {{1.37654920e-1f,  0.00000000e+0f,  4.49417940e-1f,  0.00000000e+0f,  2.57844070e-1f}},
+        {{1.49085730e-1f, -3.03561680e-1f,  1.53290060e-1f, -2.45112480e-1f, -1.50753130e-1f}},
+        {{4.90109850e-1f, -3.77305010e-1f, -3.73106990e-1f,  1.25914530e-1f,  1.45133000e-2f}},
+    }}
+};
+constexpr DecoderConfig<SingleBand, 5> X61Config{
+    2, false, {{SideLeft, FrontLeft, FrontRight, SideRight, BackCenter}},
+    DevAmbiScaling::N3D,
+    {{1.0f, 1.0f, 1.0f}},
+    {{
+        {{2.04460341e-1f,  2.17177926e-1f, -4.39996780e-2f, -2.60790269e-2f, -6.87239792e-2f}},
+        {{1.58923161e-1f,  9.21772680e-2f,  1.59658796e-1f,  6.66278083e-2f,  3.84686854e-2f}},
+        {{1.58923161e-1f, -9.21772680e-2f,  1.59658796e-1f, -6.66278083e-2f,  3.84686854e-2f}},
+        {{2.04460341e-1f, -2.17177926e-1f, -4.39996780e-2f,  2.60790269e-2f, -6.87239792e-2f}},
+        {{2.50001688e-1f,  0.00000000e+0f, -2.50000094e-1f,  0.00000000e+0f,  6.05133395e-2f}},
+    }}
+};
+constexpr DecoderConfig<DualBand, 6> X71Config{
+    2, false, {{BackLeft, SideLeft, FrontLeft, FrontRight, SideRight, BackRight}},
+    DevAmbiScaling::N3D,
+    /*HF*/{{1.41421356e+0f, 1.22474487e+0f, 7.07106781e-1f}},
+    {{
+        {{1.66666667e-1f,  9.62250449e-2f, -1.66666667e-1f, -1.49071198e-1f,  8.60662966e-2f}},
+        {{1.66666667e-1f,  1.92450090e-1f,  0.00000000e+0f,  0.00000000e+0f, -1.72132593e-1f}},
+        {{1.66666667e-1f,  9.62250449e-2f,  1.66666667e-1f,  1.49071198e-1f,  8.60662966e-2f}},
+        {{1.66666667e-1f, -9.62250449e-2f,  1.66666667e-1f, -1.49071198e-1f,  8.60662966e-2f}},
+        {{1.66666667e-1f, -1.92450090e-1f,  0.00000000e+0f,  0.00000000e+0f, -1.72132593e-1f}},
+        {{1.66666667e-1f, -9.62250449e-2f, -1.66666667e-1f,  1.49071198e-1f,  8.60662966e-2f}},
+    }},
+    /*LF*/{{1.00000000e+0f, 1.00000000e+0f, 1.00000000e+0f}},
+    {{
+        {{1.66666667e-1f,  9.62250449e-2f, -1.66666667e-1f, -1.49071198e-1f,  8.60662966e-2f}},
+        {{1.66666667e-1f,  1.92450090e-1f,  0.00000000e+0f,  0.00000000e+0f, -1.72132593e-1f}},
+        {{1.66666667e-1f,  9.62250449e-2f,  1.66666667e-1f,  1.49071198e-1f,  8.60662966e-2f}},
+        {{1.66666667e-1f, -9.62250449e-2f,  1.66666667e-1f, -1.49071198e-1f,  8.60662966e-2f}},
+        {{1.66666667e-1f, -1.92450090e-1f,  0.00000000e+0f,  0.00000000e+0f, -1.72132593e-1f}},
+        {{1.66666667e-1f, -9.62250449e-2f, -1.66666667e-1f,  1.49071198e-1f,  8.60662966e-2f}},
+    }}
+};
+constexpr DecoderConfig<DualBand, 6> X3D71Config{
+    1, true, {{Aux0, SideLeft, FrontLeft, FrontRight, SideRight, Aux1}},
+    DevAmbiScaling::N3D,
+    /*HF*/{{1.73205081e+0f, 1.00000000e+0f}},
+    {{
+        {{1.666666667e-01f,  0.000000000e+00f,  2.356640879e-01f, -1.667265410e-01f}},
+        {{1.666666667e-01f,  2.033043281e-01f, -1.175581508e-01f, -1.678904388e-01f}},
+        {{1.666666667e-01f,  2.033043281e-01f,  1.175581508e-01f,  1.678904388e-01f}},
+        {{1.666666667e-01f, -2.033043281e-01f,  1.175581508e-01f,  1.678904388e-01f}},
+        {{1.666666667e-01f, -2.033043281e-01f, -1.175581508e-01f, -1.678904388e-01f}},
+        {{1.666666667e-01f,  0.000000000e+00f, -2.356640879e-01f,  1.667265410e-01f}},
+    }},
+    /*LF*/{{1.00000000e+0f, 1.00000000e+0f}},
+    {{
+        {{1.666666667e-01f,  0.000000000e+00f,  2.356640879e-01f, -1.667265410e-01f}},
+        {{1.666666667e-01f,  2.033043281e-01f, -1.175581508e-01f, -1.678904388e-01f}},
+        {{1.666666667e-01f,  2.033043281e-01f,  1.175581508e-01f,  1.678904388e-01f}},
+        {{1.666666667e-01f, -2.033043281e-01f,  1.175581508e-01f,  1.678904388e-01f}},
+        {{1.666666667e-01f, -2.033043281e-01f, -1.175581508e-01f, -1.678904388e-01f}},
+        {{1.666666667e-01f,  0.000000000e+00f, -2.356640879e-01f,  1.667265410e-01f}},
+    }}
+};
+constexpr DecoderConfig<SingleBand, 10> X714Config{
+    1, true, {{FrontLeft, FrontRight, SideLeft, SideRight, BackLeft, BackRight, TopFrontLeft, TopFrontRight, TopBackLeft, TopBackRight }},
+    DevAmbiScaling::N3D,
+    {{1.00000000e+0f, 1.00000000e+0f, 1.00000000e+0f}},
+    {{
+        {{1.27149251e-01f,  7.63047539e-02f, -3.64373750e-02f,  1.59700680e-01f}},
+        {{1.07005418e-01f, -7.67638760e-02f, -4.92129762e-02f,  1.29012797e-01f}},
+        {{1.26400196e-01f,  1.77494694e-01f, -3.71203389e-02f,  0.00000000e+00f}},
+        {{1.26396516e-01f, -1.77488059e-01f, -3.71297878e-02f,  0.00000000e+00f}},
+        {{1.06996956e-01f,  7.67615256e-02f, -4.92166307e-02f, -1.29001640e-01f}},
+        {{1.27145671e-01f, -7.63003471e-02f, -3.64353304e-02f, -1.59697510e-01f}},
+        {{8.80919747e-02f,  7.48940670e-02f,  9.08786244e-02f,  6.22527183e-02f}},
+        {{1.57880745e-01f, -7.28755272e-02f,  1.82364187e-01f,  8.74240284e-02f}},
+        {{1.57892225e-01f,  7.28944768e-02f,  1.82363474e-01f, -8.74301086e-02f}},
+        {{8.80892603e-02f, -7.48948724e-02f,  9.08779842e-02f, -6.22480443e-02f}},
+    }}
+};
+
+void InitPanning(ALCdevice *device, const bool hqdec=false, const bool stablize=false,
+    DecoderView decoder={})
+{
+    if(!decoder)
+    {
+        switch(device->FmtChans)
+        {
+        case DevFmtMono: decoder = MonoConfig; break;
+        case DevFmtStereo: decoder = StereoConfig; break;
+        case DevFmtQuad: decoder = QuadConfig; break;
+        case DevFmtX51: decoder = X51Config; break;
+        case DevFmtX61: decoder = X61Config; break;
+        case DevFmtX71: decoder = X71Config; break;
+        case DevFmtX714: decoder = X714Config; break;
+        case DevFmtX3D71: decoder = X3D71Config; break;
+        case DevFmtAmbi3D:
+            auto&& acnmap = GetAmbiLayout(device->mAmbiLayout);
+            auto&& n3dscale = GetAmbiScales(device->mAmbiScale);
+
+            /* For DevFmtAmbi3D, the ambisonic order is already set. */
+            const size_t count{AmbiChannelsFromOrder(device->mAmbiOrder)};
+            std::transform(acnmap.begin(), acnmap.begin()+count, std::begin(device->Dry.AmbiMap),
+                [&n3dscale](const uint8_t &acn) noexcept -> BFChannelConfig
+                { return BFChannelConfig{1.0f/n3dscale[acn], acn}; });
+            AllocChannels(device, count, 0);
+            device->m2DMixing = false;
+
+            float avg_dist{};
+            if(auto distopt = device->configValue<float>("decoder", "speaker-dist"))
+                avg_dist = *distopt;
+            else if(auto delayopt = device->configValue<float>("decoder", "nfc-ref-delay"))
+            {
+                WARN("nfc-ref-delay is deprecated, use speaker-dist instead\n");
+                avg_dist = *delayopt * SpeedOfSoundMetersPerSec;
+            }
+
+            InitNearFieldCtrl(device, avg_dist, device->mAmbiOrder, true);
+            return;
+        }
+    }
+
+    const size_t ambicount{decoder.mIs3D ? AmbiChannelsFromOrder(decoder.mOrder) :
+        Ambi2DChannelsFromOrder(decoder.mOrder)};
+    const bool dual_band{hqdec && !decoder.mCoeffsLF.empty()};
+    al::vector<ChannelDec> chancoeffs, chancoeffslf;
+    for(size_t i{0u};i < decoder.mChannels.size();++i)
+    {
+        const uint idx{device->channelIdxByName(decoder.mChannels[i])};
+        if(idx == InvalidChannelIndex)
+        {
+            ERR("Failed to find %s channel in device\n",
+                GetLabelFromChannel(decoder.mChannels[i]));
+            continue;
+        }
+
+        auto ordermap = decoder.mIs3D ? AmbiIndex::OrderFromChannel().data()
+            : AmbiIndex::OrderFrom2DChannel().data();
+
+        chancoeffs.resize(maxz(chancoeffs.size(), idx+1u), ChannelDec{});
+        al::span<const float,MaxAmbiChannels> src{decoder.mCoeffs[i]};
+        al::span<float,MaxAmbiChannels> dst{chancoeffs[idx]};
+        for(size_t ambichan{0};ambichan < ambicount;++ambichan)
+            dst[ambichan] = src[ambichan] * decoder.mOrderGain[ordermap[ambichan]];
+
+        if(!dual_band)
+            continue;
+
+        chancoeffslf.resize(maxz(chancoeffslf.size(), idx+1u), ChannelDec{});
+        src = decoder.mCoeffsLF[i];
+        dst = chancoeffslf[idx];
+        for(size_t ambichan{0};ambichan < ambicount;++ambichan)
+            dst[ambichan] = src[ambichan] * decoder.mOrderGainLF[ordermap[ambichan]];
+    }
+
+    /* For non-DevFmtAmbi3D, set the ambisonic order. */
+    device->mAmbiOrder = decoder.mOrder;
+    device->m2DMixing = !decoder.mIs3D;
+
+    const al::span<const uint8_t> acnmap{decoder.mIs3D ? AmbiIndex::FromACN().data() :
+        AmbiIndex::FromACN2D().data(), ambicount};
+    auto&& coeffscale = GetAmbiScales(decoder.mScaling);
+    std::transform(acnmap.begin(), acnmap.end(), std::begin(device->Dry.AmbiMap),
+        [&coeffscale](const uint8_t &acn) noexcept
+        { return BFChannelConfig{1.0f/coeffscale[acn], acn}; });
+    AllocChannels(device, ambicount, device->channelsFromFmt());
+
+    std::unique_ptr<FrontStablizer> stablizer;
+    if(stablize)
+    {
+        /* Only enable the stablizer if the decoder does not output to the
+         * front-center channel.
+         */
+        const auto cidx = device->RealOut.ChannelIndex[FrontCenter];
+        bool hasfc{false};
+        if(cidx < chancoeffs.size())
+        {
+            for(const auto &coeff : chancoeffs[cidx])
+                hasfc |= coeff != 0.0f;
+        }
+        if(!hasfc && cidx < chancoeffslf.size())
+        {
+            for(const auto &coeff : chancoeffslf[cidx])
+                hasfc |= coeff != 0.0f;
+        }
+        if(!hasfc)
+        {
+            stablizer = CreateStablizer(device->channelsFromFmt(), device->Frequency);
+            TRACE("Front stablizer enabled\n");
+        }
+    }
+
+    TRACE("Enabling %s-band %s-order%s ambisonic decoder\n",
+        !dual_band ? "single" : "dual",
+        (decoder.mOrder > 3) ? "fourth" :
+        (decoder.mOrder > 2) ? "third" :
+        (decoder.mOrder > 1) ? "second" : "first",
+        decoder.mIs3D ? " periphonic" : "");
+    device->AmbiDecoder = BFormatDec::Create(ambicount, chancoeffs, chancoeffslf,
+        device->mXOverFreq/static_cast<float>(device->Frequency), std::move(stablizer));
+}
+
+void InitHrtfPanning(ALCdevice *device)
+{
+    constexpr float Deg180{al::numbers::pi_v<float>};
+    constexpr float Deg_90{Deg180 / 2.0f /* 90 degrees*/};
+    constexpr float Deg_45{Deg_90 / 2.0f /* 45 degrees*/};
+    constexpr float Deg135{Deg_45 * 3.0f /*135 degrees*/};
+    constexpr float Deg_21{3.648638281e-01f /* 20~ 21 degrees*/};
+    constexpr float Deg_32{5.535743589e-01f /* 31~ 32 degrees*/};
+    constexpr float Deg_35{6.154797087e-01f /* 35~ 36 degrees*/};
+    constexpr float Deg_58{1.017221968e+00f /* 58~ 59 degrees*/};
+    constexpr float Deg_69{1.205932499e+00f /* 69~ 70 degrees*/};
+    constexpr float Deg111{1.935660155e+00f /*110~111 degrees*/};
+    constexpr float Deg122{2.124370686e+00f /*121~122 degrees*/};
+    static const AngularPoint AmbiPoints1O[]{
+        { EvRadians{ Deg_35}, AzRadians{-Deg_45} },
+        { EvRadians{ Deg_35}, AzRadians{-Deg135} },
+        { EvRadians{ Deg_35}, AzRadians{ Deg_45} },
+        { EvRadians{ Deg_35}, AzRadians{ Deg135} },
+        { EvRadians{-Deg_35}, AzRadians{-Deg_45} },
+        { EvRadians{-Deg_35}, AzRadians{-Deg135} },
+        { EvRadians{-Deg_35}, AzRadians{ Deg_45} },
+        { EvRadians{-Deg_35}, AzRadians{ Deg135} },
+    }, AmbiPoints2O[]{
+        { EvRadians{-Deg_32}, AzRadians{   0.0f} },
+        { EvRadians{   0.0f}, AzRadians{ Deg_58} },
+        { EvRadians{ Deg_58}, AzRadians{ Deg_90} },
+        { EvRadians{ Deg_32}, AzRadians{   0.0f} },
+        { EvRadians{   0.0f}, AzRadians{ Deg122} },
+        { EvRadians{-Deg_58}, AzRadians{-Deg_90} },
+        { EvRadians{-Deg_32}, AzRadians{ Deg180} },
+        { EvRadians{   0.0f}, AzRadians{-Deg122} },
+        { EvRadians{ Deg_58}, AzRadians{-Deg_90} },
+        { EvRadians{ Deg_32}, AzRadians{ Deg180} },
+        { EvRadians{   0.0f}, AzRadians{-Deg_58} },
+        { EvRadians{-Deg_58}, AzRadians{ Deg_90} },
+    }, AmbiPoints3O[]{
+        { EvRadians{ Deg_69}, AzRadians{-Deg_90} },
+        { EvRadians{ Deg_69}, AzRadians{ Deg_90} },
+        { EvRadians{-Deg_69}, AzRadians{-Deg_90} },
+        { EvRadians{-Deg_69}, AzRadians{ Deg_90} },
+        { EvRadians{   0.0f}, AzRadians{-Deg_69} },
+        { EvRadians{   0.0f}, AzRadians{-Deg111} },
+        { EvRadians{   0.0f}, AzRadians{ Deg_69} },
+        { EvRadians{   0.0f}, AzRadians{ Deg111} },
+        { EvRadians{ Deg_21}, AzRadians{   0.0f} },
+        { EvRadians{ Deg_21}, AzRadians{ Deg180} },
+        { EvRadians{-Deg_21}, AzRadians{   0.0f} },
+        { EvRadians{-Deg_21}, AzRadians{ Deg180} },
+        { EvRadians{ Deg_35}, AzRadians{-Deg_45} },
+        { EvRadians{ Deg_35}, AzRadians{-Deg135} },
+        { EvRadians{ Deg_35}, AzRadians{ Deg_45} },
+        { EvRadians{ Deg_35}, AzRadians{ Deg135} },
+        { EvRadians{-Deg_35}, AzRadians{-Deg_45} },
+        { EvRadians{-Deg_35}, AzRadians{-Deg135} },
+        { EvRadians{-Deg_35}, AzRadians{ Deg_45} },
+        { EvRadians{-Deg_35}, AzRadians{ Deg135} },
+    };
+    static const float AmbiMatrix1O[][MaxAmbiChannels]{
+        { 1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f },
+        { 1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f },
+        { 1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f },
+        { 1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f },
+        { 1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f },
+        { 1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f },
+        { 1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f },
+        { 1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f },
+    }, AmbiMatrix2O[][MaxAmbiChannels]{
+        { 8.333333333e-02f,  0.000000000e+00f, -7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f, -1.443375673e-01f,  1.167715449e-01f, },
+        { 8.333333333e-02f, -1.227808683e-01f,  0.000000000e+00f,  7.588274978e-02f, -1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, },
+        { 8.333333333e-02f, -7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, },
+        { 8.333333333e-02f,  0.000000000e+00f,  7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f,  1.443375673e-01f,  1.167715449e-01f, },
+        { 8.333333333e-02f, -1.227808683e-01f,  0.000000000e+00f, -7.588274978e-02f,  1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, },
+        { 8.333333333e-02f,  7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, },
+        { 8.333333333e-02f,  0.000000000e+00f, -7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f,  1.443375673e-01f,  1.167715449e-01f, },
+        { 8.333333333e-02f,  1.227808683e-01f,  0.000000000e+00f, -7.588274978e-02f, -1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, },
+        { 8.333333333e-02f,  7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f,  1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, },
+        { 8.333333333e-02f,  0.000000000e+00f,  7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f, -1.443375673e-01f,  1.167715449e-01f, },
+        { 8.333333333e-02f,  1.227808683e-01f,  0.000000000e+00f,  7.588274978e-02f,  1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, },
+        { 8.333333333e-02f, -7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f,  1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, },
+    }, AmbiMatrix3O[][MaxAmbiChannels]{
+        { 5.000000000e-02f,  3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f,  6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f, -1.256118221e-01f,  0.000000000e+00f,  1.126112056e-01f,  7.944389175e-02f,  0.000000000e+00f,  2.421151497e-02f,  0.000000000e+00f, },
+        { 5.000000000e-02f, -3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f,  1.256118221e-01f,  0.000000000e+00f, -1.126112056e-01f,  7.944389175e-02f,  0.000000000e+00f,  2.421151497e-02f,  0.000000000e+00f, },
+        { 5.000000000e-02f,  3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f, -1.256118221e-01f,  0.000000000e+00f,  1.126112056e-01f, -7.944389175e-02f,  0.000000000e+00f, -2.421151497e-02f,  0.000000000e+00f, },
+        { 5.000000000e-02f, -3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f,  6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f,  1.256118221e-01f,  0.000000000e+00f, -1.126112056e-01f, -7.944389175e-02f,  0.000000000e+00f, -2.421151497e-02f,  0.000000000e+00f, },
+        { 5.000000000e-02f,  8.090169944e-02f,  0.000000000e+00f,  3.090169944e-02f,  6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f, -7.763237543e-02f,  0.000000000e+00f, -2.950836627e-02f,  0.000000000e+00f, -1.497759251e-01f,  0.000000000e+00f, -7.763237543e-02f, },
+        { 5.000000000e-02f,  8.090169944e-02f,  0.000000000e+00f, -3.090169944e-02f, -6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f, -7.763237543e-02f,  0.000000000e+00f, -2.950836627e-02f,  0.000000000e+00f,  1.497759251e-01f,  0.000000000e+00f,  7.763237543e-02f, },
+        { 5.000000000e-02f, -8.090169944e-02f,  0.000000000e+00f,  3.090169944e-02f, -6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f,  7.763237543e-02f,  0.000000000e+00f,  2.950836627e-02f,  0.000000000e+00f, -1.497759251e-01f,  0.000000000e+00f, -7.763237543e-02f, },
+        { 5.000000000e-02f, -8.090169944e-02f,  0.000000000e+00f, -3.090169944e-02f,  6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f,  7.763237543e-02f,  0.000000000e+00f,  2.950836627e-02f,  0.000000000e+00f,  1.497759251e-01f,  0.000000000e+00f,  7.763237543e-02f, },
+        { 5.000000000e-02f,  0.000000000e+00f,  3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f,  6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f,  3.034486645e-02f, -6.779013272e-02f,  1.659481923e-01f,  4.797944664e-02f, },
+        { 5.000000000e-02f,  0.000000000e+00f,  3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f, -6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f,  3.034486645e-02f,  6.779013272e-02f,  1.659481923e-01f, -4.797944664e-02f, },
+        { 5.000000000e-02f,  0.000000000e+00f, -3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f, -6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f, -3.034486645e-02f, -6.779013272e-02f, -1.659481923e-01f,  4.797944664e-02f, },
+        { 5.000000000e-02f,  0.000000000e+00f, -3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f,  6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f, -3.034486645e-02f,  6.779013272e-02f, -1.659481923e-01f, -4.797944664e-02f, },
+        { 5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f,  6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f,  6.338656910e-02f, -1.092600649e-02f, -7.364853795e-02f,  1.011266756e-01f, -7.086833869e-02f, -1.482646439e-02f, },
+        { 5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f, -6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f, -6.338656910e-02f, -1.092600649e-02f, -7.364853795e-02f, -1.011266756e-01f, -7.086833869e-02f,  1.482646439e-02f, },
+        { 5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f, -6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f, -6.338656910e-02f,  1.092600649e-02f, -7.364853795e-02f,  1.011266756e-01f, -7.086833869e-02f, -1.482646439e-02f, },
+        { 5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f,  6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f,  6.338656910e-02f,  1.092600649e-02f, -7.364853795e-02f, -1.011266756e-01f, -7.086833869e-02f,  1.482646439e-02f, },
+        { 5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f,  6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f, -6.338656910e-02f, -1.092600649e-02f,  7.364853795e-02f,  1.011266756e-01f,  7.086833869e-02f, -1.482646439e-02f, },
+        { 5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f, -6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f,  6.338656910e-02f, -1.092600649e-02f,  7.364853795e-02f, -1.011266756e-01f,  7.086833869e-02f,  1.482646439e-02f, },
+        { 5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f, -6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f,  6.338656910e-02f,  1.092600649e-02f,  7.364853795e-02f,  1.011266756e-01f,  7.086833869e-02f, -1.482646439e-02f, },
+        { 5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f,  6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f, -6.338656910e-02f,  1.092600649e-02f,  7.364853795e-02f, -1.011266756e-01f,  7.086833869e-02f,  1.482646439e-02f, },
+    };
+    static const float AmbiOrderHFGain1O[MaxAmbiOrder+1]{
+        /*ENRGY*/ 2.000000000e+00f, 1.154700538e+00f
+    }, AmbiOrderHFGain2O[MaxAmbiOrder+1]{
+        /*ENRGY*/ 1.825741858e+00f, 1.414213562e+00f, 7.302967433e-01f
+        /*AMP   1.000000000e+00f, 7.745966692e-01f, 4.000000000e-01f*/
+        /*RMS   9.128709292e-01f, 7.071067812e-01f, 3.651483717e-01f*/
+    }, AmbiOrderHFGain3O[MaxAmbiOrder+1]{
+        /*ENRGY 1.865086714e+00f, 1.606093894e+00f, 1.142055301e+00f, 5.683795528e-01f*/
+        /*AMP*/ 1.000000000e+00f, 8.611363116e-01f, 6.123336207e-01f, 3.047469850e-01f
+        /*RMS   8.340921354e-01f, 7.182670250e-01f, 5.107426573e-01f, 2.541870634e-01f*/
+    };
+
+    static_assert(al::size(AmbiPoints1O) == al::size(AmbiMatrix1O), "First-Order Ambisonic HRTF mismatch");
+    static_assert(al::size(AmbiPoints2O) == al::size(AmbiMatrix2O), "Second-Order Ambisonic HRTF mismatch");
+    static_assert(al::size(AmbiPoints3O) == al::size(AmbiMatrix3O), "Third-Order Ambisonic HRTF mismatch");
+
+    /* A 700hz crossover frequency provides tighter sound imaging at the sweet
+     * spot with ambisonic decoding, as the distance between the ears is closer
+     * to half this frequency wavelength, which is the optimal point where the
+     * response should change between optimizing phase vs volume. Normally this
+     * tighter imaging is at the cost of a smaller sweet spot, but since the
+     * listener is fixed in the center of the HRTF responses for the decoder,
+     * we don't have to worry about ever being out of the sweet spot.
+     *
+     * A better option here may be to have the head radius as part of the HRTF
+     * data set and calculate the optimal crossover frequency from that.
+     */
+    device->mXOverFreq = 700.0f;
+
+    /* Don't bother with HOA when using full HRTF rendering. Nothing needs it,
+     * and it eases the CPU/memory load.
+     */
+    device->mRenderMode = RenderMode::Hrtf;
+    uint ambi_order{1};
+    if(auto modeopt = device->configValue<std::string>(nullptr, "hrtf-mode"))
+    {
+        struct HrtfModeEntry {
+            char name[8];
+            RenderMode mode;
+            uint order;
+        };
+        static const HrtfModeEntry hrtf_modes[]{
+            { "full", RenderMode::Hrtf, 1 },
+            { "ambi1", RenderMode::Normal, 1 },
+            { "ambi2", RenderMode::Normal, 2 },
+            { "ambi3", RenderMode::Normal, 3 },
+        };
+
+        const char *mode{modeopt->c_str()};
+        if(al::strcasecmp(mode, "basic") == 0)
+        {
+            ERR("HRTF mode \"%s\" deprecated, substituting \"%s\"\n", mode, "ambi2");
+            mode = "ambi2";
+        }
+
+        auto match_entry = [mode](const HrtfModeEntry &entry) -> bool
+        { return al::strcasecmp(mode, entry.name) == 0; };
+        auto iter = std::find_if(std::begin(hrtf_modes), std::end(hrtf_modes), match_entry);
+        if(iter == std::end(hrtf_modes))
+            ERR("Unexpected hrtf-mode: %s\n", mode);
+        else
+        {
+            device->mRenderMode = iter->mode;
+            ambi_order = iter->order;
+        }
+    }
+    TRACE("%u%s order %sHRTF rendering enabled, using \"%s\"\n", ambi_order,
+        (((ambi_order%100)/10) == 1) ? "th" :
+        ((ambi_order%10) == 1) ? "st" :
+        ((ambi_order%10) == 2) ? "nd" :
+        ((ambi_order%10) == 3) ? "rd" : "th",
+        (device->mRenderMode == RenderMode::Hrtf) ? "+ Full " : "",
+        device->mHrtfName.c_str());
+
+    bool perHrirMin{false};
+    al::span<const AngularPoint> AmbiPoints{AmbiPoints1O};
+    const float (*AmbiMatrix)[MaxAmbiChannels]{AmbiMatrix1O};
+    al::span<const float,MaxAmbiOrder+1> AmbiOrderHFGain{AmbiOrderHFGain1O};
+    if(ambi_order >= 3)
+    {
+        perHrirMin = true;
+        AmbiPoints = AmbiPoints3O;
+        AmbiMatrix = AmbiMatrix3O;
+        AmbiOrderHFGain = AmbiOrderHFGain3O;
+    }
+    else if(ambi_order == 2)
+    {
+        AmbiPoints = AmbiPoints2O;
+        AmbiMatrix = AmbiMatrix2O;
+        AmbiOrderHFGain = AmbiOrderHFGain2O;
+    }
+    device->mAmbiOrder = ambi_order;
+    device->m2DMixing = false;
+
+    const size_t count{AmbiChannelsFromOrder(ambi_order)};
+    std::transform(AmbiIndex::FromACN().begin(), AmbiIndex::FromACN().begin()+count,
+        std::begin(device->Dry.AmbiMap),
+        [](const uint8_t &index) noexcept { return BFChannelConfig{1.0f, index}; }
+    );
+    AllocChannels(device, count, device->channelsFromFmt());
+
+    HrtfStore *Hrtf{device->mHrtf.get()};
+    auto hrtfstate = DirectHrtfState::Create(count);
+    hrtfstate->build(Hrtf, device->mIrSize, perHrirMin, AmbiPoints, AmbiMatrix, device->mXOverFreq,
+        AmbiOrderHFGain);
+    device->mHrtfState = std::move(hrtfstate);
+
+    InitNearFieldCtrl(device, Hrtf->mFields[0].distance, ambi_order, true);
+}
+
+void InitUhjPanning(ALCdevice *device)
+{
+    /* UHJ is always 2D first-order. */
+    constexpr size_t count{Ambi2DChannelsFromOrder(1)};
+
+    device->mAmbiOrder = 1;
+    device->m2DMixing = true;
+
+    auto acnmap_begin = AmbiIndex::FromFuMa2D().begin();
+    std::transform(acnmap_begin, acnmap_begin + count, std::begin(device->Dry.AmbiMap),
+        [](const uint8_t &acn) noexcept -> BFChannelConfig
+        { return BFChannelConfig{1.0f/AmbiScale::FromUHJ()[acn], acn}; });
+    AllocChannels(device, count, device->channelsFromFmt());
+}
+
+} // namespace
+
+void aluInitRenderer(ALCdevice *device, int hrtf_id, al::optional<StereoEncoding> stereomode)
+{
+    /* Hold the HRTF the device last used, in case it's used again. */
+    HrtfStorePtr old_hrtf{std::move(device->mHrtf)};
+
+    device->mHrtfState = nullptr;
+    device->mHrtf = nullptr;
+    device->mIrSize = 0;
+    device->mHrtfName.clear();
+    device->mXOverFreq = 400.0f;
+    device->m2DMixing = false;
+    device->mRenderMode = RenderMode::Normal;
+
+    if(device->FmtChans != DevFmtStereo)
+    {
+        old_hrtf = nullptr;
+        if(stereomode && *stereomode == StereoEncoding::Hrtf)
+            device->mHrtfStatus = ALC_HRTF_UNSUPPORTED_FORMAT_SOFT;
+
+        const char *layout{nullptr};
+        switch(device->FmtChans)
+        {
+        case DevFmtQuad: layout = "quad"; break;
+        case DevFmtX51: layout = "surround51"; break;
+        case DevFmtX61: layout = "surround61"; break;
+        case DevFmtX71: layout = "surround71"; break;
+        case DevFmtX714: layout = "surround714"; break;
+        case DevFmtX3D71: layout = "surround3d71"; break;
+        /* Mono, Stereo, and Ambisonics output don't use custom decoders. */
+        case DevFmtMono:
+        case DevFmtStereo:
+        case DevFmtAmbi3D:
+            break;
+        }
+
+        std::unique_ptr<DecoderConfig<DualBand,MAX_OUTPUT_CHANNELS>> decoder_store;
+        DecoderView decoder{};
+        float speakerdists[MAX_OUTPUT_CHANNELS]{};
+        auto load_config = [device,&decoder_store,&decoder,&speakerdists](const char *config)
+        {
+            AmbDecConf conf{};
+            if(auto err = conf.load(config))
+            {
+                ERR("Failed to load layout file %s\n", config);
+                ERR("  %s\n", err->c_str());
+            }
+            else if(conf.NumSpeakers > MAX_OUTPUT_CHANNELS)
+                ERR("Unsupported decoder speaker count %zu (max %d)\n", conf.NumSpeakers,
+                    MAX_OUTPUT_CHANNELS);
+            else if(conf.ChanMask > Ambi3OrderMask)
+                ERR("Unsupported decoder channel mask 0x%04x (max 0x%x)\n", conf.ChanMask,
+                    Ambi3OrderMask);
+            else
+            {
+                device->mXOverFreq = clampf(conf.XOverFreq, 100.0f, 1000.0f);
+
+                decoder_store = std::make_unique<DecoderConfig<DualBand,MAX_OUTPUT_CHANNELS>>();
+                decoder = MakeDecoderView(device, &conf, *decoder_store);
+                for(size_t i{0};i < decoder.mChannels.size();++i)
+                    speakerdists[i] = conf.Speakers[i].Distance;
+            }
+        };
+        if(layout)
+        {
+            if(auto decopt = device->configValue<std::string>("decoder", layout))
+                load_config(decopt->c_str());
+        }
+
+        /* Enable the stablizer only for formats that have front-left, front-
+         * right, and front-center outputs.
+         */
+        const bool stablize{device->RealOut.ChannelIndex[FrontCenter] != InvalidChannelIndex
+            && device->RealOut.ChannelIndex[FrontLeft] != InvalidChannelIndex
+            && device->RealOut.ChannelIndex[FrontRight] != InvalidChannelIndex
+            && device->getConfigValueBool(nullptr, "front-stablizer", false) != 0};
+        const bool hqdec{device->getConfigValueBool("decoder", "hq-mode", true) != 0};
+        InitPanning(device, hqdec, stablize, decoder);
+        if(decoder)
+        {
+            float accum_dist{0.0f}, spkr_count{0.0f};
+            for(auto dist : speakerdists)
+            {
+                if(dist > 0.0f)
+                {
+                    accum_dist += dist;
+                    spkr_count += 1.0f;
+                }
+            }
+
+            const float avg_dist{(accum_dist > 0.0f && spkr_count > 0) ? accum_dist/spkr_count :
+                device->configValue<float>("decoder", "speaker-dist").value_or(1.0f)};
+            InitNearFieldCtrl(device, avg_dist, decoder.mOrder, decoder.mIs3D);
+
+            if(spkr_count > 0)
+                InitDistanceComp(device, decoder.mChannels, speakerdists);
+        }
+        if(auto *ambidec{device->AmbiDecoder.get()})
+        {
+            device->PostProcess = ambidec->hasStablizer() ? &ALCdevice::ProcessAmbiDecStablized
+                : &ALCdevice::ProcessAmbiDec;
+        }
+        return;
+    }
+
+
+    /* If HRTF is explicitly requested, or if there's no explicit request and
+     * the device is headphones, try to enable it.
+     */
+    if(stereomode.value_or(StereoEncoding::Default) == StereoEncoding::Hrtf
+        || (!stereomode && device->Flags.test(DirectEar)))
+    {
+        if(device->mHrtfList.empty())
+            device->enumerateHrtfs();
+
+        if(hrtf_id >= 0 && static_cast<uint>(hrtf_id) < device->mHrtfList.size())
+        {
+            const std::string &hrtfname = device->mHrtfList[static_cast<uint>(hrtf_id)];
+            if(HrtfStorePtr hrtf{GetLoadedHrtf(hrtfname, device->Frequency)})
+            {
+                device->mHrtf = std::move(hrtf);
+                device->mHrtfName = hrtfname;
+            }
+        }
+
+        if(!device->mHrtf)
+        {
+            for(const auto &hrtfname : device->mHrtfList)
+            {
+                if(HrtfStorePtr hrtf{GetLoadedHrtf(hrtfname, device->Frequency)})
+                {
+                    device->mHrtf = std::move(hrtf);
+                    device->mHrtfName = hrtfname;
+                    break;
+                }
+            }
+        }
+
+        if(device->mHrtf)
+        {
+            old_hrtf = nullptr;
+
+            HrtfStore *hrtf{device->mHrtf.get()};
+            device->mIrSize = hrtf->mIrSize;
+            if(auto hrtfsizeopt = device->configValue<uint>(nullptr, "hrtf-size"))
+            {
+                if(*hrtfsizeopt > 0 && *hrtfsizeopt < device->mIrSize)
+                    device->mIrSize = maxu(*hrtfsizeopt, MinIrLength);
+            }
+
+            InitHrtfPanning(device);
+            device->PostProcess = &ALCdevice::ProcessHrtf;
+            device->mHrtfStatus = ALC_HRTF_ENABLED_SOFT;
+            return;
+        }
+    }
+    old_hrtf = nullptr;
+
+    if(stereomode.value_or(StereoEncoding::Default) == StereoEncoding::Uhj)
+    {
+        switch(UhjEncodeQuality)
+        {
+        case UhjQualityType::IIR:
+            device->mUhjEncoder = std::make_unique<UhjEncoderIIR>();
+            break;
+        case UhjQualityType::FIR256:
+            device->mUhjEncoder = std::make_unique<UhjEncoder<UhjLength256>>();
+            break;
+        case UhjQualityType::FIR512:
+            device->mUhjEncoder = std::make_unique<UhjEncoder<UhjLength512>>();
+            break;
+        }
+        assert(device->mUhjEncoder != nullptr);
+
+        TRACE("UHJ enabled\n");
+        InitUhjPanning(device);
+        device->PostProcess = &ALCdevice::ProcessUhj;
+        return;
+    }
+
+    device->mRenderMode = RenderMode::Pairwise;
+    if(device->Type != DeviceType::Loopback)
+    {
+        if(auto cflevopt = device->configValue<int>(nullptr, "cf_level"))
+        {
+            if(*cflevopt > 0 && *cflevopt <= 6)
+            {
+                device->Bs2b = std::make_unique<bs2b>();
+                bs2b_set_params(device->Bs2b.get(), *cflevopt,
+                    static_cast<int>(device->Frequency));
+                TRACE("BS2B enabled\n");
+                InitPanning(device);
+                device->PostProcess = &ALCdevice::ProcessBs2b;
+                return;
+            }
+        }
+    }
+
+    TRACE("Stereo rendering\n");
+    InitPanning(device);
+    device->PostProcess = &ALCdevice::ProcessAmbiDec;
+}
+
+
+void aluInitEffectPanning(EffectSlot *slot, ALCcontext *context)
+{
+    DeviceBase *device{context->mDevice};
+    const size_t count{AmbiChannelsFromOrder(device->mAmbiOrder)};
+
+    slot->mWetBuffer.resize(count);
+
+    auto acnmap_begin = AmbiIndex::FromACN().begin();
+    auto iter = std::transform(acnmap_begin, acnmap_begin + count, slot->Wet.AmbiMap.begin(),
+        [](const uint8_t &acn) noexcept -> BFChannelConfig
+        { return BFChannelConfig{1.0f, acn}; });
+    std::fill(iter, slot->Wet.AmbiMap.end(), BFChannelConfig{});
+    slot->Wet.Buffer = slot->mWetBuffer;
+}
diff --git a/alsoftrc.sample b/alsoftrc.sample
new file mode 100644 (file)
index 0000000..2906cca
--- /dev/null
@@ -0,0 +1,666 @@
+# OpenAL config file.
+#
+# Option blocks may appear multiple times, and duplicated options will take the
+# last value specified. Environment variables may be specified within option
+# values, and are automatically substituted when the config file is loaded.
+# Environment variable names may only contain alpha-numeric characters (a-z,
+# A-Z, 0-9) and underscores (_), and are prefixed with $. For example,
+# specifying "$HOME/file.ext" would typically result in something like
+# "/home/user/file.ext". To specify an actual "$" character, use "$$".
+#
+# Device-specific values may be specified by including the device name in the
+# block name, with "general" replaced by the device name. That is, general
+# options for the device "Name of Device" would be in the [Name of Device]
+# block, while ALSA options would be in the [alsa/Name of Device] block.
+# Options marked as "(global)" are not influenced by the device.
+#
+# The system-wide settings can be put in /etc/xdg/alsoft.conf (as determined by
+# the XDG_CONFIG_DIRS env var list, /etc/xdg being the default if unset) and
+# user-specific override settings in $HOME/.config/alsoft.conf (as determined
+# by the XDG_CONFIG_HOME env var).
+#
+# For Windows, these settings should go into $AppData\alsoft.ini
+#
+# An additional configuration file (alsoft.ini on Windows, alsoft.conf on other
+# OSs) can be placed alongside the process executable for app-specific config
+# settings.
+#
+# Option and block names are case-senstive. The supplied values are only hints
+# and may not be honored (though generally it'll try to get as close as
+# possible). Note: options that are left unset may default to app- or system-
+# specified values. These are the current available settings:
+
+##
+## General stuff
+##
+[general]
+
+## disable-cpu-exts: (global)
+#  Disables use of specialized methods that use specific CPU intrinsics.
+#  Certain methods may utilize CPU extensions for improved performance, and
+#  this option is useful for preventing some or all of those methods from being
+#  used. The available extensions are: sse, sse2, sse3, sse4.1, and neon.
+#  Specifying 'all' disables use of all such specialized methods.
+#disable-cpu-exts =
+
+## drivers: (global)
+#  Sets the backend driver list order, comma-seperated. Unknown backends and
+#  duplicated names are ignored. Unlisted backends won't be considered for use
+#  unless the list is ended with a comma (e.g. 'oss,' will try OSS first before
+#  other backends, while 'oss' will try OSS only). Backends prepended with -
+#  won't be considered for use (e.g. '-oss,' will try all available backends
+#  except OSS). An empty list means to try all backends.
+#drivers =
+
+## channels:
+#  Sets the default output channel configuration. If left unspecified, one will
+#  try to be detected from the system, with a fallback to stereo. The available
+#  values are: mono, stereo, quad, surround51, surround61, surround71,
+#  surround3d71, ambi1, ambi2, ambi3. Note that the ambi* configurations output
+#  ambisonic channels of the given order (using ACN ordering and SN3D
+#  normalization by default), which need to be decoded to play correctly on
+#  speakers.
+#channels =
+
+## sample-type:
+#  Sets the default output sample type. Currently, all mixing is done with
+#  32-bit float and converted to the output sample type as needed. Available
+#  values are:
+#  int8    - signed 8-bit int
+#  uint8   - unsigned 8-bit int
+#  int16   - signed 16-bit int
+#  uint16  - unsigned 16-bit int
+#  int32   - signed 32-bit int
+#  uint32  - unsigned 32-bit int
+#  float32 - 32-bit float
+#sample-type = float32
+
+## frequency:
+#  Sets the default output frequency. If left unspecified it will try to detect
+#  a default from the system, otherwise it will fallback to 48000.
+#frequency =
+
+## period_size:
+#  Sets the update period size, in sample frames. This is the number of frames
+#  needed for each mixing update. Acceptable values range between 64 and 8192.
+#  If left unspecified it will default to 1/50th of the frequency (20ms, or 882
+#  for 44100, 960 for 48000, etc).
+#period_size =
+
+## periods:
+#  Sets the number of update periods. Higher values create a larger mix ahead,
+#  which helps protect against skips when the CPU is under load, but increases
+#  the delay between a sound getting mixed and being heard. Acceptable values
+#  range between 2 and 16.
+#periods = 3
+
+## stereo-mode:
+#  Specifies if stereo output is treated as being headphones or speakers. With
+#  headphones, HRTF or crossfeed filters may be used for better audio quality.
+#  Valid settings are auto, speakers, and headphones.
+#stereo-mode = auto
+
+## stereo-encoding:
+#  Specifies the default encoding method for stereo output. Valid values are:
+#  basic - Standard amplitude panning (aka pair-wise, stereo pair, etc) between
+#          -30 and +30 degrees.
+#  uhj - Creates a stereo-compatible two-channel UHJ mix, which encodes some
+#        surround sound information into stereo output that can be decoded with
+#        a surround sound receiver.
+#  hrtf - Uses filters to provide better spatialization of sounds while using
+#         stereo headphones.
+#  If crossfeed filters are used, basic stereo mixing is used.
+#stereo-encoding = basic
+
+## ambi-format:
+#  Specifies the channel order and normalization for the "ambi*" set of channel
+#  configurations. Valid settings are: fuma, acn+fuma, ambix (or acn+sn3d), or
+#  acn+n3d
+#ambi-format = ambix
+
+## hrtf:
+#  Deprecated. Consider using stereo-encoding instead. Valid values are auto,
+#  off, and on.
+#hrtf = auto
+
+## hrtf-mode:
+#  Specifies the rendering mode for HRTF processing. Setting the mode to full
+#  (default) applies a unique HRIR filter to each source given its relative
+#  location, providing the clearest directional response at the cost of the
+#  highest CPU usage. Setting the mode to ambi1, ambi2, or ambi3 will instead
+#  mix to a first-, second-, or third-order ambisonic buffer respectively, then
+#  decode that buffer with HRTF filters. Ambi1 has the lowest CPU usage,
+#  replacing the per-source HRIR filter for a simple 4-channel panning mix, but
+#  retains full 3D placement at the cost of a more diffuse response. Ambi2 and
+#  ambi3 increasingly improve the directional clarity, at the cost of more CPU
+#  usage (still less than "full", given some number of active sources).
+#hrtf-mode = full
+
+## hrtf-size:
+#  Specifies the impulse response size, in samples, for the HRTF filter. Larger
+#  values increase the filter quality, while smaller values reduce processing
+#  cost. A value of 0 (default) uses the full filter size in the dataset, and
+#  the default dataset has a filter size of 64 samples at 48khz.
+#hrtf-size = 0
+
+## default-hrtf:
+#  Specifies the default HRTF to use. When multiple HRTFs are available, this
+#  determines the preferred one to use if none are specifically requested. Note
+#  that this is the enumerated HRTF name, not necessarily the filename.
+#default-hrtf =
+
+## hrtf-paths:
+#  Specifies a comma-separated list of paths containing HRTF data sets. The
+#  format of the files are described in docs/hrtf.txt. The files within the
+#  directories must have the .mhr file extension to be recognized. By default,
+#  OS-dependent data paths will be used. They will also be used if the list
+#  ends with a comma. On Windows this is:
+#  $AppData\openal\hrtf
+#  And on other systems, it's (in order):
+#  $XDG_DATA_HOME/openal/hrtf  (defaults to $HOME/.local/share/openal/hrtf)
+#  $XDG_DATA_DIRS/openal/hrtf  (defaults to /usr/local/share/openal/hrtf and
+#                               /usr/share/openal/hrtf)
+#hrtf-paths =
+
+## cf_level:
+#  Sets the crossfeed level for stereo output. Valid values are:
+#  0 - No crossfeed
+#  1 - Low crossfeed
+#  2 - Middle crossfeed
+#  3 - High crossfeed (virtual speakers are closer to itself)
+#  4 - Low easy crossfeed
+#  5 - Middle easy crossfeed
+#  6 - High easy crossfeed
+#  Users of headphones may want to try various settings. Has no effect on non-
+#  stereo modes.
+#cf_level = 0
+
+## resampler: (global)
+#  Selects the default resampler used when mixing sources. Valid values are:
+#  point - nearest sample, no interpolation
+#  linear - extrapolates samples using a linear slope between samples
+#  cubic - extrapolates samples using a Catmull-Rom spline
+#  bsinc12 - extrapolates samples using a band-limited Sinc filter (varying
+#            between 12 and 24 points, with anti-aliasing)
+#  fast_bsinc12 - same as bsinc12, except without interpolation between down-
+#                 sampling scales
+#  bsinc24 - extrapolates samples using a band-limited Sinc filter (varying
+#            between 24 and 48 points, with anti-aliasing)
+#  fast_bsinc24 - same as bsinc24, except without interpolation between down-
+#                 sampling scales
+#resampler = cubic
+
+## rt-prio: (global)
+#  Sets the real-time priority value for the mixing thread. Not all drivers may
+#  use this (eg. PortAudio) as those APIs already control the priority of the
+#  mixing thread. 0 and negative values will disable real-time priority. Note
+#  that this may constitute a security risk since a real-time priority thread
+#  can indefinitely block normal-priority threads if it fails to wait. Disable
+#  this if it turns out to be a problem.
+#rt-prio = 1
+
+## rt-time-limit: (global)
+#  On non-Windows systems, allows reducing the process's RLIMIT_RTTIME resource
+#  as necessary for acquiring real-time priority from RTKit.
+#rt-time-limit = true
+
+## sources:
+#  Sets the maximum number of allocatable sources. Lower values may help for
+#  systems with apps that try to play more sounds than the CPU can handle.
+#sources = 256
+
+## slots:
+#  Sets the maximum number of Auxiliary Effect Slots an app can create. A slot
+#  can use a non-negligible amount of CPU time if an effect is set on it even
+#  if no sources are feeding it, so this may help when apps use more than the
+#  system can handle.
+#slots = 64
+
+## sends:
+#  Limits the number of auxiliary sends allowed per source. Setting this higher
+#  than the default has no effect.
+#sends = 6
+
+## front-stablizer:
+#  Applies filters to "stablize" front sound imaging. A psychoacoustic method
+#  is used to generate a front-center channel signal from the front-left and
+#  front-right channels, improving the front response by reducing the combing
+#  artifacts and phase errors. Consequently, it will only work with channel
+#  configurations that include front-left, front-right, and front-center.
+#front-stablizer = false
+
+## output-limiter:
+#  Applies a gain limiter on the final mixed output. This reduces the volume
+#  when the output samples would otherwise clamp, avoiding excessive clipping
+#  noise.
+#output-limiter = true
+
+## dither:
+#  Applies dithering on the final mix, for 8- and 16-bit output by default.
+#  This replaces the distortion created by nearest-value quantization with low-
+#  level whitenoise.
+#dither = true
+
+## dither-depth:
+#  Quantization bit-depth for dithered output. A value of 0 (or less) will
+#  match the output sample depth. For int32, uint32, and float32 output, 0 will
+#  disable dithering because they're at or beyond the rendered precision. The
+#  maximum dither depth is 24.
+#dither-depth = 0
+
+## volume-adjust:
+#  A global volume adjustment for source output, expressed in decibels. The
+#  value is logarithmic, so +6 will be a scale of (approximately) 2x, +12 will
+#  be a scale of 4x, etc. Similarly, -6 will be x1/2, and -12 is about x1/4. A
+#  value of 0 means no change.
+#volume-adjust = 0
+
+## excludefx: (global)
+#  Sets which effects to exclude, preventing apps from using them. This can
+#  help for apps that try to use effects which are too CPU intensive for the
+#  system to handle. Available effects are: eaxreverb,reverb,autowah,chorus,
+#  compressor,distortion,echo,equalizer,flanger,modulator,dedicated,pshifter,
+#  fshifter,vmorpher.
+#excludefx =
+
+## default-reverb: (global)
+#  A reverb preset that applies by default to all sources on send 0
+#  (applications that set their own slots on send 0 will override this).
+#  Available presets include: None, Generic, PaddedCell, Room, Bathroom,
+#  Livingroom, Stoneroom, Auditorium, ConcertHall, Cave, Arena, Hangar,
+#  CarpetedHallway, Hallway, StoneCorridor, Alley, Forest, City, Mountains,
+#  Quarry, Plain, ParkingLot, SewerPipe, Underwater, Drugged, Dizzy, Psychotic.
+#default-reverb =
+
+## trap-alc-error: (global)
+#  Generates a SIGTRAP signal when an ALC device error is generated, on systems
+#  that support it. This helps when debugging, while trying to find the cause
+#  of a device error. On Windows, a breakpoint exception is generated.
+#trap-alc-error = false
+
+## trap-al-error: (global)
+#  Generates a SIGTRAP signal when an AL context error is generated, on systems
+#  that support it. This helps when debugging, while trying to find the cause
+#  of a context error. On Windows, a breakpoint exception is generated.
+#trap-al-error = false
+
+##
+## Ambisonic decoder stuff
+##
+[decoder]
+
+## hq-mode:
+#  Enables a high-quality ambisonic decoder. This mode is capable of frequency-
+#  dependent processing, creating a better reproduction of 3D sound rendering
+#  over surround sound speakers.
+#hq-mode = true
+
+## distance-comp:
+#  Enables compensation for the speakers' relative distances to the listener.
+#  This applies the necessary delays and attenuation to make the speakers
+#  behave as though they are all equidistant, which is important for proper
+#  playback of 3D sound rendering. Requires the proper distances to be
+#  specified in the decoder configuration file.
+#distance-comp = true
+
+## nfc:
+#  Enables near-field control filters. This simulates and compensates for low-
+#  frequency effects caused by the curvature of nearby sound-waves, which
+#  creates a more realistic perception of sound distance with surround sound
+#  output. Note that the effect may be stronger or weaker than intended if the
+#  application doesn't use or specify an appropriate unit scale, or if
+#  incorrect speaker distances are set. For HRTF output, hrtf-mode must be set
+#  to one of the ambi* values for this to function.
+#nfc = false
+
+## speaker-dist:
+#  Specifies the speaker distance in meters, used by the near-field control
+#  filters with surround sound output. For ambisonic output modes, this value
+#  is the basis for the NFC-HOA Reference Delay parameter (calculated as
+#  delay_seconds = speaker_dist/343.3). This value is not used when a decoder
+#  configuration is set for the output mode (since they specify the per-speaker
+#  distances, overriding this setting), or when the NFC filters are off. Valid
+#  values range from 0.1 to 10.
+#speaker-dist = 1
+
+## quad:
+#  Decoder configuration file for Quadraphonic channel output. See
+#  docs/ambdec.txt for a description of the file format.
+#quad =
+
+## surround51:
+#  Decoder configuration file for 5.1 Surround (Side and Rear) channel output.
+#  See docs/ambdec.txt for a description of the file format.
+#surround51 =
+
+## surround61:
+#  Decoder configuration file for 6.1 Surround channel output. See
+#  docs/ambdec.txt for a description of the file format.
+#surround61 =
+
+## surround71:
+#  Decoder configuration file for 7.1 Surround channel output. See
+#  docs/ambdec.txt for a description of the file format.
+#surround71 =
+
+## surround3d71:
+#  Decoder configuration file for 3D7.1 Surround channel output. See
+#  docs/ambdec.txt for a description of the file format. See also
+#  docs/3D7.1.txt for information about 3D7.1.
+#surround3d71 =
+
+##
+## UHJ and Super Stereo stuff
+##
+[uhj]
+
+## decode-filter: (global)
+#  Specifies the all-pass filter type for UHJ decoding and Super Stereo
+#  processing. Valid values are:
+#  iir - utilizes dual IIR filters, providing a wide pass-band with low CPU
+#        use, but causes additional phase shifts on the signal.
+#  fir256 - utilizes a 256-point FIR filter, providing more stable results but
+#           exhibiting attenuation in the lower and higher frequency bands.
+#  fir512 - utilizes a 512-point FIR filter, providing a wider pass-band than
+#           fir256, at the cost of more CPU use.
+#decode-filter = iir
+
+## encode-filter: (global)
+#  Specifies the all-pass filter type for UHJ output encoding. Valid values are
+#  the same as for decode-filter.
+#encode-filter = iir
+
+##
+## Reverb effect stuff (includes EAX reverb)
+##
+[reverb]
+
+## boost: (global)
+#  A global amplification for reverb output, expressed in decibels. The value
+#  is logarithmic, so +6 will be a scale of (approximately) 2x, +12 will be a
+#  scale of 4x, etc. Similarly, -6 will be about half, and -12 about 1/4th. A
+#  value of 0 means no change.
+#boost = 0
+
+##
+## PipeWire backend stuff
+##
+[pipewire]
+
+## assume-audio: (global)
+#  Causes the backend to succeed initialization even if PipeWire reports no
+#  audio support. Currently, audio support is detected by the presence of audio
+#  source or sink nodes, although this can cause false negatives in cases where
+#  device availability during library initialization is spotty. Future versions
+#  of PipeWire are expected to have a more robust method to test audio support,
+#  but in the mean time this can be set to true to assume PipeWire has audio
+#  support even when no nodes may be reported at initialization time.
+#assume-audio = false
+
+## rt-mix:
+#  Renders samples directly in the real-time processing callback. This allows
+#  for lower latency and less overall CPU utilization, but can increase the
+#  risk of underruns when increasing the amount of work the mixer needs to do.
+#rt-mix = true
+
+##
+## PulseAudio backend stuff
+##
+[pulse]
+
+## spawn-server: (global)
+#  Attempts to autospawn a PulseAudio server whenever needed (initializing the
+#  backend, enumerating devices, etc). Setting autospawn to false in Pulse's
+#  client.conf will still prevent autospawning even if this is set to true.
+#spawn-server = false
+
+## allow-moves: (global)
+#  Allows PulseAudio to move active streams to different devices. Note that the
+#  device specifier (seen by applications) will not be updated when this
+#  occurs, and neither will the AL device configuration (sample rate, format,
+#  etc).
+#allow-moves = true
+
+## fix-rate:
+#  Specifies whether to match the playback stream's sample rate to the device's
+#  sample rate. Enabling this forces OpenAL Soft to mix sources and effects
+#  directly to the actual output rate, avoiding a second resample pass by the
+#  PulseAudio server.
+#fix-rate = false
+
+## adjust-latency:
+#  Attempts to adjust the overall latency of device playback. Note that this
+#  may have adverse effects on the resulting internal buffer sizes and mixing
+#  updates, leading to performance problems and drop-outs. However, if the
+#  PulseAudio server is creating a lot of latency, enabling this may help make
+#  it more manageable.
+#adjust-latency = false
+
+##
+## ALSA backend stuff
+##
+[alsa]
+
+## device: (global)
+#  Sets the device name for the default playback device.
+#device = default
+
+## device-prefix: (global)
+#  Sets the prefix used by the discovered (non-default) playback devices. This
+#  will be appended with "CARD=c,DEV=d", where c is the card id and d is the
+#  device index for the requested device name.
+#device-prefix = plughw:
+
+## device-prefix-*: (global)
+#  Card- and device-specific prefixes may be used to override the device-prefix
+#  option. The option may specify the card id (eg, device-prefix-NVidia), or
+#  the card id and device index (eg, device-prefix-NVidia-0). The card id is
+#  case-sensitive.
+#device-prefix- =
+
+## custom-devices: (global)
+#  Specifies a list of enumerated playback devices and the ALSA devices they
+#  refer to. The list pattern is "Display Name=ALSA device;...". The display
+#  names will be returned for device enumeration, and the ALSA device is the
+#  device name to open for each enumerated device.
+#custom-devices =
+
+## capture: (global)
+#  Sets the device name for the default capture device.
+#capture = default
+
+## capture-prefix: (global)
+#  Sets the prefix used by the discovered (non-default) capture devices. This
+#  will be appended with "CARD=c,DEV=d", where c is the card id and d is the
+#  device number for the requested device name.
+#capture-prefix = plughw:
+
+## capture-prefix-*: (global)
+#  Card- and device-specific prefixes may be used to override the
+#  capture-prefix option. The option may specify the card id (eg,
+#  capture-prefix-NVidia), or the card id and device index (eg,
+#  capture-prefix-NVidia-0). The card id is case-sensitive.
+#capture-prefix- =
+
+## custom-captures: (global)
+#  Specifies a list of enumerated capture devices and the ALSA devices they
+#  refer to. The list pattern is "Display Name=ALSA device;...". The display
+#  names will be returned for device enumeration, and the ALSA device is the
+#  device name to open for each enumerated device.
+#custom-captures =
+
+## mmap:
+#  Sets whether to try using mmap mode (helps reduce latencies and CPU
+#  consumption). If mmap isn't available, it will automatically fall back to
+#  non-mmap mode. True, yes, on, and non-0 values will attempt to use mmap. 0
+#  and anything else will force mmap off.
+#mmap = true
+
+## allow-resampler:
+#  Specifies whether to allow ALSA's built-in resampler. Enabling this will
+#  allow the playback device to be set to a different sample rate than the
+#  actual output, causing ALSA to apply its own resampling pass after OpenAL
+#  Soft resamples and mixes the sources and effects for output.
+#allow-resampler = false
+
+##
+## OSS backend stuff
+##
+[oss]
+
+## device: (global)
+#  Sets the device name for OSS output.
+#device = /dev/dsp
+
+## capture: (global)
+#  Sets the device name for OSS capture.
+#capture = /dev/dsp
+
+##
+## Solaris backend stuff
+##
+[solaris]
+
+## device: (global)
+#  Sets the device name for Solaris output.
+#device = /dev/audio
+
+##
+## QSA backend stuff
+##
+[qsa]
+
+##
+## JACK backend stuff
+##
+[jack]
+
+## spawn-server: (global)
+#  Attempts to autospawn a JACK server when initializing.
+#spawn-server = false
+
+## custom-devices: (global)
+#  Specifies a list of enumerated devices and the ports they connect to. The
+#  list pattern is "Display Name=ports regex;Display Name=ports regex;...". The
+#  display names will be returned for device enumeration, and the ports regex
+#  is the regular expression to identify the target ports on the server (as
+#  given by the jack_get_ports function) for each enumerated device.
+#custom-devices =
+
+## rt-mix:
+#  Renders samples directly in the real-time processing callback. This allows
+#  for lower latency and less overall CPU utilization, but can increase the
+#  risk of underruns when increasing the amount of work the mixer needs to do.
+#rt-mix = true
+
+## connect-ports:
+#  Attempts to automatically connect the client ports to physical server ports.
+#  Client ports that fail to connect will leave the remaining channels
+#  unconnected and silent (the device format won't change to accommodate).
+#connect-ports = true
+
+## buffer-size:
+#  Sets the update buffer size, in samples, that the backend will keep buffered
+#  to handle the server's real-time processing requests. This value must be a
+#  power of 2, or else it will be rounded up to the next power of 2. If it is
+#  less than JACK's buffer update size, it will be clamped. This option may
+#  be useful in case the server's update size is too small and doesn't give the
+#  mixer time to keep enough audio available for the processing requests.
+#  Ignored when rt-mix is true.
+#buffer-size = 0
+
+##
+## WASAPI backend stuff
+##
+[wasapi]
+
+## allow-resampler:
+#  Specifies whether to allow an extra resampler pass on the output. Enabling
+#  this will allow the playback device to be set to a different sample rate
+#  than the actual output can accept, causing the backend to apply its own
+#  resampling pass after OpenAL Soft mixes the sources and processes effects
+#  for output.
+#allow-resampler = true
+
+##
+## DirectSound backend stuff
+##
+[dsound]
+
+##
+## Windows Multimedia backend stuff
+##
+[winmm]
+
+##
+## PortAudio backend stuff
+##
+[port]
+
+## device: (global)
+#  Sets the device index for output. Negative values will use the default as
+#  given by PortAudio itself.
+#device = -1
+
+## capture: (global)
+#  Sets the device index for capture. Negative values will use the default as
+#  given by PortAudio itself.
+#capture = -1
+
+##
+## Wave File Writer stuff
+##
+[wave]
+
+## file: (global)
+#  Sets the filename of the wave file to write to. An empty name prevents the
+#  backend from opening, even when explicitly requested.
+#  THIS WILL OVERWRITE EXISTING FILES WITHOUT QUESTION!
+#file =
+
+## bformat: (global)
+#  Creates AMB format files using first-order ambisonics instead of a standard
+#  single- or multi-channel .wav file.
+#bformat = false
+
+##
+## EAX extensions stuff
+##
+[eax]
+
+## enable: (global)
+#  Sets whether to enable EAX extensions or not.
+#enable = true
+
+##
+## Per-game compatibility options (these should only be set in per-game config
+## files, *NOT* system- or user-level!)
+##
+[game_compat]
+
+## nfc-scale: (global)
+#  A meters-per-unit distance scale applied to NFC filters. If a game doesn't
+#  use real-world meters for in-game units, the filters may create a too-near
+#  or too-distant effect. For instance, if the game uses 1 foot per unit, a
+#  value of 0.3048 will correctly adjust the filters. Or if the game uses 1
+#  kilometer per unit, a value of 1000 will correctly adjust the filters.
+#nfc-scale = 1
+
+## enable-sub-data-ext: (global)
+#  Enables the AL_SOFT_buffer_sub_data extension, disabling the
+#  AL_EXT_SOURCE_RADIUS extension. These extensions are incompatible, so only
+#  one can be available. The latter extension is more commonly used, but this
+#  option can be enabled for older apps that want the former extension.
+#enable-sub-data-ext = false
+
+## reverse-x: (global)
+#  Reverses the local X (left-right) position of 3D sound sources.
+#reverse-x = false
+
+## reverse-y: (global)
+#  Reverses the local Y (up-down) position of 3D sound sources.
+#reverse-y = false
+
+## reverse-z: (global)
+#  Reverses the local Z (front-back) position of 3D sound sources.
+#reverse-z = false
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644 (file)
index 0000000..aa155af
--- /dev/null
@@ -0,0 +1,21 @@
+version: 1.23.1.{build}
+
+environment:
+    APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
+    GEN: "Visual Studio 15 2017"
+    matrix:
+      - ARCH: Win32
+        CFG: Release
+      - ARCH: x64
+        CFG: Release
+
+after_build:
+- 7z a ..\soft_oal.zip "%APPVEYOR_BUILD_FOLDER%\build\%CFG%\soft_oal.dll" "%APPVEYOR_BUILD_FOLDER%\README.md" "%APPVEYOR_BUILD_FOLDER%\COPYING"
+
+artifacts:
+- path: soft_oal.zip
+
+build_script:
+    - cd build
+    - cmake -G "%GEN%" -A %ARCH% -DALSOFT_BUILD_ROUTER=ON -DALSOFT_REQUIRE_WINMM=ON -DALSOFT_REQUIRE_DSOUND=ON -DALSOFT_REQUIRE_WASAPI=ON -DALSOFT_EMBED_HRTF_DATA=YES ..
+    - cmake --build . --config %CFG% --clean-first
diff --git a/cmake/FindALSA.cmake b/cmake/FindALSA.cmake
new file mode 100644 (file)
index 0000000..519304d
--- /dev/null
@@ -0,0 +1,73 @@
+# - Find alsa
+# Find the alsa libraries (asound)
+#
+#  This module defines the following variables:
+#     ALSA_FOUND       - True if ALSA_INCLUDE_DIR & ALSA_LIBRARY are found
+#     ALSA_LIBRARIES   - Set when ALSA_LIBRARY is found
+#     ALSA_INCLUDE_DIRS - Set when ALSA_INCLUDE_DIR is found
+#
+#     ALSA_INCLUDE_DIR - where to find asoundlib.h, etc.
+#     ALSA_LIBRARY     - the asound library
+#     ALSA_VERSION_STRING - the version of alsa found (since CMake 2.8.8)
+#
+
+#=============================================================================
+# Copyright 2009-2011 Kitware, Inc.
+# Copyright 2009-2011 Philip Lowman <philip@yhbt.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#  * Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+#  * Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+#  * The names of Kitware, Inc., the Insight Consortium, or the names of
+#    any consortium members, or of any contributors, may not be used to
+#    endorse or promote products derived from this software without
+#    specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#=============================================================================
+
+find_path(ALSA_INCLUDE_DIR NAMES alsa/asoundlib.h
+          DOC "The ALSA (asound) include directory"
+)
+
+find_library(ALSA_LIBRARY NAMES asound
+             DOC "The ALSA (asound) library"
+)
+
+if(ALSA_INCLUDE_DIR AND EXISTS "${ALSA_INCLUDE_DIR}/alsa/version.h")
+  file(STRINGS "${ALSA_INCLUDE_DIR}/alsa/version.h" alsa_version_str REGEX "^#define[\t ]+SND_LIB_VERSION_STR[\t ]+\".*\"")
+
+  string(REGEX REPLACE "^.*SND_LIB_VERSION_STR[\t ]+\"([^\"]*)\".*$" "\\1" ALSA_VERSION_STRING "${alsa_version_str}")
+  unset(alsa_version_str)
+endif()
+
+# handle the QUIETLY and REQUIRED arguments and set ALSA_FOUND to TRUE if
+# all listed variables are TRUE
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(ALSA
+                                  REQUIRED_VARS ALSA_LIBRARY ALSA_INCLUDE_DIR
+                                  VERSION_VAR ALSA_VERSION_STRING)
+
+if(ALSA_FOUND)
+  set( ALSA_LIBRARIES ${ALSA_LIBRARY} )
+  set( ALSA_INCLUDE_DIRS ${ALSA_INCLUDE_DIR} )
+endif()
+
+mark_as_advanced(ALSA_INCLUDE_DIR ALSA_LIBRARY)
diff --git a/cmake/FindAudioIO.cmake b/cmake/FindAudioIO.cmake
new file mode 100644 (file)
index 0000000..f0f8b2a
--- /dev/null
@@ -0,0 +1,21 @@
+# - Find AudioIO includes and libraries
+#
+#   AUDIOIO_FOUND        - True if AUDIOIO_INCLUDE_DIR is found
+#   AUDIOIO_INCLUDE_DIRS - Set when AUDIOIO_INCLUDE_DIR is found
+#
+#   AUDIOIO_INCLUDE_DIR - where to find sys/audioio.h, etc.
+#
+
+find_path(AUDIOIO_INCLUDE_DIR
+          NAMES sys/audioio.h
+          DOC "The AudioIO include directory"
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(AudioIO  REQUIRED_VARS AUDIOIO_INCLUDE_DIR)
+
+if(AUDIOIO_FOUND)
+    set(AUDIOIO_INCLUDE_DIRS ${AUDIOIO_INCLUDE_DIR})
+endif()
+
+mark_as_advanced(AUDIOIO_INCLUDE_DIR)
diff --git a/cmake/FindFFmpeg.cmake b/cmake/FindFFmpeg.cmake
new file mode 100644 (file)
index 0000000..26ed4d2
--- /dev/null
@@ -0,0 +1,157 @@
+# vim: ts=2 sw=2
+# - Try to find the required ffmpeg components(default: AVFORMAT, AVUTIL, AVCODEC)
+#
+# Once done this will define
+#  FFMPEG_FOUND         - System has the all required components.
+#  FFMPEG_INCLUDE_DIRS  - Include directory necessary for using the required components headers.
+#  FFMPEG_LIBRARIES     - Link these to use the required ffmpeg components.
+#  FFMPEG_DEFINITIONS   - Compiler switches required for using the required ffmpeg components.
+#
+# For each of the components it will additionaly set.
+#   - AVCODEC
+#   - AVDEVICE
+#   - AVFORMAT
+#   - AVUTIL
+#   - POSTPROC
+#   - SWSCALE
+#   - SWRESAMPLE
+# the following variables will be defined
+#  <component>_FOUND        - System has <component>
+#  <component>_INCLUDE_DIRS - Include directory necessary for using the <component> headers
+#  <component>_LIBRARIES    - Link these to use <component>
+#  <component>_DEFINITIONS  - Compiler switches required for using <component>
+#  <component>_VERSION      - The components version
+#
+# Copyright (c) 2006, Matthias Kretz, <kretz@kde.org>
+# Copyright (c) 2008, Alexander Neundorf, <neundorf@kde.org>
+# Copyright (c) 2011, Michael Jansen, <kde@michael-jansen.biz>
+#
+# Redistribution and use is allowed according to the terms of the BSD license.
+
+include(FindPackageHandleStandardArgs)
+
+if(NOT FFmpeg_FIND_COMPONENTS)
+    set(FFmpeg_FIND_COMPONENTS AVFORMAT AVCODEC AVUTIL)
+endif()
+
+#
+### Macro: set_component_found
+#
+# Marks the given component as found if both *_LIBRARIES AND *_INCLUDE_DIRS is present.
+#
+macro(set_component_found _component)
+    if(${_component}_LIBRARIES AND ${_component}_INCLUDE_DIRS)
+        # message(STATUS "  - ${_component} found.")
+        set(${_component}_FOUND TRUE)
+    else()
+        # message(STATUS "  - ${_component} not found.")
+    endif()
+endmacro()
+
+#
+### Macro: find_component
+#
+# Checks for the given component by invoking pkgconfig and then looking up the libraries and
+# include directories.
+#
+macro(find_component _component _pkgconfig _library _header)
+    if(NOT WIN32)
+        # use pkg-config to get the directories and then use these values
+        # in the FIND_PATH() and FIND_LIBRARY() calls
+        find_package(PkgConfig)
+        if(PKG_CONFIG_FOUND)
+            pkg_check_modules(PC_${_component} ${_pkgconfig})
+        endif()
+    endif()
+
+    find_path(${_component}_INCLUDE_DIRS ${_header}
+        HINTS
+            ${FFMPEGSDK_INC}
+            ${PC_LIB${_component}_INCLUDEDIR}
+            ${PC_LIB${_component}_INCLUDE_DIRS}
+        PATH_SUFFIXES
+            ffmpeg
+    )
+
+    find_library(${_component}_LIBRARIES NAMES ${_library}
+        HINTS
+            ${FFMPEGSDK_LIB}
+            ${PC_LIB${_component}_LIBDIR}
+            ${PC_LIB${_component}_LIBRARY_DIRS}
+    )
+
+    set(${_component}_VERSION     ${PC_${_component}_VERSION}      CACHE STRING "The ${_component} version number." FORCE)
+    set(${_component}_DEFINITIONS ${PC_${_component}_CFLAGS_OTHER} CACHE STRING "The ${_component} CFLAGS." FORCE)
+
+    set_component_found(${_component})
+
+    mark_as_advanced(
+        ${_component}_INCLUDE_DIRS
+        ${_component}_LIBRARIES
+        ${_component}_DEFINITIONS
+        ${_component}_VERSION)
+endmacro()
+
+
+set(FFMPEGSDK $ENV{FFMPEG_HOME})
+if(FFMPEGSDK)
+    set(FFMPEGSDK_INC "${FFMPEGSDK}/include")
+    set(FFMPEGSDK_LIB "${FFMPEGSDK}/lib")
+endif()
+
+# Check for all possible components.
+find_component(AVCODEC    libavcodec    avcodec    libavcodec/avcodec.h)
+find_component(AVFORMAT   libavformat   avformat   libavformat/avformat.h)
+find_component(AVDEVICE   libavdevice   avdevice   libavdevice/avdevice.h)
+find_component(AVUTIL     libavutil     avutil     libavutil/avutil.h)
+find_component(SWSCALE    libswscale    swscale    libswscale/swscale.h)
+find_component(SWRESAMPLE libswresample swresample libswresample/swresample.h)
+find_component(POSTPROC   libpostproc   postproc   libpostproc/postprocess.h)
+
+# Check if the required components were found and add their stuff to the FFMPEG_* vars.
+foreach(_component ${FFmpeg_FIND_COMPONENTS})
+    if(${_component}_FOUND)
+        # message(STATUS "Required component ${_component} present.")
+        set(FFMPEG_LIBRARIES   ${FFMPEG_LIBRARIES}   ${${_component}_LIBRARIES})
+        set(FFMPEG_DEFINITIONS ${FFMPEG_DEFINITIONS} ${${_component}_DEFINITIONS})
+        list(APPEND FFMPEG_INCLUDE_DIRS ${${_component}_INCLUDE_DIRS})
+    else()
+        # message(STATUS "Required component ${_component} missing.")
+    endif()
+endforeach()
+
+# Add libz if it exists (needed for static ffmpeg builds)
+find_library(_FFmpeg_HAVE_LIBZ NAMES z)
+if(_FFmpeg_HAVE_LIBZ)
+    set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${_FFmpeg_HAVE_LIBZ})
+endif()
+
+# Build the include path and library list with duplicates removed.
+if(FFMPEG_INCLUDE_DIRS)
+    list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS)
+endif()
+
+if(FFMPEG_LIBRARIES)
+    list(REMOVE_DUPLICATES FFMPEG_LIBRARIES)
+endif()
+
+# cache the vars.
+set(FFMPEG_INCLUDE_DIRS ${FFMPEG_INCLUDE_DIRS} CACHE STRING "The FFmpeg include directories." FORCE)
+set(FFMPEG_LIBRARIES    ${FFMPEG_LIBRARIES}    CACHE STRING "The FFmpeg libraries." FORCE)
+set(FFMPEG_DEFINITIONS  ${FFMPEG_DEFINITIONS}  CACHE STRING "The FFmpeg cflags." FORCE)
+
+mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARIES FFMPEG_DEFINITIONS)
+
+# Now set the noncached _FOUND vars for the components.
+foreach(_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWRESAMPLE SWSCALE)
+    set_component_found(${_component})
+endforeach ()
+
+# Compile the list of required vars
+set(_FFmpeg_REQUIRED_VARS FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS)
+foreach(_component ${FFmpeg_FIND_COMPONENTS})
+    list(APPEND _FFmpeg_REQUIRED_VARS ${_component}_LIBRARIES ${_component}_INCLUDE_DIRS)
+endforeach()
+
+# Give a nice error message if some of the required vars are missing.
+find_package_handle_standard_args(FFmpeg DEFAULT_MSG ${_FFmpeg_REQUIRED_VARS})
diff --git a/cmake/FindJACK.cmake b/cmake/FindJACK.cmake
new file mode 100644 (file)
index 0000000..b72fe3f
--- /dev/null
@@ -0,0 +1,60 @@
+# - Find JACK
+# Find the JACK libraries
+#
+#  This module defines the following variables:
+#     JACK_FOUND        - True if JACK_INCLUDE_DIR & JACK_LIBRARY are found
+#     JACK_INCLUDE_DIRS - where to find jack.h, etc.
+#     JACK_LIBRARIES    - the jack library
+#
+
+#=============================================================================
+# Copyright 2009-2011 Kitware, Inc.
+# Copyright 2009-2011 Philip Lowman <philip@yhbt.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#  * Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+#  * Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+#  * The names of Kitware, Inc., the Insight Consortium, or the names of
+#    any consortium members, or of any contributors, may not be used to
+#    endorse or promote products derived from this software without
+#    specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#=============================================================================
+
+find_path(JACK_INCLUDE_DIR NAMES jack/jack.h
+          DOC "The JACK include directory"
+)
+
+find_library(JACK_LIBRARY NAMES jack
+             DOC "The JACK library"
+)
+
+# handle the QUIETLY and REQUIRED arguments and set JACK_FOUND to TRUE if
+# all listed variables are TRUE
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(JACK REQUIRED_VARS JACK_LIBRARY JACK_INCLUDE_DIR)
+
+if(JACK_FOUND)
+    set(JACK_LIBRARIES ${JACK_LIBRARY})
+    set(JACK_INCLUDE_DIRS ${JACK_INCLUDE_DIR})
+endif()
+
+mark_as_advanced(JACK_INCLUDE_DIR JACK_LIBRARY)
diff --git a/cmake/FindMySOFA.cmake b/cmake/FindMySOFA.cmake
new file mode 100644 (file)
index 0000000..7d485c3
--- /dev/null
@@ -0,0 +1,81 @@
+# - Find MySOFA
+# Find the MySOFA libraries
+#
+#  This module defines the following variables:
+#     MYSOFA_FOUND        - True if MYSOFA_INCLUDE_DIR & MYSOFA_LIBRARY are found
+#     MYSOFA_INCLUDE_DIRS - where to find mysofa.h, etc.
+#     MYSOFA_LIBRARIES    - the MySOFA library
+#
+
+#=============================================================================
+# Copyright 2009-2011 Kitware, Inc.
+# Copyright 2009-2011 Philip Lowman <philip@yhbt.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#  * Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+#  * Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+#  * The names of Kitware, Inc., the Insight Consortium, or the names of
+#    any consortium members, or of any contributors, may not be used to
+#    endorse or promote products derived from this software without
+#    specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#=============================================================================
+
+find_package(ZLIB)
+
+find_path(MYSOFA_INCLUDE_DIR NAMES mysofa.h
+          DOC "The MySOFA include directory"
+)
+
+find_library(MYSOFA_LIBRARY NAMES mysofa
+             DOC "The MySOFA library"
+)
+
+find_library(MYSOFA_M_LIBRARY NAMES m
+             DOC "The math library for MySOFA"
+)
+
+# handle the QUIETLY and REQUIRED arguments and set MYSOFA_FOUND to TRUE if
+# all listed variables are TRUE
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(MySOFA REQUIRED_VARS MYSOFA_LIBRARY MYSOFA_INCLUDE_DIR ZLIB_FOUND)
+
+if(MYSOFA_FOUND)
+    set(MYSOFA_INCLUDE_DIRS ${MYSOFA_INCLUDE_DIR})
+    set(MYSOFA_LIBRARIES ${MYSOFA_LIBRARY})
+    set(MYSOFA_LIBRARIES ${MYSOFA_LIBRARIES} ZLIB::ZLIB)
+    if(MYSOFA_M_LIBRARY)
+        set(MYSOFA_LIBRARIES ${MYSOFA_LIBRARIES} ${MYSOFA_M_LIBRARY})
+    endif()
+
+    add_library(MySOFA::MySOFA UNKNOWN IMPORTED)
+    set_property(TARGET MySOFA::MySOFA PROPERTY
+        IMPORTED_LOCATION ${MYSOFA_LIBRARY})
+    set_target_properties(MySOFA::MySOFA PROPERTIES
+        INTERFACE_INCLUDE_DIRECTORIES ${MYSOFA_INCLUDE_DIRS}
+        INTERFACE_LINK_LIBRARIES ZLIB::ZLIB)
+    if(MYSOFA_M_LIBRARY)
+        set_property(TARGET MySOFA::MySOFA APPEND PROPERTY
+            INTERFACE_LINK_LIBRARIES ${MYSOFA_M_LIBRARY})
+    endif()
+endif()
+
+mark_as_advanced(MYSOFA_INCLUDE_DIR MYSOFA_LIBRARY)
diff --git a/cmake/FindOSS.cmake b/cmake/FindOSS.cmake
new file mode 100644 (file)
index 0000000..feffb45
--- /dev/null
@@ -0,0 +1,33 @@
+# - Find OSS includes
+#
+#   OSS_FOUND        - True if OSS_INCLUDE_DIR is found
+#   OSS_INCLUDE_DIRS - Set when OSS_INCLUDE_DIR is found
+#   OSS_LIBRARIES    - Set when OSS_LIBRARY is found
+#
+#   OSS_INCLUDE_DIR - where to find sys/soundcard.h, etc.
+#   OSS_LIBRARY     - where to find libossaudio (optional).
+#
+
+find_path(OSS_INCLUDE_DIR
+          NAMES sys/soundcard.h
+          DOC "The OSS include directory"
+)
+
+find_library(OSS_LIBRARY
+             NAMES ossaudio
+             DOC "Optional OSS library"
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(OSS  REQUIRED_VARS OSS_INCLUDE_DIR)
+
+if(OSS_FOUND)
+    set(OSS_INCLUDE_DIRS ${OSS_INCLUDE_DIR})
+    if(OSS_LIBRARY)
+        set(OSS_LIBRARIES ${OSS_LIBRARY})
+    else()
+        unset(OSS_LIBRARIES)
+    endif()
+endif()
+
+mark_as_advanced(OSS_INCLUDE_DIR OSS_LIBRARY)
diff --git a/cmake/FindOboe.cmake b/cmake/FindOboe.cmake
new file mode 100644 (file)
index 0000000..bf12c12
--- /dev/null
@@ -0,0 +1,31 @@
+# - Find Oboe
+# Find the Oboe library
+#
+# This module defines the following variable:
+#   OBOE_FOUND - True if Oboe was found
+#
+# This module defines the following target:
+#   oboe::oboe - Import target for linking Oboe to a project
+#
+
+find_path(OBOE_INCLUDE_DIR NAMES oboe/Oboe.h
+    DOC "The Oboe include directory"
+)
+
+find_library(OBOE_LIBRARY NAMES oboe
+    DOC "The Oboe library"
+)
+
+# handle the QUIETLY and REQUIRED arguments and set OBOE_FOUND to TRUE if
+# all listed variables are TRUE
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Oboe REQUIRED_VARS OBOE_LIBRARY OBOE_INCLUDE_DIR)
+
+if(OBOE_FOUND)
+    add_library(oboe::oboe UNKNOWN IMPORTED)
+    set_target_properties(oboe::oboe PROPERTIES
+        IMPORTED_LOCATION ${OBOE_LIBRARY}
+        INTERFACE_INCLUDE_DIRECTORIES ${OBOE_INCLUDE_DIR})
+endif()
+
+mark_as_advanced(OBOE_INCLUDE_DIR OBOE_LIBRARY)
diff --git a/cmake/FindOpenSL.cmake b/cmake/FindOpenSL.cmake
new file mode 100644 (file)
index 0000000..0042874
--- /dev/null
@@ -0,0 +1,63 @@
+# - Find OpenSL
+# Find the OpenSL libraries
+#
+#  This module defines the following variables and targets:
+#     OPENSL_FOUND     - True if OPENSL was found
+#     OpenSL::OpenSLES - The OpenSLES target
+#
+
+#=============================================================================
+# Copyright 2009-2011 Kitware, Inc.
+# Copyright 2009-2011 Philip Lowman <philip@yhbt.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#  * Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+#
+#  * Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+#  * The names of Kitware, Inc., the Insight Consortium, or the names of
+#    any consortium members, or of any contributors, may not be used to
+#    endorse or promote products derived from this software without
+#    specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#=============================================================================
+
+find_path(OPENSL_INCLUDE_DIR NAMES SLES/OpenSLES.h
+    DOC "The OpenSL include directory")
+find_path(OPENSL_ANDROID_INCLUDE_DIR NAMES SLES/OpenSLES_Android.h
+    DOC "The OpenSL Android include directory")
+
+find_library(OPENSL_LIBRARY NAMES OpenSLES
+    DOC "The OpenSL library")
+
+# handle the QUIETLY and REQUIRED arguments and set OPENSL_FOUND to TRUE if
+# all listed variables are TRUE
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(OpenSL REQUIRED_VARS OPENSL_LIBRARY OPENSL_INCLUDE_DIR
+    OPENSL_ANDROID_INCLUDE_DIR)
+
+if(OPENSL_FOUND)
+    add_library(OpenSL::OpenSLES UNKNOWN IMPORTED)
+    set_target_properties(OpenSL::OpenSLES PROPERTIES
+        IMPORTED_LOCATION ${OPENSL_LIBRARY}
+        INTERFACE_INCLUDE_DIRECTORIES ${OPENSL_INCLUDE_DIR}
+        INTERFACE_INCLUDE_DIRECTORIES ${OPENSL_ANDROID_INCLUDE_DIR})
+endif()
+
+mark_as_advanced(OPENSL_INCLUDE_DIR OPENSL_ANDROID_INCLUDE_DIR OPENSL_LIBRARY)
diff --git a/cmake/FindPortAudio.cmake b/cmake/FindPortAudio.cmake
new file mode 100644 (file)
index 0000000..fad2313
--- /dev/null
@@ -0,0 +1,32 @@
+# - Find PortAudio includes and libraries
+#
+#   PORTAUDIO_FOUND        - True if PORTAUDIO_INCLUDE_DIR & PORTAUDIO_LIBRARY
+#                            are found
+#   PORTAUDIO_LIBRARIES    - Set when PORTAUDIO_LIBRARY is found
+#   PORTAUDIO_INCLUDE_DIRS - Set when PORTAUDIO_INCLUDE_DIR is found
+#
+#   PORTAUDIO_INCLUDE_DIR - where to find portaudio.h, etc.
+#   PORTAUDIO_LIBRARY     - the portaudio library
+#
+
+find_path(PORTAUDIO_INCLUDE_DIR
+          NAMES portaudio.h
+          DOC "The PortAudio include directory"
+)
+
+find_library(PORTAUDIO_LIBRARY
+             NAMES portaudio
+             DOC "The PortAudio library"
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(PortAudio
+    REQUIRED_VARS PORTAUDIO_LIBRARY PORTAUDIO_INCLUDE_DIR
+)
+
+if(PORTAUDIO_FOUND)
+    set(PORTAUDIO_LIBRARIES ${PORTAUDIO_LIBRARY})
+    set(PORTAUDIO_INCLUDE_DIRS ${PORTAUDIO_INCLUDE_DIR})
+endif()
+
+mark_as_advanced(PORTAUDIO_INCLUDE_DIR PORTAUDIO_LIBRARY)
diff --git a/cmake/FindPulseAudio.cmake b/cmake/FindPulseAudio.cmake
new file mode 100644 (file)
index 0000000..fdcbc20
--- /dev/null
@@ -0,0 +1,34 @@
+# - Find PulseAudio includes and libraries
+#
+#   PULSEAUDIO_FOUND        - True if PULSEAUDIO_INCLUDE_DIR &
+#                             PULSEAUDIO_LIBRARY are found
+#
+#   PULSEAUDIO_INCLUDE_DIR - where to find pulse/pulseaudio.h, etc.
+#   PULSEAUDIO_LIBRARY     - the pulse library
+#   PULSEAUDIO_VERSION_STRING - the version of PulseAudio found
+#
+
+find_path(PULSEAUDIO_INCLUDE_DIR
+          NAMES pulse/pulseaudio.h
+          DOC "The PulseAudio include directory"
+)
+
+find_library(PULSEAUDIO_LIBRARY
+             NAMES pulse
+             DOC "The PulseAudio library"
+)
+
+if(PULSEAUDIO_INCLUDE_DIR AND EXISTS "${PULSEAUDIO_INCLUDE_DIR}/pulse/version.h")
+    file(STRINGS "${PULSEAUDIO_INCLUDE_DIR}/pulse/version.h" pulse_version_str
+         REGEX "^#define[\t ]+pa_get_headers_version\\(\\)[\t ]+\\(\".*\"\\)")
+
+    string(REGEX REPLACE "^.*pa_get_headers_version\\(\\)[\t ]+\\(\"([^\"]*)\"\\).*$" "\\1"
+           PULSEAUDIO_VERSION_STRING "${pulse_version_str}")
+    unset(pulse_version_str)
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(PulseAudio
+    REQUIRED_VARS PULSEAUDIO_LIBRARY PULSEAUDIO_INCLUDE_DIR
+    VERSION_VAR PULSEAUDIO_VERSION_STRING
+)
diff --git a/cmake/FindSndFile.cmake b/cmake/FindSndFile.cmake
new file mode 100644 (file)
index 0000000..b931d3c
--- /dev/null
@@ -0,0 +1,25 @@
+# - Try to find SndFile
+# Once done this will define
+#
+#  SNDFILE_FOUND - system has SndFile
+#  SndFile::SndFile - the SndFile target
+#
+
+find_path(SNDFILE_INCLUDE_DIR NAMES sndfile.h)
+
+find_library(SNDFILE_LIBRARY NAMES sndfile sndfile-1)
+
+# handle the QUIETLY and REQUIRED arguments and set SNDFILE_FOUND to TRUE if
+# all listed variables are TRUE
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(SndFile DEFAULT_MSG SNDFILE_LIBRARY SNDFILE_INCLUDE_DIR)
+
+if(SNDFILE_FOUND)
+    add_library(SndFile::SndFile UNKNOWN IMPORTED)
+    set_target_properties(SndFile::SndFile PROPERTIES
+        IMPORTED_LOCATION ${SNDFILE_LIBRARY}
+        INTERFACE_INCLUDE_DIRECTORIES ${SNDFILE_INCLUDE_DIR})
+endif()
+
+# show the SNDFILE_INCLUDE_DIR and SNDFILE_LIBRARY variables only in the advanced view
+mark_as_advanced(SNDFILE_INCLUDE_DIR SNDFILE_LIBRARY)
diff --git a/cmake/FindSoundIO.cmake b/cmake/FindSoundIO.cmake
new file mode 100644 (file)
index 0000000..1045025
--- /dev/null
@@ -0,0 +1,32 @@
+# - Find SoundIO (sndio) includes and libraries
+#
+#   SOUNDIO_FOUND        - True if SOUNDIO_INCLUDE_DIR & SOUNDIO_LIBRARY are
+#                          found
+#   SOUNDIO_LIBRARIES    - Set when SOUNDIO_LIBRARY is found
+#   SOUNDIO_INCLUDE_DIRS - Set when SOUNDIO_INCLUDE_DIR is found
+#
+#   SOUNDIO_INCLUDE_DIR - where to find sndio.h, etc.
+#   SOUNDIO_LIBRARY     - the sndio library
+#
+
+find_path(SOUNDIO_INCLUDE_DIR
+          NAMES sndio.h
+          DOC "The SoundIO include directory"
+)
+
+find_library(SOUNDIO_LIBRARY
+             NAMES sndio
+             DOC "The SoundIO library"
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(SoundIO
+    REQUIRED_VARS SOUNDIO_LIBRARY SOUNDIO_INCLUDE_DIR
+)
+
+if(SOUNDIO_FOUND)
+    set(SOUNDIO_LIBRARIES ${SOUNDIO_LIBRARY})
+    set(SOUNDIO_INCLUDE_DIRS ${SOUNDIO_INCLUDE_DIR})
+endif()
+
+mark_as_advanced(SOUNDIO_INCLUDE_DIR SOUNDIO_LIBRARY)
diff --git a/cmake/bin2h.script.cmake b/cmake/bin2h.script.cmake
new file mode 100644 (file)
index 0000000..7e74a7a
--- /dev/null
@@ -0,0 +1,12 @@
+# Read the input file into 'indata', converting each byte to a pair of hex
+# characters
+file(READ "${INPUT_FILE}" indata HEX)
+
+# For each pair of characters, indent them and prepend the 0x prefix, and
+# append a comma separateor.
+# TODO: Prettify this. Should group a number of bytes per line instead of one
+# per line.
+string(REGEX REPLACE "(..)" "    0x\\1,\n" output "${indata}")
+
+# Write the list of hex chars to the output file
+file(WRITE "${OUTPUT_FILE}" "${output}")
diff --git a/common/albit.h b/common/albit.h
new file mode 100644 (file)
index 0000000..ad59620
--- /dev/null
@@ -0,0 +1,155 @@
+#ifndef AL_BIT_H
+#define AL_BIT_H
+
+#include <cstdint>
+#include <limits>
+#include <type_traits>
+#if !defined(__GNUC__) && (defined(_WIN32) || defined(_WIN64))
+#include <intrin.h>
+#endif
+
+namespace al {
+
+#ifdef __BYTE_ORDER__
+enum class endian {
+    little = __ORDER_LITTLE_ENDIAN__,
+    big = __ORDER_BIG_ENDIAN__,
+    native = __BYTE_ORDER__
+};
+
+#else
+
+/* This doesn't support mixed-endian. */
+namespace detail_ {
+constexpr bool IsLittleEndian() noexcept
+{
+    static_assert(sizeof(char) < sizeof(int), "char is too big");
+
+    constexpr int test_val{1};
+    return static_cast<const char&>(test_val) ? true : false;
+}
+} // namespace detail_
+
+enum class endian {
+    big = 0,
+    little = 1,
+    native = detail_::IsLittleEndian() ? little : big
+};
+#endif
+
+
+/* Define popcount (population count/count 1 bits) and countr_zero (count
+ * trailing zero bits, starting from the lsb) methods, for various integer
+ * types.
+ */
+#ifdef __GNUC__
+
+namespace detail_ {
+    inline int popcount(unsigned long long val) noexcept { return __builtin_popcountll(val); }
+    inline int popcount(unsigned long val) noexcept { return __builtin_popcountl(val); }
+    inline int popcount(unsigned int val) noexcept { return __builtin_popcount(val); }
+
+    inline int countr_zero(unsigned long long val) noexcept { return __builtin_ctzll(val); }
+    inline int countr_zero(unsigned long val) noexcept { return __builtin_ctzl(val); }
+    inline int countr_zero(unsigned int val) noexcept { return __builtin_ctz(val); }
+} // namespace detail_
+
+template<typename T>
+inline std::enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value,
+int> popcount(T v) noexcept { return detail_::popcount(v); }
+
+template<typename T>
+inline std::enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value,
+int> countr_zero(T val) noexcept
+{ return val ? detail_::countr_zero(val) : std::numeric_limits<T>::digits; }
+
+#else
+
+/* There be black magics here. The popcount method is derived from
+ * https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
+ * while the ctz-utilizing-popcount algorithm is shown here
+ * http://www.hackersdelight.org/hdcodetxt/ntz.c.txt
+ * as the ntz2 variant. These likely aren't the most efficient methods, but
+ * they're good enough if the GCC built-ins aren't available.
+ */
+namespace detail_ {
+    template<typename T, size_t = std::numeric_limits<T>::digits>
+    struct fast_utype { };
+    template<typename T>
+    struct fast_utype<T,8> { using type = std::uint_fast8_t; };
+    template<typename T>
+    struct fast_utype<T,16> { using type = std::uint_fast16_t; };
+    template<typename T>
+    struct fast_utype<T,32> { using type = std::uint_fast32_t; };
+    template<typename T>
+    struct fast_utype<T,64> { using type = std::uint_fast64_t; };
+
+    template<typename T>
+    constexpr T repbits(unsigned char bits) noexcept
+    {
+        T ret{bits};
+        for(size_t i{1};i < sizeof(T);++i)
+            ret = (ret<<8) | bits;
+        return ret;
+    }
+} // namespace detail_
+
+template<typename T>
+constexpr std::enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value,
+int> popcount(T val) noexcept
+{
+    using fast_type = typename detail_::fast_utype<T>::type;
+    constexpr fast_type b01010101{detail_::repbits<fast_type>(0x55)};
+    constexpr fast_type b00110011{detail_::repbits<fast_type>(0x33)};
+    constexpr fast_type b00001111{detail_::repbits<fast_type>(0x0f)};
+    constexpr fast_type b00000001{detail_::repbits<fast_type>(0x01)};
+
+    fast_type v{fast_type{val} - ((fast_type{val} >> 1) & b01010101)};
+    v = (v & b00110011) + ((v >> 2) & b00110011);
+    v = (v + (v >> 4)) & b00001111;
+    return static_cast<int>(((v * b00000001) >> ((sizeof(T)-1)*8)) & 0xff);
+}
+
+#ifdef _WIN32
+
+template<typename T>
+inline std::enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value
+    && std::numeric_limits<T>::digits <= 32,
+int> countr_zero(T v)
+{
+    unsigned long idx{std::numeric_limits<T>::digits};
+    _BitScanForward(&idx, static_cast<uint32_t>(v));
+    return static_cast<int>(idx);
+}
+
+template<typename T>
+inline std::enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value
+    && 32 < std::numeric_limits<T>::digits && std::numeric_limits<T>::digits <= 64,
+int> countr_zero(T v)
+{
+    unsigned long idx{std::numeric_limits<T>::digits};
+#ifdef _WIN64
+    _BitScanForward64(&idx, v);
+#else
+    if(!_BitScanForward(&idx, static_cast<uint32_t>(v)))
+    {
+        if(_BitScanForward(&idx, static_cast<uint32_t>(v>>32)))
+            idx += 32;
+    }
+#endif /* _WIN64 */
+    return static_cast<int>(idx);
+}
+
+#else
+
+template<typename T>
+constexpr std::enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value,
+int> countr_zero(T value)
+{ return popcount(static_cast<T>(~value & (value - 1))); }
+
+#endif
+#endif
+
+} // namespace al
+
+#endif /* AL_BIT_H */
diff --git a/common/albyte.h b/common/albyte.h
new file mode 100644 (file)
index 0000000..be58686
--- /dev/null
@@ -0,0 +1,17 @@
+#ifndef AL_BYTE_H
+#define AL_BYTE_H
+
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <type_traits>
+
+using uint = unsigned int;
+
+namespace al {
+
+using byte = unsigned char;
+
+} // namespace al
+
+#endif /* AL_BYTE_H */
diff --git a/common/alcomplex.cpp b/common/alcomplex.cpp
new file mode 100644 (file)
index 0000000..4420a1b
--- /dev/null
@@ -0,0 +1,171 @@
+
+#include "config.h"
+
+#include "alcomplex.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <cstddef>
+#include <functional>
+#include <utility>
+
+#include "albit.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "opthelpers.h"
+
+
+namespace {
+
+using ushort = unsigned short;
+using ushort2 = std::pair<ushort,ushort>;
+
+constexpr size_t BitReverseCounter(size_t log2_size) noexcept
+{
+    /* Some magic math that calculates the number of swaps needed for a
+     * sequence of bit-reversed indices when index < reversed_index.
+     */
+    return (1u<<(log2_size-1)) - (1u<<((log2_size-1u)/2u));
+}
+
+
+template<size_t N>
+struct BitReverser {
+    static_assert(N <= sizeof(ushort)*8, "Too many bits for the bit-reversal table.");
+
+    ushort2 mData[BitReverseCounter(N)]{};
+
+    constexpr BitReverser()
+    {
+        const size_t fftsize{1u << N};
+        size_t ret_i{0};
+
+        /* Bit-reversal permutation applied to a sequence of fftsize items. */
+        for(size_t idx{1u};idx < fftsize-1;++idx)
+        {
+            size_t revidx{0u}, imask{idx};
+            for(size_t i{0};i < N;++i)
+            {
+                revidx = (revidx<<1) | (imask&1);
+                imask >>= 1;
+            }
+
+            if(idx < revidx)
+            {
+                mData[ret_i].first  = static_cast<ushort>(idx);
+                mData[ret_i].second = static_cast<ushort>(revidx);
+                ++ret_i;
+            }
+        }
+        assert(ret_i == al::size(mData));
+    }
+};
+
+/* These bit-reversal swap tables support up to 10-bit indices (1024 elements),
+ * which is the largest used by OpenAL Soft's filters and effects. Larger FFT
+ * requests, used by some utilities where performance is less important, will
+ * use a slower table-less path.
+ */
+constexpr BitReverser<2> BitReverser2{};
+constexpr BitReverser<3> BitReverser3{};
+constexpr BitReverser<4> BitReverser4{};
+constexpr BitReverser<5> BitReverser5{};
+constexpr BitReverser<6> BitReverser6{};
+constexpr BitReverser<7> BitReverser7{};
+constexpr BitReverser<8> BitReverser8{};
+constexpr BitReverser<9> BitReverser9{};
+constexpr BitReverser<10> BitReverser10{};
+constexpr std::array<al::span<const ushort2>,11> gBitReverses{{
+    {}, {},
+    BitReverser2.mData,
+    BitReverser3.mData,
+    BitReverser4.mData,
+    BitReverser5.mData,
+    BitReverser6.mData,
+    BitReverser7.mData,
+    BitReverser8.mData,
+    BitReverser9.mData,
+    BitReverser10.mData
+}};
+
+} // namespace
+
+template<typename Real>
+std::enable_if_t<std::is_floating_point<Real>::value>
+complex_fft(const al::span<std::complex<Real>> buffer, const al::type_identity_t<Real> sign)
+{
+    const size_t fftsize{buffer.size()};
+    /* Get the number of bits used for indexing. Simplifies bit-reversal and
+     * the main loop count.
+     */
+    const size_t log2_size{static_cast<size_t>(al::countr_zero(fftsize))};
+
+    if(log2_size >= gBitReverses.size()) UNLIKELY
+    {
+        for(size_t idx{1u};idx < fftsize-1;++idx)
+        {
+            size_t revidx{0u}, imask{idx};
+            for(size_t i{0};i < log2_size;++i)
+            {
+                revidx = (revidx<<1) | (imask&1);
+                imask >>= 1;
+            }
+
+            if(idx < revidx)
+                std::swap(buffer[idx], buffer[revidx]);
+        }
+    }
+    else for(auto &rev : gBitReverses[log2_size])
+        std::swap(buffer[rev.first], buffer[rev.second]);
+
+    /* Iterative form of Danielson-Lanczos lemma */
+    const Real pi{al::numbers::pi_v<Real> * sign};
+    size_t step2{1u};
+    for(size_t i{0};i < log2_size;++i)
+    {
+        const Real arg{pi / static_cast<Real>(step2)};
+
+        /* TODO: Would std::polar(1.0, arg) be any better? */
+        const std::complex<Real> w{std::cos(arg), std::sin(arg)};
+        std::complex<Real> u{1.0, 0.0};
+        const size_t step{step2 << 1};
+        for(size_t j{0};j < step2;j++)
+        {
+            for(size_t k{j};k < fftsize;k+=step)
+            {
+                std::complex<Real> temp{buffer[k+step2] * u};
+                buffer[k+step2] = buffer[k] - temp;
+                buffer[k] += temp;
+            }
+
+            u *= w;
+        }
+
+        step2 <<= 1;
+    }
+}
+
+void complex_hilbert(const al::span<std::complex<double>> buffer)
+{
+    using namespace std::placeholders;
+
+    inverse_fft(buffer);
+
+    const double inverse_size = 1.0/static_cast<double>(buffer.size());
+    auto bufiter = buffer.begin();
+    const auto halfiter = bufiter + (buffer.size()>>1);
+
+    *bufiter *= inverse_size; ++bufiter;
+    bufiter = std::transform(bufiter, halfiter, bufiter,
+        [scale=inverse_size*2.0](std::complex<double> d){ return d * scale; });
+    *bufiter *= inverse_size; ++bufiter;
+
+    std::fill(bufiter, buffer.end(), std::complex<double>{});
+
+    forward_fft(buffer);
+}
+
+
+template void complex_fft<>(const al::span<std::complex<float>> buffer, const float sign);
+template void complex_fft<>(const al::span<std::complex<double>> buffer, const double sign);
diff --git a/common/alcomplex.h b/common/alcomplex.h
new file mode 100644 (file)
index 0000000..794c352
--- /dev/null
@@ -0,0 +1,45 @@
+#ifndef ALCOMPLEX_H
+#define ALCOMPLEX_H
+
+#include <complex>
+#include <type_traits>
+
+#include "alspan.h"
+
+/**
+ * Iterative implementation of 2-radix FFT (In-place algorithm). Sign = -1 is
+ * FFT and 1 is inverse FFT. Applies the Discrete Fourier Transform (DFT) to
+ * the data supplied in the buffer, which MUST BE power of two.
+ */
+template<typename Real>
+std::enable_if_t<std::is_floating_point<Real>::value>
+complex_fft(const al::span<std::complex<Real>> buffer, const al::type_identity_t<Real> sign);
+
+/**
+ * Calculate the frequency-domain response of the time-domain signal in the
+ * provided buffer, which MUST BE power of two.
+ */
+template<typename Real, size_t N>
+std::enable_if_t<std::is_floating_point<Real>::value>
+forward_fft(const al::span<std::complex<Real>,N> buffer)
+{ complex_fft(buffer.subspan(0), -1); }
+
+/**
+ * Calculate the time-domain signal of the frequency-domain response in the
+ * provided buffer, which MUST BE power of two.
+ */
+template<typename Real, size_t N>
+std::enable_if_t<std::is_floating_point<Real>::value>
+inverse_fft(const al::span<std::complex<Real>,N> buffer)
+{ complex_fft(buffer.subspan(0), 1); }
+
+/**
+ * Calculate the complex helical sequence (discrete-time analytical signal) of
+ * the given input using the discrete Hilbert transform (In-place algorithm).
+ * Fills the buffer with the discrete-time analytical signal stored in the
+ * buffer. The buffer is an array of complex numbers and MUST BE power of two,
+ * and the imaginary components should be cleared to 0.
+ */
+void complex_hilbert(const al::span<std::complex<double>> buffer);
+
+#endif /* ALCOMPLEX_H */
diff --git a/common/aldeque.h b/common/aldeque.h
new file mode 100644 (file)
index 0000000..3f99bf0
--- /dev/null
@@ -0,0 +1,16 @@
+#ifndef ALDEQUE_H
+#define ALDEQUE_H
+
+#include <deque>
+
+#include "almalloc.h"
+
+
+namespace al {
+
+template<typename T>
+using deque = std::deque<T, al::allocator<T>>;
+
+} // namespace al
+
+#endif /* ALDEQUE_H */
diff --git a/common/alfstream.cpp b/common/alfstream.cpp
new file mode 100644 (file)
index 0000000..8991ce0
--- /dev/null
@@ -0,0 +1,26 @@
+
+#include "config.h"
+
+#include "alfstream.h"
+
+#include "strutils.h"
+
+#ifdef _WIN32
+
+namespace al {
+
+ifstream::ifstream(const char *filename, std::ios_base::openmode mode)
+  : std::ifstream{utf8_to_wstr(filename).c_str(), mode}
+{ }
+
+void ifstream::open(const char *filename, std::ios_base::openmode mode)
+{
+    std::wstring wstr{utf8_to_wstr(filename)};
+    std::ifstream::open(wstr.c_str(), mode);
+}
+
+ifstream::~ifstream() = default;
+
+} // namespace al
+
+#endif
diff --git a/common/alfstream.h b/common/alfstream.h
new file mode 100644 (file)
index 0000000..62b2e12
--- /dev/null
@@ -0,0 +1,45 @@
+#ifndef AL_FSTREAM_H
+#define AL_FSTREAM_H
+
+#ifdef _WIN32
+
+#include <string>
+#include <fstream>
+
+
+namespace al {
+
+// Inherit from std::ifstream to accept UTF-8 filenames
+class ifstream final : public std::ifstream {
+public:
+    explicit ifstream(const char *filename, std::ios_base::openmode mode=std::ios_base::in);
+    explicit ifstream(const std::string &filename, std::ios_base::openmode mode=std::ios_base::in)
+        : ifstream{filename.c_str(), mode} { }
+
+    explicit ifstream(const wchar_t *filename, std::ios_base::openmode mode=std::ios_base::in)
+        : std::ifstream{filename, mode} { }
+    explicit ifstream(const std::wstring &filename, std::ios_base::openmode mode=std::ios_base::in)
+        : ifstream{filename.c_str(), mode} { }
+
+    void open(const char *filename, std::ios_base::openmode mode=std::ios_base::in);
+    void open(const std::string &filename, std::ios_base::openmode mode=std::ios_base::in)
+    { open(filename.c_str(), mode); }
+
+    ~ifstream() override;
+};
+
+} // namespace al
+
+#else /* _WIN32 */
+
+#include <fstream>
+
+namespace al {
+
+using ifstream = std::ifstream;
+
+} // namespace al
+
+#endif /* _WIN32 */
+
+#endif /* AL_FSTREAM_H */
diff --git a/common/almalloc.cpp b/common/almalloc.cpp
new file mode 100644 (file)
index 0000000..ad1dc6b
--- /dev/null
@@ -0,0 +1,61 @@
+
+#include "config.h"
+
+#include "almalloc.h"
+
+#include <cassert>
+#include <cstddef>
+#include <cstdlib>
+#include <cstring>
+#include <memory>
+#ifdef HAVE_MALLOC_H
+#include <malloc.h>
+#endif
+
+
+void *al_malloc(size_t alignment, size_t size)
+{
+    assert((alignment & (alignment-1)) == 0);
+    alignment = std::max(alignment, alignof(std::max_align_t));
+
+#if defined(HAVE_POSIX_MEMALIGN)
+    void *ret{};
+    if(posix_memalign(&ret, alignment, size) == 0)
+        return ret;
+    return nullptr;
+#elif defined(HAVE__ALIGNED_MALLOC)
+    return _aligned_malloc(size, alignment);
+#else
+    size_t total_size{size + alignment-1 + sizeof(void*)};
+    void *base{std::malloc(total_size)};
+    if(base != nullptr)
+    {
+        void *aligned_ptr{static_cast<char*>(base) + sizeof(void*)};
+        total_size -= sizeof(void*);
+
+        std::align(alignment, size, aligned_ptr, total_size);
+        *(static_cast<void**>(aligned_ptr)-1) = base;
+        base = aligned_ptr;
+    }
+    return base;
+#endif
+}
+
+void *al_calloc(size_t alignment, size_t size)
+{
+    void *ret{al_malloc(alignment, size)};
+    if(ret) std::memset(ret, 0, size);
+    return ret;
+}
+
+void al_free(void *ptr) noexcept
+{
+#if defined(HAVE_POSIX_MEMALIGN)
+    std::free(ptr);
+#elif defined(HAVE__ALIGNED_MALLOC)
+    _aligned_free(ptr);
+#else
+    if(ptr != nullptr)
+        std::free(*(static_cast<void**>(ptr) - 1));
+#endif
+}
diff --git a/common/almalloc.h b/common/almalloc.h
new file mode 100644 (file)
index 0000000..a795fc3
--- /dev/null
@@ -0,0 +1,311 @@
+#ifndef AL_MALLOC_H
+#define AL_MALLOC_H
+
+#include <algorithm>
+#include <cstddef>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#include "pragmadefs.h"
+
+
+void al_free(void *ptr) noexcept;
+[[gnu::alloc_align(1), gnu::alloc_size(2), gnu::malloc]]
+void *al_malloc(size_t alignment, size_t size);
+[[gnu::alloc_align(1), gnu::alloc_size(2), gnu::malloc]]
+void *al_calloc(size_t alignment, size_t size);
+
+
+#define DISABLE_ALLOC()                                                       \
+    void *operator new(size_t) = delete;                                      \
+    void *operator new[](size_t) = delete;                                    \
+    void operator delete(void*) noexcept = delete;                            \
+    void operator delete[](void*) noexcept = delete;
+
+#define DEF_NEWDEL(T)                                                         \
+    void *operator new(size_t size)                                           \
+    {                                                                         \
+        static_assert(&operator new == &T::operator new,                      \
+            "Incorrect container type specified");                            \
+        if(void *ret{al_malloc(alignof(T), size)})                            \
+            return ret;                                                       \
+        throw std::bad_alloc();                                               \
+    }                                                                         \
+    void *operator new[](size_t size) { return operator new(size); }          \
+    void operator delete(void *block) noexcept { al_free(block); }            \
+    void operator delete[](void *block) noexcept { operator delete(block); }
+
+#define DEF_PLACE_NEWDEL()                                                    \
+    void *operator new(size_t /*size*/, void *ptr) noexcept { return ptr; }   \
+    void *operator new[](size_t /*size*/, void *ptr) noexcept { return ptr; } \
+    void operator delete(void *block, void*) noexcept { al_free(block); }     \
+    void operator delete(void *block) noexcept { al_free(block); }            \
+    void operator delete[](void *block, void*) noexcept { al_free(block); }   \
+    void operator delete[](void *block) noexcept { al_free(block); }
+
+enum FamCount : size_t { };
+
+#define DEF_FAM_NEWDEL(T, FamMem)                                             \
+    static constexpr size_t Sizeof(size_t count) noexcept                     \
+    {                                                                         \
+        static_assert(&Sizeof == &T::Sizeof,                                  \
+            "Incorrect container type specified");                            \
+        return std::max(decltype(FamMem)::Sizeof(count, offsetof(T, FamMem)), \
+            sizeof(T));                                                       \
+    }                                                                         \
+                                                                              \
+    void *operator new(size_t /*size*/, FamCount count)                       \
+    {                                                                         \
+        if(void *ret{al_malloc(alignof(T), T::Sizeof(count))})                \
+            return ret;                                                       \
+        throw std::bad_alloc();                                               \
+    }                                                                         \
+    void *operator new[](size_t /*size*/) = delete;                           \
+    void operator delete(void *block, FamCount) { al_free(block); }           \
+    void operator delete(void *block) noexcept { al_free(block); }            \
+    void operator delete[](void* /*block*/) = delete;
+
+
+namespace al {
+
+template<typename T, std::size_t Align=alignof(T)>
+struct allocator {
+    static constexpr std::size_t alignment{std::max(Align, alignof(T))};
+
+    using value_type = T;
+    using reference = T&;
+    using const_reference = const T&;
+    using pointer = T*;
+    using const_pointer = const T*;
+    using size_type = std::size_t;
+    using difference_type = std::ptrdiff_t;
+    using is_always_equal = std::true_type;
+
+    template<typename U>
+    struct rebind {
+        using other = allocator<U, Align>;
+    };
+
+    constexpr explicit allocator() noexcept = default;
+    template<typename U, std::size_t N>
+    constexpr explicit allocator(const allocator<U,N>&) noexcept { }
+
+    T *allocate(std::size_t n)
+    {
+        if(n > std::numeric_limits<std::size_t>::max()/sizeof(T)) throw std::bad_alloc();
+        if(auto p = al_malloc(alignment, n*sizeof(T))) return static_cast<T*>(p);
+        throw std::bad_alloc();
+    }
+    void deallocate(T *p, std::size_t) noexcept { al_free(p); }
+};
+template<typename T, std::size_t N, typename U, std::size_t M>
+constexpr bool operator==(const allocator<T,N>&, const allocator<U,M>&) noexcept { return true; }
+template<typename T, std::size_t N, typename U, std::size_t M>
+constexpr bool operator!=(const allocator<T,N>&, const allocator<U,M>&) noexcept { return false; }
+
+
+template<typename T>
+constexpr T *to_address(T *p) noexcept
+{
+    static_assert(!std::is_function<T>::value, "Can't be a function type");
+    return p;
+}
+
+template<typename T>
+constexpr auto to_address(const T &p) noexcept
+{ return to_address(p.operator->()); }
+
+
+template<typename T, typename ...Args>
+constexpr T* construct_at(T *ptr, Args&& ...args)
+    noexcept(std::is_nothrow_constructible<T, Args...>::value)
+{ return ::new(static_cast<void*>(ptr)) T{std::forward<Args>(args)...}; }
+
+/* At least VS 2015 complains that 'ptr' is unused when the given type's
+ * destructor is trivial (a no-op). So disable that warning for this call.
+ */
+DIAGNOSTIC_PUSH
+msc_pragma(warning(disable : 4100))
+template<typename T>
+constexpr std::enable_if_t<!std::is_array<T>::value>
+destroy_at(T *ptr) noexcept(std::is_nothrow_destructible<T>::value)
+{ ptr->~T(); }
+DIAGNOSTIC_POP
+template<typename T>
+constexpr std::enable_if_t<std::is_array<T>::value>
+destroy_at(T *ptr) noexcept(std::is_nothrow_destructible<std::remove_all_extents_t<T>>::value)
+{
+    for(auto &elem : *ptr)
+        al::destroy_at(std::addressof(elem));
+}
+
+template<typename T>
+constexpr void destroy(T first, T end) noexcept(noexcept(al::destroy_at(std::addressof(*first))))
+{
+    while(first != end)
+    {
+        al::destroy_at(std::addressof(*first));
+        ++first;
+    }
+}
+
+template<typename T, typename N>
+constexpr std::enable_if_t<std::is_integral<N>::value,T>
+destroy_n(T first, N count) noexcept(noexcept(al::destroy_at(std::addressof(*first))))
+{
+    if(count != 0)
+    {
+        do {
+            al::destroy_at(std::addressof(*first));
+            ++first;
+        } while(--count);
+    }
+    return first;
+}
+
+
+template<typename T, typename N>
+inline std::enable_if_t<std::is_integral<N>::value,
+T> uninitialized_default_construct_n(T first, N count)
+{
+    using ValueT = typename std::iterator_traits<T>::value_type;
+    T current{first};
+    if(count != 0)
+    {
+        try {
+            do {
+                ::new(static_cast<void*>(std::addressof(*current))) ValueT;
+                ++current;
+            } while(--count);
+        }
+        catch(...) {
+            al::destroy(first, current);
+            throw;
+        }
+    }
+    return current;
+}
+
+
+/* Storage for flexible array data. This is trivially destructible if type T is
+ * trivially destructible.
+ */
+template<typename T, size_t alignment, bool = std::is_trivially_destructible<T>::value>
+struct FlexArrayStorage {
+    const size_t mSize;
+    union {
+        char mDummy;
+        alignas(alignment) T mArray[1];
+    };
+
+    static constexpr size_t Sizeof(size_t count, size_t base=0u) noexcept
+    {
+        const size_t len{sizeof(T)*count};
+        return std::max(offsetof(FlexArrayStorage,mArray)+len, sizeof(FlexArrayStorage)) + base;
+    }
+
+    FlexArrayStorage(size_t size) : mSize{size}
+    { al::uninitialized_default_construct_n(mArray, mSize); }
+    ~FlexArrayStorage() = default;
+
+    FlexArrayStorage(const FlexArrayStorage&) = delete;
+    FlexArrayStorage& operator=(const FlexArrayStorage&) = delete;
+};
+
+template<typename T, size_t alignment>
+struct FlexArrayStorage<T,alignment,false> {
+    const size_t mSize;
+    union {
+        char mDummy;
+        alignas(alignment) T mArray[1];
+    };
+
+    static constexpr size_t Sizeof(size_t count, size_t base) noexcept
+    {
+        const size_t len{sizeof(T)*count};
+        return std::max(offsetof(FlexArrayStorage,mArray)+len, sizeof(FlexArrayStorage)) + base;
+    }
+
+    FlexArrayStorage(size_t size) : mSize{size}
+    { al::uninitialized_default_construct_n(mArray, mSize); }
+    ~FlexArrayStorage() { al::destroy_n(mArray, mSize); }
+
+    FlexArrayStorage(const FlexArrayStorage&) = delete;
+    FlexArrayStorage& operator=(const FlexArrayStorage&) = delete;
+};
+
+/* A flexible array type. Used either standalone or at the end of a parent
+ * struct, with placement new, to have a run-time-sized array that's embedded
+ * with its size.
+ */
+template<typename T, size_t alignment=alignof(T)>
+struct FlexArray {
+    using element_type = T;
+    using value_type = std::remove_cv_t<T>;
+    using index_type = size_t;
+    using difference_type = ptrdiff_t;
+
+    using pointer = T*;
+    using const_pointer = const T*;
+    using reference = T&;
+    using const_reference = const T&;
+
+    using iterator = pointer;
+    using const_iterator = const_pointer;
+    using reverse_iterator = std::reverse_iterator<iterator>;
+    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+
+    using Storage_t_ = FlexArrayStorage<element_type,alignment>;
+
+    Storage_t_ mStore;
+
+    static constexpr index_type Sizeof(index_type count, index_type base=0u) noexcept
+    { return Storage_t_::Sizeof(count, base); }
+    static std::unique_ptr<FlexArray> Create(index_type count)
+    {
+        void *ptr{al_calloc(alignof(FlexArray), Sizeof(count))};
+        return std::unique_ptr<FlexArray>{al::construct_at(static_cast<FlexArray*>(ptr), count)};
+    }
+
+    FlexArray(index_type size) : mStore{size} { }
+    ~FlexArray() = default;
+
+    index_type size() const noexcept { return mStore.mSize; }
+    bool empty() const noexcept { return mStore.mSize == 0; }
+
+    pointer data() noexcept { return mStore.mArray; }
+    const_pointer data() const noexcept { return mStore.mArray; }
+
+    reference operator[](index_type i) noexcept { return mStore.mArray[i]; }
+    const_reference operator[](index_type i) const noexcept { return mStore.mArray[i]; }
+
+    reference front() noexcept { return mStore.mArray[0]; }
+    const_reference front() const noexcept { return mStore.mArray[0]; }
+
+    reference back() noexcept { return mStore.mArray[mStore.mSize-1]; }
+    const_reference back() const noexcept { return mStore.mArray[mStore.mSize-1]; }
+
+    iterator begin() noexcept { return mStore.mArray; }
+    const_iterator begin() const noexcept { return mStore.mArray; }
+    const_iterator cbegin() const noexcept { return mStore.mArray; }
+    iterator end() noexcept { return mStore.mArray + mStore.mSize; }
+    const_iterator end() const noexcept { return mStore.mArray + mStore.mSize; }
+    const_iterator cend() const noexcept { return mStore.mArray + mStore.mSize; }
+
+    reverse_iterator rbegin() noexcept { return end(); }
+    const_reverse_iterator rbegin() const noexcept { return end(); }
+    const_reverse_iterator crbegin() const noexcept { return cend(); }
+    reverse_iterator rend() noexcept { return begin(); }
+    const_reverse_iterator rend() const noexcept { return begin(); }
+    const_reverse_iterator crend() const noexcept { return cbegin(); }
+
+    DEF_PLACE_NEWDEL()
+};
+
+} // namespace al
+
+#endif /* AL_MALLOC_H */
diff --git a/common/alnumbers.h b/common/alnumbers.h
new file mode 100644 (file)
index 0000000..37a5541
--- /dev/null
@@ -0,0 +1,36 @@
+#ifndef COMMON_ALNUMBERS_H
+#define COMMON_ALNUMBERS_H
+
+#include <utility>
+
+namespace al {
+
+namespace numbers {
+
+namespace detail_ {
+    template<typename T>
+    using as_fp = std::enable_if_t<std::is_floating_point<T>::value, T>;
+} // detail_
+
+template<typename T>
+static constexpr auto pi_v = detail_::as_fp<T>(3.141592653589793238462643383279502884L);
+
+template<typename T>
+static constexpr auto inv_pi_v = detail_::as_fp<T>(0.318309886183790671537767526745028724L);
+
+template<typename T>
+static constexpr auto sqrt2_v = detail_::as_fp<T>(1.414213562373095048801688724209698079L);
+
+template<typename T>
+static constexpr auto sqrt3_v = detail_::as_fp<T>(1.732050807568877293527446341505872367L);
+
+static constexpr auto pi = pi_v<double>;
+static constexpr auto inv_pi = inv_pi_v<double>;
+static constexpr auto sqrt2 = sqrt2_v<double>;
+static constexpr auto sqrt3 = sqrt3_v<double>;
+
+} // namespace numbers
+
+} // namespace al
+
+#endif /* COMMON_ALNUMBERS_H */
diff --git a/common/alnumeric.h b/common/alnumeric.h
new file mode 100644 (file)
index 0000000..d6919e4
--- /dev/null
@@ -0,0 +1,308 @@
+#ifndef AL_NUMERIC_H
+#define AL_NUMERIC_H
+
+#include <algorithm>
+#include <cmath>
+#include <cstddef>
+#include <cstdint>
+#ifdef HAVE_INTRIN_H
+#include <intrin.h>
+#endif
+#ifdef HAVE_SSE_INTRINSICS
+#include <xmmintrin.h>
+#endif
+
+#include "altraits.h"
+#include "opthelpers.h"
+
+
+inline constexpr int64_t operator "" _i64(unsigned long long int n) noexcept { return static_cast<int64_t>(n); }
+inline constexpr uint64_t operator "" _u64(unsigned long long int n) noexcept { return static_cast<uint64_t>(n); }
+
+
+constexpr inline float minf(float a, float b) noexcept
+{ return ((a > b) ? b : a); }
+constexpr inline float maxf(float a, float b) noexcept
+{ return ((a > b) ? a : b); }
+constexpr inline float clampf(float val, float min, float max) noexcept
+{ return minf(max, maxf(min, val)); }
+
+constexpr inline double mind(double a, double b) noexcept
+{ return ((a > b) ? b : a); }
+constexpr inline double maxd(double a, double b) noexcept
+{ return ((a > b) ? a : b); }
+constexpr inline double clampd(double val, double min, double max) noexcept
+{ return mind(max, maxd(min, val)); }
+
+constexpr inline unsigned int minu(unsigned int a, unsigned int b) noexcept
+{ return ((a > b) ? b : a); }
+constexpr inline unsigned int maxu(unsigned int a, unsigned int b) noexcept
+{ return ((a > b) ? a : b); }
+constexpr inline unsigned int clampu(unsigned int val, unsigned int min, unsigned int max) noexcept
+{ return minu(max, maxu(min, val)); }
+
+constexpr inline int mini(int a, int b) noexcept
+{ return ((a > b) ? b : a); }
+constexpr inline int maxi(int a, int b) noexcept
+{ return ((a > b) ? a : b); }
+constexpr inline int clampi(int val, int min, int max) noexcept
+{ return mini(max, maxi(min, val)); }
+
+constexpr inline int64_t mini64(int64_t a, int64_t b) noexcept
+{ return ((a > b) ? b : a); }
+constexpr inline int64_t maxi64(int64_t a, int64_t b) noexcept
+{ return ((a > b) ? a : b); }
+constexpr inline int64_t clampi64(int64_t val, int64_t min, int64_t max) noexcept
+{ return mini64(max, maxi64(min, val)); }
+
+constexpr inline uint64_t minu64(uint64_t a, uint64_t b) noexcept
+{ return ((a > b) ? b : a); }
+constexpr inline uint64_t maxu64(uint64_t a, uint64_t b) noexcept
+{ return ((a > b) ? a : b); }
+constexpr inline uint64_t clampu64(uint64_t val, uint64_t min, uint64_t max) noexcept
+{ return minu64(max, maxu64(min, val)); }
+
+constexpr inline size_t minz(size_t a, size_t b) noexcept
+{ return ((a > b) ? b : a); }
+constexpr inline size_t maxz(size_t a, size_t b) noexcept
+{ return ((a > b) ? a : b); }
+constexpr inline size_t clampz(size_t val, size_t min, size_t max) noexcept
+{ return minz(max, maxz(min, val)); }
+
+
+constexpr inline float lerpf(float val1, float val2, float mu) noexcept
+{ return val1 + (val2-val1)*mu; }
+constexpr inline float cubic(float val1, float val2, float val3, float val4, float mu) noexcept
+{
+    const float mu2{mu*mu}, mu3{mu2*mu};
+    const float a0{-0.5f*mu3 +       mu2 + -0.5f*mu};
+    const float a1{ 1.5f*mu3 + -2.5f*mu2            + 1.0f};
+    const float a2{-1.5f*mu3 +  2.0f*mu2 +  0.5f*mu};
+    const float a3{ 0.5f*mu3 + -0.5f*mu2};
+    return val1*a0 + val2*a1 + val3*a2 + val4*a3;
+}
+
+
+/** Find the next power-of-2 for non-power-of-2 numbers. */
+inline uint32_t NextPowerOf2(uint32_t value) noexcept
+{
+    if(value > 0)
+    {
+        value--;
+        value |= value>>1;
+        value |= value>>2;
+        value |= value>>4;
+        value |= value>>8;
+        value |= value>>16;
+    }
+    return value+1;
+}
+
+/**
+ * If the value is not already a multiple of r, round down to the next
+ * multiple.
+ */
+template<typename T>
+constexpr T RoundDown(T value, al::type_identity_t<T> r) noexcept
+{ return value - (value%r); }
+
+/**
+ * If the value is not already a multiple of r, round up to the next multiple.
+ */
+template<typename T>
+constexpr T RoundUp(T value, al::type_identity_t<T> r) noexcept
+{ return RoundDown(value + r-1, r); }
+
+
+/**
+ * Fast float-to-int conversion. No particular rounding mode is assumed; the
+ * IEEE-754 default is round-to-nearest with ties-to-even, though an app could
+ * change it on its own threads. On some systems, a truncating conversion may
+ * always be the fastest method.
+ */
+inline int fastf2i(float f) noexcept
+{
+#if defined(HAVE_SSE_INTRINSICS)
+    return _mm_cvt_ss2si(_mm_set_ss(f));
+
+#elif defined(_MSC_VER) && defined(_M_IX86_FP)
+
+    int i;
+    __asm fld f
+    __asm fistp i
+    return i;
+
+#elif (defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__))
+
+    int i;
+#ifdef __SSE_MATH__
+    __asm__("cvtss2si %1, %0" : "=r"(i) : "x"(f));
+#else
+    __asm__ __volatile__("fistpl %0" : "=m"(i) : "t"(f) : "st");
+#endif
+    return i;
+
+#else
+
+    return static_cast<int>(f);
+#endif
+}
+inline unsigned int fastf2u(float f) noexcept
+{ return static_cast<unsigned int>(fastf2i(f)); }
+
+/** Converts float-to-int using standard behavior (truncation). */
+inline int float2int(float f) noexcept
+{
+#if defined(HAVE_SSE_INTRINSICS)
+    return _mm_cvtt_ss2si(_mm_set_ss(f));
+
+#elif (defined(_MSC_VER) && defined(_M_IX86_FP) && _M_IX86_FP == 0) \
+    || ((defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__)) \
+        && !defined(__SSE_MATH__))
+    int sign, shift, mant;
+    union {
+        float f;
+        int i;
+    } conv;
+
+    conv.f = f;
+    sign = (conv.i>>31) | 1;
+    shift = ((conv.i>>23)&0xff) - (127+23);
+
+    /* Over/underflow */
+    if(shift >= 31 || shift < -23) UNLIKELY
+        return 0;
+
+    mant = (conv.i&0x7fffff) | 0x800000;
+    if(shift < 0) LIKELY
+        return (mant >> -shift) * sign;
+    return (mant << shift) * sign;
+
+#else
+
+    return static_cast<int>(f);
+#endif
+}
+inline unsigned int float2uint(float f) noexcept
+{ return static_cast<unsigned int>(float2int(f)); }
+
+/** Converts double-to-int using standard behavior (truncation). */
+inline int double2int(double d) noexcept
+{
+#if defined(HAVE_SSE_INTRINSICS)
+    return _mm_cvttsd_si32(_mm_set_sd(d));
+
+#elif (defined(_MSC_VER) && defined(_M_IX86_FP) && _M_IX86_FP < 2) \
+    || ((defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__)) \
+        && !defined(__SSE2_MATH__))
+    int sign, shift;
+    int64_t mant;
+    union {
+        double d;
+        int64_t i64;
+    } conv;
+
+    conv.d = d;
+    sign = (conv.i64 >> 63) | 1;
+    shift = ((conv.i64 >> 52) & 0x7ff) - (1023 + 52);
+
+    /* Over/underflow */
+    if(shift >= 63 || shift < -52) UNLIKELY
+        return 0;
+
+    mant = (conv.i64 & 0xfffffffffffff_i64) | 0x10000000000000_i64;
+    if(shift < 0) LIKELY
+        return (int)(mant >> -shift) * sign;
+    return (int)(mant << shift) * sign;
+
+#else
+
+    return static_cast<int>(d);
+#endif
+}
+
+/**
+ * Rounds a float to the nearest integral value, according to the current
+ * rounding mode. This is essentially an inlined version of rintf, although
+ * makes fewer promises (e.g. -0 or -0.25 rounded to 0 may result in +0).
+ */
+inline float fast_roundf(float f) noexcept
+{
+#if (defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__)) \
+    && !defined(__SSE_MATH__)
+
+    float out;
+    __asm__ __volatile__("frndint" : "=t"(out) : "0"(f));
+    return out;
+
+#elif (defined(__GNUC__) || defined(__clang__)) && defined(__aarch64__)
+
+    float out;
+    __asm__ volatile("frintx %s0, %s1" : "=w"(out) : "w"(f));
+    return out;
+
+#else
+
+    /* Integral limit, where sub-integral precision is not available for
+     * floats.
+     */
+    static const float ilim[2]{
+         8388608.0f /*  0x1.0p+23 */,
+        -8388608.0f /* -0x1.0p+23 */
+    };
+    unsigned int sign, expo;
+    union {
+        float f;
+        unsigned int i;
+    } conv;
+
+    conv.f = f;
+    sign = (conv.i>>31)&0x01;
+    expo = (conv.i>>23)&0xff;
+
+    if(expo >= 150/*+23*/) UNLIKELY
+    {
+        /* An exponent (base-2) of 23 or higher is incapable of sub-integral
+         * precision, so it's already an integral value. We don't need to worry
+         * about infinity or NaN here.
+         */
+        return f;
+    }
+    /* Adding the integral limit to the value (with a matching sign) forces a
+     * result that has no sub-integral precision, and is consequently forced to
+     * round to an integral value. Removing the integral limit then restores
+     * the initial value rounded to the integral. The compiler should not
+     * optimize this out because of non-associative rules on floating-point
+     * math (as long as you don't use -fassociative-math,
+     * -funsafe-math-optimizations, -ffast-math, or -Ofast, in which case this
+     * may break).
+     */
+    f += ilim[sign];
+    return f - ilim[sign];
+#endif
+}
+
+
+template<typename T>
+constexpr const T& clamp(const T& value, const T& min_value, const T& max_value) noexcept
+{
+    return std::min(std::max(value, min_value), max_value);
+}
+
+// Converts level (mB) to gain.
+inline float level_mb_to_gain(float x)
+{
+    if(x <= -10'000.0f)
+        return 0.0f;
+    return std::pow(10.0f, x / 2'000.0f);
+}
+
+// Converts gain to level (mB).
+inline float gain_to_level_mb(float x)
+{
+    if (x <= 0.0f)
+        return -10'000.0f;
+    return maxf(std::log10(x) * 2'000.0f, -10'000.0f);
+}
+
+#endif /* AL_NUMERIC_H */
diff --git a/common/aloptional.h b/common/aloptional.h
new file mode 100644 (file)
index 0000000..6de1679
--- /dev/null
@@ -0,0 +1,353 @@
+#ifndef AL_OPTIONAL_H
+#define AL_OPTIONAL_H
+
+#include <initializer_list>
+#include <type_traits>
+#include <utility>
+
+#include "almalloc.h"
+
+namespace al {
+
+struct nullopt_t { };
+struct in_place_t { };
+
+constexpr nullopt_t nullopt{};
+constexpr in_place_t in_place{};
+
+#define NOEXCEPT_AS(...)  noexcept(noexcept(__VA_ARGS__))
+
+namespace detail_ {
+/* Base storage struct for an optional. Defines a trivial destructor, for types
+ * that can be trivially destructed.
+ */
+template<typename T, bool = std::is_trivially_destructible<T>::value>
+struct optstore_base {
+    bool mHasValue{false};
+    union {
+        char mDummy{};
+        T mValue;
+    };
+
+    constexpr optstore_base() noexcept { }
+    template<typename ...Args>
+    constexpr explicit optstore_base(in_place_t, Args&& ...args)
+        noexcept(std::is_nothrow_constructible<T, Args...>::value)
+        : mHasValue{true}, mValue{std::forward<Args>(args)...}
+    { }
+    ~optstore_base() = default;
+};
+
+/* Specialization needing a non-trivial destructor. */
+template<typename T>
+struct optstore_base<T, false> {
+    bool mHasValue{false};
+    union {
+        char mDummy{};
+        T mValue;
+    };
+
+    constexpr optstore_base() noexcept { }
+    template<typename ...Args>
+    constexpr explicit optstore_base(in_place_t, Args&& ...args)
+        noexcept(std::is_nothrow_constructible<T, Args...>::value)
+        : mHasValue{true}, mValue{std::forward<Args>(args)...}
+    { }
+    ~optstore_base() { if(mHasValue) al::destroy_at(std::addressof(mValue)); }
+};
+
+/* Next level of storage, which defines helpers to construct and destruct the
+ * stored object.
+ */
+template<typename T>
+struct optstore_helper : public optstore_base<T> {
+    using optstore_base<T>::optstore_base;
+
+    template<typename... Args>
+    constexpr void construct(Args&& ...args) noexcept(std::is_nothrow_constructible<T, Args...>::value)
+    {
+        al::construct_at(std::addressof(this->mValue), std::forward<Args>(args)...);
+        this->mHasValue = true;
+    }
+
+    constexpr void reset() noexcept
+    {
+        if(this->mHasValue)
+            al::destroy_at(std::addressof(this->mValue));
+        this->mHasValue = false;
+    }
+
+    constexpr void assign(const optstore_helper &rhs)
+        noexcept(std::is_nothrow_copy_constructible<T>::value
+            && std::is_nothrow_copy_assignable<T>::value)
+    {
+        if(!rhs.mHasValue)
+            this->reset();
+        else if(this->mHasValue)
+            this->mValue = rhs.mValue;
+        else
+            this->construct(rhs.mValue);
+    }
+
+    constexpr void assign(optstore_helper&& rhs)
+        noexcept(std::is_nothrow_move_constructible<T>::value
+            && std::is_nothrow_move_assignable<T>::value)
+    {
+        if(!rhs.mHasValue)
+            this->reset();
+        else if(this->mHasValue)
+            this->mValue = std::move(rhs.mValue);
+        else
+            this->construct(std::move(rhs.mValue));
+    }
+};
+
+/* Define copy and move constructors and assignment operators, which may or may
+ * not be trivial.
+ */
+template<typename T, bool trivial_copy = std::is_trivially_copy_constructible<T>::value,
+    bool trivial_move = std::is_trivially_move_constructible<T>::value,
+    /* Trivial assignment is dependent on trivial construction+destruction. */
+    bool = trivial_copy && std::is_trivially_copy_assignable<T>::value
+        && std::is_trivially_destructible<T>::value,
+    bool = trivial_move && std::is_trivially_move_assignable<T>::value
+        && std::is_trivially_destructible<T>::value>
+struct optional_storage;
+
+/* Some versions of GCC have issues with 'this' in the following noexcept(...)
+ * statements, so this macro is a workaround.
+ */
+#define _this std::declval<optional_storage*>()
+
+/* Completely trivial. */
+template<typename T>
+struct optional_storage<T, true, true, true, true> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage&) = default;
+    constexpr optional_storage(optional_storage&&) = default;
+    constexpr optional_storage& operator=(const optional_storage&) = default;
+    constexpr optional_storage& operator=(optional_storage&&) = default;
+};
+
+/* Non-trivial move assignment. */
+template<typename T>
+struct optional_storage<T, true, true, true, false> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage&) = default;
+    constexpr optional_storage(optional_storage&&) = default;
+    constexpr optional_storage& operator=(const optional_storage&) = default;
+    constexpr optional_storage& operator=(optional_storage&& rhs) NOEXCEPT_AS(_this->assign(std::move(rhs)))
+    { this->assign(std::move(rhs)); return *this; }
+};
+
+/* Non-trivial move construction. */
+template<typename T>
+struct optional_storage<T, true, false, true, false> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage&) = default;
+    constexpr optional_storage(optional_storage&& rhs) NOEXCEPT_AS(_this->construct(std::move(rhs.mValue)))
+    { if(rhs.mHasValue) this->construct(std::move(rhs.mValue)); }
+    constexpr optional_storage& operator=(const optional_storage&) = default;
+    constexpr optional_storage& operator=(optional_storage&& rhs) NOEXCEPT_AS(_this->assign(std::move(rhs)))
+    { this->assign(std::move(rhs)); return *this; }
+};
+
+/* Non-trivial copy assignment. */
+template<typename T>
+struct optional_storage<T, true, true, false, true> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage&) = default;
+    constexpr optional_storage(optional_storage&&) = default;
+    constexpr optional_storage& operator=(const optional_storage &rhs) NOEXCEPT_AS(_this->assign(rhs))
+    { this->assign(rhs); return *this; }
+    constexpr optional_storage& operator=(optional_storage&&) = default;
+};
+
+/* Non-trivial copy construction. */
+template<typename T>
+struct optional_storage<T, false, true, false, true> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage &rhs) NOEXCEPT_AS(_this->construct(rhs.mValue))
+    { if(rhs.mHasValue) this->construct(rhs.mValue); }
+    constexpr optional_storage(optional_storage&&) = default;
+    constexpr optional_storage& operator=(const optional_storage &rhs) NOEXCEPT_AS(_this->assign(rhs))
+    { this->assign(rhs); return *this; }
+    constexpr optional_storage& operator=(optional_storage&&) = default;
+};
+
+/* Non-trivial assignment. */
+template<typename T>
+struct optional_storage<T, true, true, false, false> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage&) = default;
+    constexpr optional_storage(optional_storage&&) = default;
+    constexpr optional_storage& operator=(const optional_storage &rhs) NOEXCEPT_AS(_this->assign(rhs))
+    { this->assign(rhs); return *this; }
+    constexpr optional_storage& operator=(optional_storage&& rhs) NOEXCEPT_AS(_this->assign(std::move(rhs)))
+    { this->assign(std::move(rhs)); return *this; }
+};
+
+/* Non-trivial assignment, non-trivial move construction. */
+template<typename T>
+struct optional_storage<T, true, false, false, false> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage&) = default;
+    constexpr optional_storage(optional_storage&& rhs) NOEXCEPT_AS(_this->construct(std::move(rhs.mValue)))
+    { if(rhs.mHasValue) this->construct(std::move(rhs.mValue)); }
+    constexpr optional_storage& operator=(const optional_storage &rhs) NOEXCEPT_AS(_this->assign(rhs))
+    { this->assign(rhs); return *this; }
+    constexpr optional_storage& operator=(optional_storage&& rhs) NOEXCEPT_AS(_this->assign(std::move(rhs)))
+    { this->assign(std::move(rhs)); return *this; }
+};
+
+/* Non-trivial assignment, non-trivial copy construction. */
+template<typename T>
+struct optional_storage<T, false, true, false, false> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage &rhs) NOEXCEPT_AS(_this->construct(rhs.mValue))
+    { if(rhs.mHasValue) this->construct(rhs.mValue); }
+    constexpr optional_storage(optional_storage&&) = default;
+    constexpr optional_storage& operator=(const optional_storage &rhs) NOEXCEPT_AS(_this->assign(rhs))
+    { this->assign(rhs); return *this; }
+    constexpr optional_storage& operator=(optional_storage&& rhs) NOEXCEPT_AS(_this->assign(std::move(rhs)))
+    { this->assign(std::move(rhs)); return *this; }
+};
+
+/* Completely non-trivial. */
+template<typename T>
+struct optional_storage<T, false, false, false, false> : public optstore_helper<T> {
+    using optstore_helper<T>::optstore_helper;
+    constexpr optional_storage() noexcept = default;
+    constexpr optional_storage(const optional_storage &rhs) NOEXCEPT_AS(_this->construct(rhs.mValue))
+    { if(rhs.mHasValue) this->construct(rhs.mValue); }
+    constexpr optional_storage(optional_storage&& rhs) NOEXCEPT_AS(_this->construct(std::move(rhs.mValue)))
+    { if(rhs.mHasValue) this->construct(std::move(rhs.mValue)); }
+    constexpr optional_storage& operator=(const optional_storage &rhs) NOEXCEPT_AS(_this->assign(rhs))
+    { this->assign(rhs); return *this; }
+    constexpr optional_storage& operator=(optional_storage&& rhs) NOEXCEPT_AS(_this->assign(std::move(rhs)))
+    { this->assign(std::move(rhs)); return *this; }
+};
+
+#undef _this
+
+} // namespace detail_
+
+#define REQUIRES(...) std::enable_if_t<(__VA_ARGS__),bool> = true
+
+template<typename T>
+class optional {
+    using storage_t = detail_::optional_storage<T>;
+
+    storage_t mStore{};
+
+public:
+    using value_type = T;
+
+    constexpr optional() = default;
+    constexpr optional(const optional&) = default;
+    constexpr optional(optional&&) = default;
+    constexpr optional(nullopt_t) noexcept { }
+    template<typename ...Args>
+    constexpr explicit optional(in_place_t, Args&& ...args)
+        NOEXCEPT_AS(storage_t{al::in_place, std::forward<Args>(args)...})
+        : mStore{al::in_place, std::forward<Args>(args)...}
+    { }
+    template<typename U, REQUIRES(std::is_constructible<T, U&&>::value
+        && !std::is_same<std::decay_t<U>, al::in_place_t>::value
+        && !std::is_same<std::decay_t<U>, optional<T>>::value
+        && std::is_convertible<U&&, T>::value)>
+    constexpr optional(U&& rhs) NOEXCEPT_AS(storage_t{al::in_place, std::forward<U>(rhs)})
+        : mStore{al::in_place, std::forward<U>(rhs)}
+    { }
+    template<typename U, REQUIRES(std::is_constructible<T, U&&>::value
+        && !std::is_same<std::decay_t<U>, al::in_place_t>::value
+        && !std::is_same<std::decay_t<U>, optional<T>>::value
+        && !std::is_convertible<U&&, T>::value)>
+    constexpr explicit optional(U&& rhs) NOEXCEPT_AS(storage_t{al::in_place, std::forward<U>(rhs)})
+        : mStore{al::in_place, std::forward<U>(rhs)}
+    { }
+    ~optional() = default;
+
+    constexpr optional& operator=(const optional&) = default;
+    constexpr optional& operator=(optional&&) = default;
+    constexpr optional& operator=(nullopt_t) noexcept { mStore.reset(); return *this; }
+    template<typename U=T>
+    constexpr std::enable_if_t<std::is_constructible<T, U>::value
+        && std::is_assignable<T&, U>::value
+        && !std::is_same<std::decay_t<U>, optional<T>>::value
+        && (!std::is_same<std::decay_t<U>, T>::value || !std::is_scalar<U>::value),
+    optional&> operator=(U&& rhs)
+    {
+        if(mStore.mHasValue)
+            mStore.mValue = std::forward<U>(rhs);
+        else
+            mStore.construct(std::forward<U>(rhs));
+        return *this;
+    }
+
+    constexpr const T* operator->() const { return std::addressof(mStore.mValue); }
+    constexpr T* operator->() { return std::addressof(mStore.mValue); }
+    constexpr const T& operator*() const& { return mStore.mValue; }
+    constexpr T& operator*() & { return mStore.mValue; }
+    constexpr const T&& operator*() const&& { return std::move(mStore.mValue); }
+    constexpr T&& operator*() && { return std::move(mStore.mValue); }
+
+    constexpr explicit operator bool() const noexcept { return mStore.mHasValue; }
+    constexpr bool has_value() const noexcept { return mStore.mHasValue; }
+
+    constexpr T& value() & { return mStore.mValue; }
+    constexpr const T& value() const& { return mStore.mValue; }
+    constexpr T&& value() && { return std::move(mStore.mValue); }
+    constexpr const T&& value() const&& { return std::move(mStore.mValue); }
+
+    template<typename U>
+    constexpr T value_or(U&& defval) const&
+    { return bool(*this) ? **this : static_cast<T>(std::forward<U>(defval)); }
+    template<typename U>
+    constexpr T value_or(U&& defval) &&
+    { return bool(*this) ? std::move(**this) : static_cast<T>(std::forward<U>(defval)); }
+
+    template<typename ...Args>
+    constexpr T& emplace(Args&& ...args)
+    {
+        mStore.reset();
+        mStore.construct(std::forward<Args>(args)...);
+        return mStore.mValue;
+    }
+    template<typename U, typename ...Args>
+    constexpr std::enable_if_t<std::is_constructible<T, std::initializer_list<U>&, Args&&...>::value,
+    T&> emplace(std::initializer_list<U> il, Args&& ...args)
+    {
+        mStore.reset();
+        mStore.construct(il, std::forward<Args>(args)...);
+        return mStore.mValue;
+    }
+
+    constexpr void reset() noexcept { mStore.reset(); }
+};
+
+template<typename T>
+constexpr optional<std::decay_t<T>> make_optional(T&& arg)
+{ return optional<std::decay_t<T>>{in_place, std::forward<T>(arg)}; }
+
+template<typename T, typename... Args>
+constexpr optional<T> make_optional(Args&& ...args)
+{ return optional<T>{in_place, std::forward<Args>(args)...}; }
+
+template<typename T, typename U, typename... Args>
+constexpr optional<T> make_optional(std::initializer_list<U> il, Args&& ...args)
+{ return optional<T>{in_place, il, std::forward<Args>(args)...}; }
+
+#undef REQUIRES
+#undef NOEXCEPT_AS
+} // namespace al
+
+#endif /* AL_OPTIONAL_H */
diff --git a/common/alspan.h b/common/alspan.h
new file mode 100644 (file)
index 0000000..1d6cdfe
--- /dev/null
@@ -0,0 +1,354 @@
+#ifndef AL_SPAN_H
+#define AL_SPAN_H
+
+#include <array>
+#include <cstddef>
+#include <initializer_list>
+#include <iterator>
+#include <type_traits>
+
+#include "almalloc.h"
+#include "altraits.h"
+
+namespace al {
+
+template<typename T>
+constexpr auto size(const T &cont) noexcept(noexcept(cont.size())) -> decltype(cont.size())
+{ return cont.size(); }
+
+template<typename T, size_t N>
+constexpr size_t size(const T (&)[N]) noexcept
+{ return N; }
+
+
+template<typename T>
+constexpr auto data(T &cont) noexcept(noexcept(cont.data())) -> decltype(cont.data())
+{ return cont.data(); }
+
+template<typename T>
+constexpr auto data(const T &cont) noexcept(noexcept(cont.data())) -> decltype(cont.data())
+{ return cont.data(); }
+
+template<typename T, size_t N>
+constexpr T* data(T (&arr)[N]) noexcept
+{ return arr; }
+
+template<typename T>
+constexpr const T* data(std::initializer_list<T> list) noexcept
+{ return list.begin(); }
+
+
+constexpr size_t dynamic_extent{static_cast<size_t>(-1)};
+
+template<typename T, size_t E=dynamic_extent>
+class span;
+
+namespace detail_ {
+    template<typename... Ts>
+    using void_t = void;
+
+    template<typename T>
+    struct is_span_ : std::false_type { };
+    template<typename T, size_t E>
+    struct is_span_<span<T,E>> : std::true_type { };
+    template<typename T>
+    constexpr bool is_span_v = is_span_<std::remove_cv_t<T>>::value;
+
+    template<typename T>
+    struct is_std_array_ : std::false_type { };
+    template<typename T, size_t N>
+    struct is_std_array_<std::array<T,N>> : std::true_type { };
+    template<typename T>
+    constexpr bool is_std_array_v = is_std_array_<std::remove_cv_t<T>>::value;
+
+    template<typename T, typename = void>
+    constexpr bool has_size_and_data = false;
+    template<typename T>
+    constexpr bool has_size_and_data<T,
+        void_t<decltype(al::size(std::declval<T>())), decltype(al::data(std::declval<T>()))>>
+        = true;
+
+    template<typename T, typename U>
+    constexpr bool is_array_compatible = std::is_convertible<T(*)[],U(*)[]>::value;
+
+    template<typename C, typename T>
+    constexpr bool is_valid_container = !is_span_v<C> && !is_std_array_v<C>
+        && !std::is_array<C>::value && has_size_and_data<C>
+        && is_array_compatible<std::remove_pointer_t<decltype(al::data(std::declval<C&>()))>,T>;
+} // namespace detail_
+
+#define REQUIRES(...) std::enable_if_t<(__VA_ARGS__),bool> = true
+
+template<typename T, size_t E>
+class span {
+public:
+    using element_type = T;
+    using value_type = std::remove_cv_t<T>;
+    using index_type = size_t;
+    using difference_type = ptrdiff_t;
+
+    using pointer = T*;
+    using const_pointer = const T*;
+    using reference = T&;
+    using const_reference = const T&;
+
+    using iterator = pointer;
+    using const_iterator = const_pointer;
+    using reverse_iterator = std::reverse_iterator<iterator>;
+    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+
+    static constexpr size_t extent{E};
+
+    template<bool is0=(extent == 0), REQUIRES(is0)>
+    constexpr span() noexcept { }
+    template<typename U>
+    constexpr explicit span(U iter, index_type) : mData{to_address(iter)} { }
+    template<typename U, typename V, REQUIRES(!std::is_convertible<V,size_t>::value)>
+    constexpr explicit span(U first, V) : mData{to_address(first)} { }
+
+    constexpr span(type_identity_t<element_type> (&arr)[E]) noexcept
+        : span{al::data(arr), al::size(arr)}
+    { }
+    constexpr span(std::array<value_type,E> &arr) noexcept : span{al::data(arr), al::size(arr)} { }
+    template<typename U=T, REQUIRES(std::is_const<U>::value)>
+    constexpr span(const std::array<value_type,E> &arr) noexcept
+      : span{al::data(arr), al::size(arr)}
+    { }
+
+    template<typename U, REQUIRES(detail_::is_valid_container<U, element_type>)>
+    constexpr explicit span(U&& cont) : span{al::data(cont), al::size(cont)} { }
+
+    template<typename U, index_type N, REQUIRES(!std::is_same<element_type,U>::value
+        && detail_::is_array_compatible<U,element_type> && N == dynamic_extent)>
+    constexpr explicit span(const span<U,N> &span_) noexcept
+        : span{al::data(span_), al::size(span_)}
+    { }
+    template<typename U, index_type N, REQUIRES(!std::is_same<element_type,U>::value
+        && detail_::is_array_compatible<U,element_type> && N == extent)>
+    constexpr span(const span<U,N> &span_) noexcept : span{al::data(span_), al::size(span_)} { }
+    constexpr span(const span&) noexcept = default;
+
+    constexpr span& operator=(const span &rhs) noexcept = default;
+
+    constexpr reference front() const { return *mData; }
+    constexpr reference back() const { return *(mData+E-1); }
+    constexpr reference operator[](index_type idx) const { return mData[idx]; }
+    constexpr pointer data() const noexcept { return mData; }
+
+    constexpr index_type size() const noexcept { return E; }
+    constexpr index_type size_bytes() const noexcept { return E * sizeof(value_type); }
+    constexpr bool empty() const noexcept { return E == 0; }
+
+    constexpr iterator begin() const noexcept { return mData; }
+    constexpr iterator end() const noexcept { return mData+E; }
+    constexpr const_iterator cbegin() const noexcept { return mData; }
+    constexpr const_iterator cend() const noexcept { return mData+E; }
+
+    constexpr reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; }
+    constexpr reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; }
+    constexpr const_reverse_iterator crbegin() const noexcept
+    { return const_reverse_iterator{cend()}; }
+    constexpr const_reverse_iterator crend() const noexcept
+    { return const_reverse_iterator{cbegin()}; }
+
+    template<size_t C>
+    constexpr span<element_type,C> first() const
+    {
+        static_assert(E >= C, "New size exceeds original capacity");
+        return span<element_type,C>{mData, C};
+    }
+
+    template<size_t C>
+    constexpr span<element_type,C> last() const
+    {
+        static_assert(E >= C, "New size exceeds original capacity");
+        return span<element_type,C>{mData+(E-C), C};
+    }
+
+    template<size_t O, size_t C>
+    constexpr auto subspan() const -> std::enable_if_t<C!=dynamic_extent,span<element_type,C>>
+    {
+        static_assert(E >= O, "Offset exceeds extent");
+        static_assert(E-O >= C, "New size exceeds original capacity");
+        return span<element_type,C>{mData+O, C};
+    }
+
+    template<size_t O, size_t C=dynamic_extent>
+    constexpr auto subspan() const -> std::enable_if_t<C==dynamic_extent,span<element_type,E-O>>
+    {
+        static_assert(E >= O, "Offset exceeds extent");
+        return span<element_type,E-O>{mData+O, E-O};
+    }
+
+    /* NOTE: Can't declare objects of a specialized template class prior to
+     * defining the specialization. As a result, these methods need to be
+     * defined later.
+     */
+    constexpr span<element_type,dynamic_extent> first(size_t count) const;
+    constexpr span<element_type,dynamic_extent> last(size_t count) const;
+    constexpr span<element_type,dynamic_extent> subspan(size_t offset,
+        size_t count=dynamic_extent) const;
+
+private:
+    pointer mData{nullptr};
+};
+
+template<typename T>
+class span<T,dynamic_extent> {
+public:
+    using element_type = T;
+    using value_type = std::remove_cv_t<T>;
+    using index_type = size_t;
+    using difference_type = ptrdiff_t;
+
+    using pointer = T*;
+    using const_pointer = const T*;
+    using reference = T&;
+    using const_reference = const T&;
+
+    using iterator = pointer;
+    using const_iterator = const_pointer;
+    using reverse_iterator = std::reverse_iterator<iterator>;
+    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+
+    static constexpr size_t extent{dynamic_extent};
+
+    constexpr span() noexcept = default;
+    template<typename U>
+    constexpr span(U iter, index_type count)
+        : mData{to_address(iter)}, mDataEnd{to_address(iter)+count}
+    { }
+    template<typename U, typename V, REQUIRES(!std::is_convertible<V,size_t>::value)>
+    constexpr span(U first, V last) : span{to_address(first), static_cast<size_t>(last-first)}
+    { }
+
+    template<size_t N>
+    constexpr span(type_identity_t<element_type> (&arr)[N]) noexcept
+        : span{al::data(arr), al::size(arr)}
+    { }
+    template<size_t N>
+    constexpr span(std::array<value_type,N> &arr) noexcept : span{al::data(arr), al::size(arr)} { }
+    template<size_t N, typename U=T, REQUIRES(std::is_const<U>::value)>
+    constexpr span(const std::array<value_type,N> &arr) noexcept
+      : span{al::data(arr), al::size(arr)}
+    { }
+
+    template<typename U, REQUIRES(detail_::is_valid_container<U, element_type>)>
+    constexpr span(U&& cont) : span{al::data(cont), al::size(cont)} { }
+
+    template<typename U, size_t N, REQUIRES((!std::is_same<element_type,U>::value || extent != N)
+        && detail_::is_array_compatible<U,element_type>)>
+    constexpr span(const span<U,N> &span_) noexcept : span{al::data(span_), al::size(span_)} { }
+    constexpr span(const span&) noexcept = default;
+
+    constexpr span& operator=(const span &rhs) noexcept = default;
+
+    constexpr reference front() const { return *mData; }
+    constexpr reference back() const { return *(mDataEnd-1); }
+    constexpr reference operator[](index_type idx) const { return mData[idx]; }
+    constexpr pointer data() const noexcept { return mData; }
+
+    constexpr index_type size() const noexcept { return static_cast<index_type>(mDataEnd-mData); }
+    constexpr index_type size_bytes() const noexcept
+    { return static_cast<index_type>(mDataEnd-mData) * sizeof(value_type); }
+    constexpr bool empty() const noexcept { return mData == mDataEnd; }
+
+    constexpr iterator begin() const noexcept { return mData; }
+    constexpr iterator end() const noexcept { return mDataEnd; }
+    constexpr const_iterator cbegin() const noexcept { return mData; }
+    constexpr const_iterator cend() const noexcept { return mDataEnd; }
+
+    constexpr reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; }
+    constexpr reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; }
+    constexpr const_reverse_iterator crbegin() const noexcept
+    { return const_reverse_iterator{cend()}; }
+    constexpr const_reverse_iterator crend() const noexcept
+    { return const_reverse_iterator{cbegin()}; }
+
+    template<size_t C>
+    constexpr span<element_type,C> first() const
+    { return span<element_type,C>{mData, C}; }
+
+    constexpr span first(size_t count) const
+    { return (count >= size()) ? *this : span{mData, mData+count}; }
+
+    template<size_t C>
+    constexpr span<element_type,C> last() const
+    { return span<element_type,C>{mDataEnd-C, C}; }
+
+    constexpr span last(size_t count) const
+    { return (count >= size()) ? *this : span{mDataEnd-count, mDataEnd}; }
+
+    template<size_t O, size_t C>
+    constexpr auto subspan() const -> std::enable_if_t<C!=dynamic_extent,span<element_type,C>>
+    { return span<element_type,C>{mData+O, C}; }
+
+    template<size_t O, size_t C=dynamic_extent>
+    constexpr auto subspan() const -> std::enable_if_t<C==dynamic_extent,span<element_type,C>>
+    { return span<element_type,C>{mData+O, mDataEnd}; }
+
+    constexpr span subspan(size_t offset, size_t count=dynamic_extent) const
+    {
+        return (offset > size()) ? span{} :
+            (count >= size()-offset) ? span{mData+offset, mDataEnd} :
+            span{mData+offset, mData+offset+count};
+    }
+
+private:
+    pointer mData{nullptr};
+    pointer mDataEnd{nullptr};
+};
+
+template<typename T, size_t E>
+constexpr inline auto span<T,E>::first(size_t count) const -> span<element_type,dynamic_extent>
+{
+    return (count >= size()) ? span<element_type>{mData, extent} :
+        span<element_type>{mData, count};
+}
+
+template<typename T, size_t E>
+constexpr inline auto span<T,E>::last(size_t count) const -> span<element_type,dynamic_extent>
+{
+    return (count >= size()) ? span<element_type>{mData, extent} :
+        span<element_type>{mData+extent-count, count};
+}
+
+template<typename T, size_t E>
+constexpr inline auto span<T,E>::subspan(size_t offset, size_t count) const
+    -> span<element_type,dynamic_extent>
+{
+    return (offset > size()) ? span<element_type>{} :
+        (count >= size()-offset) ? span<element_type>{mData+offset, mData+extent} :
+        span<element_type>{mData+offset, mData+offset+count};
+}
+
+/* Helpers to deal with the lack of user-defined deduction guides (C++17). */
+template<typename T, typename U>
+constexpr auto as_span(T ptr, U count_or_end)
+{
+    using value_type = typename std::pointer_traits<T>::element_type;
+    return span<value_type>{ptr, count_or_end};
+}
+template<typename T, size_t N>
+constexpr auto as_span(T (&arr)[N]) noexcept { return span<T,N>{al::data(arr), al::size(arr)}; }
+template<typename T, size_t N>
+constexpr auto as_span(std::array<T,N> &arr) noexcept
+{ return span<T,N>{al::data(arr), al::size(arr)}; }
+template<typename T, size_t N>
+constexpr auto as_span(const std::array<T,N> &arr) noexcept
+{ return span<std::add_const_t<T>,N>{al::data(arr), al::size(arr)}; }
+template<typename U, REQUIRES(!detail_::is_span_v<U> && !detail_::is_std_array_v<U>
+    && !std::is_array<U>::value && detail_::has_size_and_data<U>)>
+constexpr auto as_span(U&& cont)
+{
+    using value_type = std::remove_pointer_t<decltype(al::data(std::declval<U&>()))>;
+    return span<value_type>{al::data(cont), al::size(cont)};
+}
+template<typename T, size_t N>
+constexpr auto as_span(span<T,N> span_) noexcept { return span_; }
+
+#undef REQUIRES
+
+} // namespace al
+
+#endif /* AL_SPAN_H */
diff --git a/common/alstring.cpp b/common/alstring.cpp
new file mode 100644 (file)
index 0000000..4a84be1
--- /dev/null
@@ -0,0 +1,45 @@
+
+#include "config.h"
+
+#include "alstring.h"
+
+#include <cctype>
+#include <string>
+
+
+namespace {
+
+int to_upper(const char ch)
+{
+    using char8_traits = std::char_traits<char>;
+    return std::toupper(char8_traits::to_int_type(ch));
+}
+
+} // namespace
+
+namespace al {
+
+int strcasecmp(const char *str0, const char *str1) noexcept
+{
+    do {
+        const int diff{to_upper(*str0) - to_upper(*str1)};
+        if(diff < 0) return -1;
+        if(diff > 0) return 1;
+    } while(*(str0++) && *(str1++));
+    return 0;
+}
+
+int strncasecmp(const char *str0, const char *str1, std::size_t len) noexcept
+{
+    if(len > 0)
+    {
+        do {
+            const int diff{to_upper(*str0) - to_upper(*str1)};
+            if(diff < 0) return -1;
+            if(diff > 0) return 1;
+        } while(--len && *(str0++) && *(str1++));
+    }
+    return 0;
+}
+
+} // namespace al
diff --git a/common/alstring.h b/common/alstring.h
new file mode 100644 (file)
index 0000000..6c5004e
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef AL_STRING_H
+#define AL_STRING_H
+
+#include <cstddef>
+#include <cstring>
+
+
+namespace al {
+
+/* These would be better served by using a string_view-like span/view with
+ * case-insensitive char traits.
+ */
+int strcasecmp(const char *str0, const char *str1) noexcept;
+int strncasecmp(const char *str0, const char *str1, std::size_t len) noexcept;
+
+} // namespace al
+
+#endif /* AL_STRING_H */
diff --git a/common/altraits.h b/common/altraits.h
new file mode 100644 (file)
index 0000000..7ce0422
--- /dev/null
@@ -0,0 +1,14 @@
+#ifndef COMMON_ALTRAITS_H
+#define COMMON_ALTRAITS_H
+
+namespace al {
+
+template<typename T>
+struct type_identity { using type = T; };
+
+template<typename T>
+using type_identity_t = typename type_identity<T>::type;
+
+} // namespace al
+
+#endif /* COMMON_ALTRAITS_H */
diff --git a/common/atomic.h b/common/atomic.h
new file mode 100644 (file)
index 0000000..5e9b04c
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef AL_ATOMIC_H
+#define AL_ATOMIC_H
+
+#include <atomic>
+
+
+using RefCount = std::atomic<unsigned int>;
+
+inline void InitRef(RefCount &ref, unsigned int value)
+{ ref.store(value, std::memory_order_relaxed); }
+inline unsigned int ReadRef(RefCount &ref)
+{ return ref.load(std::memory_order_acquire); }
+inline unsigned int IncrementRef(RefCount &ref)
+{ return ref.fetch_add(1u, std::memory_order_acq_rel)+1u; }
+inline unsigned int DecrementRef(RefCount &ref)
+{ return ref.fetch_sub(1u, std::memory_order_acq_rel)-1u; }
+
+
+/* WARNING: A livelock is theoretically possible if another thread keeps
+ * changing the head without giving this a chance to actually swap in the new
+ * one (practically impossible with this little code, but...).
+ */
+template<typename T>
+inline void AtomicReplaceHead(std::atomic<T> &head, T newhead)
+{
+    T first_ = head.load(std::memory_order_acquire);
+    do {
+        newhead->next.store(first_, std::memory_order_relaxed);
+    } while(!head.compare_exchange_weak(first_, newhead,
+            std::memory_order_acq_rel, std::memory_order_acquire));
+}
+
+#endif /* AL_ATOMIC_H */
diff --git a/common/comptr.h b/common/comptr.h
new file mode 100644 (file)
index 0000000..cdc6dec
--- /dev/null
@@ -0,0 +1,68 @@
+#ifndef COMMON_COMPTR_H
+#define COMMON_COMPTR_H
+
+#include <cstddef>
+#include <utility>
+
+#include "opthelpers.h"
+
+
+template<typename T>
+class ComPtr {
+    T *mPtr{nullptr};
+
+public:
+    ComPtr() noexcept = default;
+    ComPtr(const ComPtr &rhs) : mPtr{rhs.mPtr} { if(mPtr) mPtr->AddRef(); }
+    ComPtr(ComPtr&& rhs) noexcept : mPtr{rhs.mPtr} { rhs.mPtr = nullptr; }
+    ComPtr(std::nullptr_t) noexcept { }
+    explicit ComPtr(T *ptr) noexcept : mPtr{ptr} { }
+    ~ComPtr() { if(mPtr) mPtr->Release(); }
+
+    ComPtr& operator=(const ComPtr &rhs)
+    {
+        if(!rhs.mPtr)
+        {
+            if(mPtr)
+                mPtr->Release();
+            mPtr = nullptr;
+        }
+        else
+        {
+            rhs.mPtr->AddRef();
+            try {
+                if(mPtr)
+                    mPtr->Release();
+                mPtr = rhs.mPtr;
+            }
+            catch(...) {
+                rhs.mPtr->Release();
+                throw;
+            }
+        }
+        return *this;
+    }
+    ComPtr& operator=(ComPtr&& rhs)
+    {
+        if(&rhs != this) LIKELY
+        {
+            if(mPtr) mPtr->Release();
+            mPtr = std::exchange(rhs.mPtr, nullptr);
+        }
+        return *this;
+    }
+
+    explicit operator bool() const noexcept { return mPtr != nullptr; }
+
+    T& operator*() const noexcept { return *mPtr; }
+    T* operator->() const noexcept { return mPtr; }
+    T* get() const noexcept { return mPtr; }
+    T** getPtr() noexcept { return &mPtr; }
+
+    T* release() noexcept { return std::exchange(mPtr, nullptr); }
+
+    void swap(ComPtr &rhs) noexcept { std::swap(mPtr, rhs.mPtr); }
+    void swap(ComPtr&& rhs) noexcept { std::swap(mPtr, rhs.mPtr); }
+};
+
+#endif
diff --git a/common/dynload.cpp b/common/dynload.cpp
new file mode 100644 (file)
index 0000000..f1c2a7e
--- /dev/null
@@ -0,0 +1,44 @@
+
+#include "config.h"
+
+#include "dynload.h"
+
+#include "strutils.h"
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+void *LoadLib(const char *name)
+{
+    std::wstring wname{utf8_to_wstr(name)};
+    return LoadLibraryW(wname.c_str());
+}
+void CloseLib(void *handle)
+{ FreeLibrary(static_cast<HMODULE>(handle)); }
+void *GetSymbol(void *handle, const char *name)
+{ return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(handle), name)); }
+
+#elif defined(HAVE_DLFCN_H)
+
+#include <dlfcn.h>
+
+void *LoadLib(const char *name)
+{
+    dlerror();
+    void *handle{dlopen(name, RTLD_NOW)};
+    const char *err{dlerror()};
+    if(err) handle = nullptr;
+    return handle;
+}
+void CloseLib(void *handle)
+{ dlclose(handle); }
+void *GetSymbol(void *handle, const char *name)
+{
+    dlerror();
+    void *sym{dlsym(handle, name)};
+    const char *err{dlerror()};
+    if(err) sym = nullptr;
+    return sym;
+}
+#endif
diff --git a/common/dynload.h b/common/dynload.h
new file mode 100644 (file)
index 0000000..bd9e86f
--- /dev/null
@@ -0,0 +1,14 @@
+#ifndef AL_DYNLOAD_H
+#define AL_DYNLOAD_H
+
+#if defined(_WIN32) || defined(HAVE_DLFCN_H)
+
+#define HAVE_DYNLOAD
+
+void *LoadLib(const char *name);
+void CloseLib(void *handle);
+void *GetSymbol(void *handle, const char *name);
+
+#endif
+
+#endif /* AL_DYNLOAD_H */
diff --git a/common/intrusive_ptr.h b/common/intrusive_ptr.h
new file mode 100644 (file)
index 0000000..2707534
--- /dev/null
@@ -0,0 +1,120 @@
+#ifndef INTRUSIVE_PTR_H
+#define INTRUSIVE_PTR_H
+
+#include <utility>
+
+#include "atomic.h"
+#include "opthelpers.h"
+
+
+namespace al {
+
+template<typename T>
+class intrusive_ref {
+    RefCount mRef{1u};
+
+public:
+    unsigned int add_ref() noexcept { return IncrementRef(mRef); }
+    unsigned int dec_ref() noexcept
+    {
+        auto ref = DecrementRef(mRef);
+        if(ref == 0) UNLIKELY
+            delete static_cast<T*>(this);
+        return ref;
+    }
+
+    /**
+     * Release only if doing so would not bring the object to 0 references and
+     * delete it. Returns false if the object could not be released.
+     *
+     * NOTE: The caller is responsible for handling a failed release, as it
+     * means the object has no other references and needs to be be deleted
+     * somehow.
+     */
+    bool releaseIfNoDelete() noexcept
+    {
+        auto val = mRef.load(std::memory_order_acquire);
+        while(val > 1 && !mRef.compare_exchange_strong(val, val-1, std::memory_order_acq_rel))
+        {
+            /* val was updated with the current value on failure, so just try
+             * again.
+             */
+        }
+
+        return val >= 2;
+    }
+};
+
+
+template<typename T>
+class intrusive_ptr {
+    T *mPtr{nullptr};
+
+public:
+    intrusive_ptr() noexcept = default;
+    intrusive_ptr(const intrusive_ptr &rhs) noexcept : mPtr{rhs.mPtr}
+    { if(mPtr) mPtr->add_ref(); }
+    intrusive_ptr(intrusive_ptr&& rhs) noexcept : mPtr{rhs.mPtr}
+    { rhs.mPtr = nullptr; }
+    intrusive_ptr(std::nullptr_t) noexcept { }
+    explicit intrusive_ptr(T *ptr) noexcept : mPtr{ptr} { }
+    ~intrusive_ptr() { if(mPtr) mPtr->dec_ref(); }
+
+    intrusive_ptr& operator=(const intrusive_ptr &rhs) noexcept
+    {
+        static_assert(noexcept(std::declval<T*>()->dec_ref()), "dec_ref must be noexcept");
+
+        if(rhs.mPtr) rhs.mPtr->add_ref();
+        if(mPtr) mPtr->dec_ref();
+        mPtr = rhs.mPtr;
+        return *this;
+    }
+    intrusive_ptr& operator=(intrusive_ptr&& rhs) noexcept
+    {
+        if(&rhs != this) LIKELY
+        {
+            if(mPtr) mPtr->dec_ref();
+            mPtr = std::exchange(rhs.mPtr, nullptr);
+        }
+        return *this;
+    }
+
+    explicit operator bool() const noexcept { return mPtr != nullptr; }
+
+    T& operator*() const noexcept { return *mPtr; }
+    T* operator->() const noexcept { return mPtr; }
+    T* get() const noexcept { return mPtr; }
+
+    void reset(T *ptr=nullptr) noexcept
+    {
+        if(mPtr)
+            mPtr->dec_ref();
+        mPtr = ptr;
+    }
+
+    T* release() noexcept { return std::exchange(mPtr, nullptr); }
+
+    void swap(intrusive_ptr &rhs) noexcept { std::swap(mPtr, rhs.mPtr); }
+    void swap(intrusive_ptr&& rhs) noexcept { std::swap(mPtr, rhs.mPtr); }
+};
+
+#define AL_DECL_OP(op)                                                        \
+template<typename T>                                                          \
+inline bool operator op(const intrusive_ptr<T> &lhs, const T *rhs) noexcept   \
+{ return lhs.get() op rhs; }                                                  \
+template<typename T>                                                          \
+inline bool operator op(const T *lhs, const intrusive_ptr<T> &rhs) noexcept   \
+{ return lhs op rhs.get(); }
+
+AL_DECL_OP(==)
+AL_DECL_OP(!=)
+AL_DECL_OP(<=)
+AL_DECL_OP(>=)
+AL_DECL_OP(<)
+AL_DECL_OP(>)
+
+#undef AL_DECL_OP
+
+} // namespace al
+
+#endif /* INTRUSIVE_PTR_H */
diff --git a/common/opthelpers.h b/common/opthelpers.h
new file mode 100644 (file)
index 0000000..596c245
--- /dev/null
@@ -0,0 +1,93 @@
+#ifndef OPTHELPERS_H
+#define OPTHELPERS_H
+
+#include <cstdint>
+#include <utility>
+#include <memory>
+
+#ifdef __has_builtin
+#define HAS_BUILTIN __has_builtin
+#else
+#define HAS_BUILTIN(x) (0)
+#endif
+
+#ifdef __has_cpp_attribute
+#define HAS_ATTRIBUTE __has_cpp_attribute
+#else
+#define HAS_ATTRIBUTE(x) (0)
+#endif
+
+#ifdef __GNUC__
+#define force_inline [[gnu::always_inline]] inline
+#elif defined(_MSC_VER)
+#define force_inline __forceinline
+#else
+#define force_inline inline
+#endif
+
+/* Unlike the likely attribute, ASSUME requires the condition to be true or
+ * else it invokes undefined behavior. It's essentially an assert without
+ * actually checking the condition at run-time, allowing for stronger
+ * optimizations than the likely attribute.
+ */
+#if HAS_BUILTIN(__builtin_assume)
+#define ASSUME __builtin_assume
+#elif defined(_MSC_VER)
+#define ASSUME __assume
+#elif __has_attribute(assume)
+#define ASSUME(x) [[assume(x)]]
+#elif HAS_BUILTIN(__builtin_unreachable)
+#define ASSUME(x) do { if(x) break; __builtin_unreachable(); } while(0)
+#else
+#define ASSUME(x) ((void)0)
+#endif
+
+/* This shouldn't be needed since unknown attributes are ignored, but older
+ * versions of GCC choke on the attribute syntax in certain situations.
+ */
+#if HAS_ATTRIBUTE(likely)
+#define LIKELY [[likely]]
+#define UNLIKELY [[unlikely]]
+#else
+#define LIKELY
+#define UNLIKELY
+#endif
+
+namespace al {
+
+template<typename T>
+constexpr std::underlying_type_t<T> to_underlying(T e) noexcept
+{ return static_cast<std::underlying_type_t<T>>(e); }
+
+[[noreturn]] inline void unreachable()
+{
+#if HAS_BUILTIN(__builtin_unreachable)
+    __builtin_unreachable();
+#else
+    ASSUME(false);
+#endif
+}
+
+template<std::size_t alignment, typename T>
+force_inline constexpr auto assume_aligned(T *ptr) noexcept
+{
+#ifdef __cpp_lib_assume_aligned
+    return std::assume_aligned<alignment,T>(ptr);
+#elif HAS_BUILTIN(__builtin_assume_aligned)
+    return static_cast<T*>(__builtin_assume_aligned(ptr, alignment));
+#elif defined(_MSC_VER)
+    constexpr std::size_t alignment_mask{(1<<alignment) - 1};
+    if((reinterpret_cast<std::uintptr_t>(ptr)&alignment_mask) == 0)
+        return ptr;
+    __assume(0);
+#elif defined(__ICC)
+    __assume_aligned(ptr, alignment);
+    return ptr;
+#else
+    return ptr;
+#endif
+}
+
+} // namespace al
+
+#endif /* OPTHELPERS_H */
diff --git a/common/phase_shifter.h b/common/phase_shifter.h
new file mode 100644 (file)
index 0000000..0d4166b
--- /dev/null
@@ -0,0 +1,214 @@
+#ifndef PHASE_SHIFTER_H
+#define PHASE_SHIFTER_H
+
+#ifdef HAVE_SSE_INTRINSICS
+#include <xmmintrin.h>
+#elif defined(HAVE_NEON)
+#include <arm_neon.h>
+#endif
+
+#include <array>
+#include <stddef.h>
+
+#include "alcomplex.h"
+#include "alspan.h"
+
+
+/* Implements a wide-band +90 degree phase-shift. Note that this should be
+ * given one sample less of a delay (FilterSize/2 - 1) compared to the direct
+ * signal delay (FilterSize/2) to properly align.
+ */
+template<size_t FilterSize>
+struct PhaseShifterT {
+    static_assert(FilterSize >= 16, "FilterSize needs to be at least 16");
+    static_assert((FilterSize&(FilterSize-1)) == 0, "FilterSize needs to be power-of-two");
+
+    alignas(16) std::array<float,FilterSize/2> mCoeffs{};
+
+    /* Some notes on this filter construction.
+     *
+     * A wide-band phase-shift filter needs a delay to maintain linearity. A
+     * dirac impulse in the center of a time-domain buffer represents a filter
+     * passing all frequencies through as-is with a pure delay. Converting that
+     * to the frequency domain, adjusting the phase of each frequency bin by
+     * +90 degrees, then converting back to the time domain, results in a FIR
+     * filter that applies a +90 degree wide-band phase-shift.
+     *
+     * A particularly notable aspect of the time-domain filter response is that
+     * every other coefficient is 0. This allows doubling the effective size of
+     * the filter, by storing only the non-0 coefficients and double-stepping
+     * over the input to apply it.
+     *
+     * Additionally, the resulting filter is independent of the sample rate.
+     * The same filter can be applied regardless of the device's sample rate
+     * and achieve the same effect.
+     */
+    PhaseShifterT()
+    {
+        using complex_d = std::complex<double>;
+        constexpr size_t fft_size{FilterSize};
+        constexpr size_t half_size{fft_size / 2};
+
+        auto fftBuffer = std::make_unique<complex_d[]>(fft_size);
+        std::fill_n(fftBuffer.get(), fft_size, complex_d{});
+        fftBuffer[half_size] = 1.0;
+
+        forward_fft(al::as_span(fftBuffer.get(), fft_size));
+        for(size_t i{0};i < half_size+1;++i)
+            fftBuffer[i] = complex_d{-fftBuffer[i].imag(), fftBuffer[i].real()};
+        for(size_t i{half_size+1};i < fft_size;++i)
+            fftBuffer[i] = std::conj(fftBuffer[fft_size - i]);
+        inverse_fft(al::as_span(fftBuffer.get(), fft_size));
+
+        auto fftiter = fftBuffer.get() + half_size + (FilterSize/2 - 1);
+        for(float &coeff : mCoeffs)
+        {
+            coeff = static_cast<float>(fftiter->real() / double{fft_size});
+            fftiter -= 2;
+        }
+    }
+
+    void process(al::span<float> dst, const float *RESTRICT src) const;
+
+private:
+#if defined(HAVE_NEON)
+    /* There doesn't seem to be NEON intrinsics to do this kind of stipple
+     * shuffling, so there's two custom methods for it.
+     */
+    static auto shuffle_2020(float32x4_t a, float32x4_t b)
+    {
+        float32x4_t ret{vmovq_n_f32(vgetq_lane_f32(a, 0))};
+        ret = vsetq_lane_f32(vgetq_lane_f32(a, 2), ret, 1);
+        ret = vsetq_lane_f32(vgetq_lane_f32(b, 0), ret, 2);
+        ret = vsetq_lane_f32(vgetq_lane_f32(b, 2), ret, 3);
+        return ret;
+    }
+    static auto shuffle_3131(float32x4_t a, float32x4_t b)
+    {
+        float32x4_t ret{vmovq_n_f32(vgetq_lane_f32(a, 1))};
+        ret = vsetq_lane_f32(vgetq_lane_f32(a, 3), ret, 1);
+        ret = vsetq_lane_f32(vgetq_lane_f32(b, 1), ret, 2);
+        ret = vsetq_lane_f32(vgetq_lane_f32(b, 3), ret, 3);
+        return ret;
+    }
+    static auto unpacklo(float32x4_t a, float32x4_t b)
+    {
+        float32x2x2_t result{vzip_f32(vget_low_f32(a), vget_low_f32(b))};
+        return vcombine_f32(result.val[0], result.val[1]);
+    }
+    static auto unpackhi(float32x4_t a, float32x4_t b)
+    {
+        float32x2x2_t result{vzip_f32(vget_high_f32(a), vget_high_f32(b))};
+        return vcombine_f32(result.val[0], result.val[1]);
+    }
+    static auto load4(float32_t a, float32_t b, float32_t c, float32_t d)
+    {
+        float32x4_t ret{vmovq_n_f32(a)};
+        ret = vsetq_lane_f32(b, ret, 1);
+        ret = vsetq_lane_f32(c, ret, 2);
+        ret = vsetq_lane_f32(d, ret, 3);
+        return ret;
+    }
+#endif
+};
+
+template<size_t S>
+inline void PhaseShifterT<S>::process(al::span<float> dst, const float *RESTRICT src) const
+{
+#ifdef HAVE_SSE_INTRINSICS
+    if(size_t todo{dst.size()>>1})
+    {
+        auto *out = reinterpret_cast<__m64*>(dst.data());
+        do {
+            __m128 r04{_mm_setzero_ps()};
+            __m128 r14{_mm_setzero_ps()};
+            for(size_t j{0};j < mCoeffs.size();j+=4)
+            {
+                const __m128 coeffs{_mm_load_ps(&mCoeffs[j])};
+                const __m128 s0{_mm_loadu_ps(&src[j*2])};
+                const __m128 s1{_mm_loadu_ps(&src[j*2 + 4])};
+
+                __m128 s{_mm_shuffle_ps(s0, s1, _MM_SHUFFLE(2, 0, 2, 0))};
+                r04 = _mm_add_ps(r04, _mm_mul_ps(s, coeffs));
+
+                s = _mm_shuffle_ps(s0, s1, _MM_SHUFFLE(3, 1, 3, 1));
+                r14 = _mm_add_ps(r14, _mm_mul_ps(s, coeffs));
+            }
+            src += 2;
+
+            __m128 r4{_mm_add_ps(_mm_unpackhi_ps(r04, r14), _mm_unpacklo_ps(r04, r14))};
+            r4 = _mm_add_ps(r4, _mm_movehl_ps(r4, r4));
+
+            _mm_storel_pi(out, r4);
+            ++out;
+        } while(--todo);
+    }
+    if((dst.size()&1))
+    {
+        __m128 r4{_mm_setzero_ps()};
+        for(size_t j{0};j < mCoeffs.size();j+=4)
+        {
+            const __m128 coeffs{_mm_load_ps(&mCoeffs[j])};
+            const __m128 s{_mm_setr_ps(src[j*2], src[j*2 + 2], src[j*2 + 4], src[j*2 + 6])};
+            r4 = _mm_add_ps(r4, _mm_mul_ps(s, coeffs));
+        }
+        r4 = _mm_add_ps(r4, _mm_shuffle_ps(r4, r4, _MM_SHUFFLE(0, 1, 2, 3)));
+        r4 = _mm_add_ps(r4, _mm_movehl_ps(r4, r4));
+
+        dst.back() = _mm_cvtss_f32(r4);
+    }
+
+#elif defined(HAVE_NEON)
+
+    size_t pos{0};
+    if(size_t todo{dst.size()>>1})
+    {
+        do {
+            float32x4_t r04{vdupq_n_f32(0.0f)};
+            float32x4_t r14{vdupq_n_f32(0.0f)};
+            for(size_t j{0};j < mCoeffs.size();j+=4)
+            {
+                const float32x4_t coeffs{vld1q_f32(&mCoeffs[j])};
+                const float32x4_t s0{vld1q_f32(&src[j*2])};
+                const float32x4_t s1{vld1q_f32(&src[j*2 + 4])};
+
+                r04 = vmlaq_f32(r04, shuffle_2020(s0, s1), coeffs);
+                r14 = vmlaq_f32(r14, shuffle_3131(s0, s1), coeffs);
+            }
+            src += 2;
+
+            float32x4_t r4{vaddq_f32(unpackhi(r04, r14), unpacklo(r04, r14))};
+            float32x2_t r2{vadd_f32(vget_low_f32(r4), vget_high_f32(r4))};
+
+            vst1_f32(&dst[pos], r2);
+            pos += 2;
+        } while(--todo);
+    }
+    if((dst.size()&1))
+    {
+        float32x4_t r4{vdupq_n_f32(0.0f)};
+        for(size_t j{0};j < mCoeffs.size();j+=4)
+        {
+            const float32x4_t coeffs{vld1q_f32(&mCoeffs[j])};
+            const float32x4_t s{load4(src[j*2], src[j*2 + 2], src[j*2 + 4], src[j*2 + 6])};
+            r4 = vmlaq_f32(r4, s, coeffs);
+        }
+        r4 = vaddq_f32(r4, vrev64q_f32(r4));
+        dst[pos] = vget_lane_f32(vadd_f32(vget_low_f32(r4), vget_high_f32(r4)), 0);
+    }
+
+#else
+
+    for(float &output : dst)
+    {
+        float ret{0.0f};
+        for(size_t j{0};j < mCoeffs.size();++j)
+            ret += src[j*2] * mCoeffs[j];
+
+        output = ret;
+        ++src;
+    }
+#endif
+}
+
+#endif /* PHASE_SHIFTER_H */
diff --git a/common/polyphase_resampler.cpp b/common/polyphase_resampler.cpp
new file mode 100644 (file)
index 0000000..14f7e40
--- /dev/null
@@ -0,0 +1,222 @@
+
+#include "polyphase_resampler.h"
+
+#include <algorithm>
+#include <cmath>
+
+#include "alnumbers.h"
+#include "opthelpers.h"
+
+
+namespace {
+
+constexpr double Epsilon{1e-9};
+
+using uint = unsigned int;
+
+/* This is the normalized cardinal sine (sinc) function.
+ *
+ *   sinc(x) = { 1,                   x = 0
+ *             { sin(pi x) / (pi x),  otherwise.
+ */
+double Sinc(const double x)
+{
+    if(std::abs(x) < Epsilon) UNLIKELY
+        return 1.0;
+    return std::sin(al::numbers::pi*x) / (al::numbers::pi*x);
+}
+
+/* The zero-order modified Bessel function of the first kind, used for the
+ * Kaiser window.
+ *
+ *   I_0(x) = sum_{k=0}^inf (1 / k!)^2 (x / 2)^(2 k)
+ *          = sum_{k=0}^inf ((x / 2)^k / k!)^2
+ */
+constexpr double BesselI_0(const double x)
+{
+    // Start at k=1 since k=0 is trivial.
+    const double x2{x/2.0};
+    double term{1.0};
+    double sum{1.0};
+    int k{1};
+
+    // Let the integration converge until the term of the sum is no longer
+    // significant.
+    double last_sum{};
+    do {
+        const double y{x2 / k};
+        ++k;
+        last_sum = sum;
+        term *= y * y;
+        sum += term;
+    } while(sum != last_sum);
+    return sum;
+}
+
+/* Calculate a Kaiser window from the given beta value and a normalized k
+ * [-1, 1].
+ *
+ *   w(k) = { I_0(B sqrt(1 - k^2)) / I_0(B),  -1 <= k <= 1
+ *          { 0,                              elsewhere.
+ *
+ * Where k can be calculated as:
+ *
+ *   k = i / l,         where -l <= i <= l.
+ *
+ * or:
+ *
+ *   k = 2 i / M - 1,   where 0 <= i <= M.
+ */
+double Kaiser(const double b, const double k)
+{
+    if(!(k >= -1.0 && k <= 1.0))
+        return 0.0;
+    return BesselI_0(b * std::sqrt(1.0 - k*k)) / BesselI_0(b);
+}
+
+// Calculates the greatest common divisor of a and b.
+constexpr uint Gcd(uint x, uint y)
+{
+    while(y > 0)
+    {
+        const uint z{y};
+        y = x % y;
+        x = z;
+    }
+    return x;
+}
+
+/* Calculates the size (order) of the Kaiser window.  Rejection is in dB and
+ * the transition width is normalized frequency (0.5 is nyquist).
+ *
+ *   M = { ceil((r - 7.95) / (2.285 2 pi f_t)),  r > 21
+ *       { ceil(5.79 / 2 pi f_t),                r <= 21.
+ *
+ */
+constexpr uint CalcKaiserOrder(const double rejection, const double transition)
+{
+    const double w_t{2.0 * al::numbers::pi * transition};
+    if(rejection > 21.0) LIKELY
+        return static_cast<uint>(std::ceil((rejection - 7.95) / (2.285 * w_t)));
+    return static_cast<uint>(std::ceil(5.79 / w_t));
+}
+
+// Calculates the beta value of the Kaiser window.  Rejection is in dB.
+constexpr double CalcKaiserBeta(const double rejection)
+{
+    if(rejection > 50.0) LIKELY
+        return 0.1102 * (rejection - 8.7);
+    if(rejection >= 21.0)
+        return (0.5842 * std::pow(rejection - 21.0, 0.4)) +
+               (0.07886 * (rejection - 21.0));
+    return 0.0;
+}
+
+/* Calculates a point on the Kaiser-windowed sinc filter for the given half-
+ * width, beta, gain, and cutoff.  The point is specified in non-normalized
+ * samples, from 0 to M, where M = (2 l + 1).
+ *
+ *   w(k) 2 p f_t sinc(2 f_t x)
+ *
+ *   x    -- centered sample index (i - l)
+ *   k    -- normalized and centered window index (x / l)
+ *   w(k) -- window function (Kaiser)
+ *   p    -- gain compensation factor when sampling
+ *   f_t  -- normalized center frequency (or cutoff; 0.5 is nyquist)
+ */
+double SincFilter(const uint l, const double b, const double gain, const double cutoff,
+    const uint i)
+{
+    const double x{static_cast<double>(i) - l};
+    return Kaiser(b, x / l) * 2.0 * gain * cutoff * Sinc(2.0 * cutoff * x);
+}
+
+} // namespace
+
+// Calculate the resampling metrics and build the Kaiser-windowed sinc filter
+// that's used to cut frequencies above the destination nyquist.
+void PPhaseResampler::init(const uint srcRate, const uint dstRate)
+{
+    const uint gcd{Gcd(srcRate, dstRate)};
+    mP = dstRate / gcd;
+    mQ = srcRate / gcd;
+
+    /* The cutoff is adjusted by half the transition width, so the transition
+     * ends before the nyquist (0.5).  Both are scaled by the downsampling
+     * factor.
+     */
+    double cutoff, width;
+    if(mP > mQ)
+    {
+        cutoff = 0.475 / mP;
+        width = 0.05 / mP;
+    }
+    else
+    {
+        cutoff = 0.475 / mQ;
+        width = 0.05 / mQ;
+    }
+    // A rejection of -180 dB is used for the stop band. Round up when
+    // calculating the left offset to avoid increasing the transition width.
+    const uint l{(CalcKaiserOrder(180.0, width)+1) / 2};
+    const double beta{CalcKaiserBeta(180.0)};
+    mM = l*2 + 1;
+    mL = l;
+    mF.resize(mM);
+    for(uint i{0};i < mM;i++)
+        mF[i] = SincFilter(l, beta, mP, cutoff, i);
+}
+
+// Perform the upsample-filter-downsample resampling operation using a
+// polyphase filter implementation.
+void PPhaseResampler::process(const uint inN, const double *in, const uint outN, double *out)
+{
+    if(outN == 0) UNLIKELY
+        return;
+
+    // Handle in-place operation.
+    std::vector<double> workspace;
+    double *work{out};
+    if(work == in) UNLIKELY
+    {
+        workspace.resize(outN);
+        work = workspace.data();
+    }
+
+    // Resample the input.
+    const uint p{mP}, q{mQ}, m{mM}, l{mL};
+    const double *f{mF.data()};
+    for(uint i{0};i < outN;i++)
+    {
+        // Input starts at l to compensate for the filter delay.  This will
+        // drop any build-up from the first half of the filter.
+        size_t j_f{(l + q*i) % p};
+        size_t j_s{(l + q*i) / p};
+
+        // Only take input when 0 <= j_s < inN.
+        double r{0.0};
+        if(j_f < m) LIKELY
+        {
+            size_t filt_len{(m-j_f+p-1) / p};
+            if(j_s+1 > inN) LIKELY
+            {
+                size_t skip{std::min<size_t>(j_s+1 - inN, filt_len)};
+                j_f += p*skip;
+                j_s -= skip;
+                filt_len -= skip;
+            }
+            if(size_t todo{std::min<size_t>(j_s+1, filt_len)}) LIKELY
+            {
+                do {
+                    r += f[j_f] * in[j_s];
+                    j_f += p;
+                    --j_s;
+                } while(--todo);
+            }
+        }
+        work[i] = r;
+    }
+    // Clean up after in-place operation.
+    if(work != out)
+        std::copy_n(work, outN, out);
+}
diff --git a/common/polyphase_resampler.h b/common/polyphase_resampler.h
new file mode 100644 (file)
index 0000000..557485b
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef POLYPHASE_RESAMPLER_H
+#define POLYPHASE_RESAMPLER_H
+
+#include <vector>
+
+
+using uint = unsigned int;
+
+/* This is a polyphase sinc-filtered resampler. It is built for very high
+ * quality results, rather than real-time performance.
+ *
+ *              Upsample                      Downsample
+ *
+ *              p/q = 3/2                     p/q = 3/5
+ *
+ *          M-+-+-+->                     M-+-+-+->
+ *         -------------------+          ---------------------+
+ *   p  s * f f f f|f|        |    p  s * f f f f f           |
+ *   |  0 *   0 0 0|0|0       |    |  0 *   0 0 0 0|0|        |
+ *   v  0 *     0 0|0|0 0     |    v  0 *     0 0 0|0|0       |
+ *      s *       f|f|f f f   |       s *       f f|f|f f     |
+ *      0 *        |0|0 0 0 0 |       0 *         0|0|0 0 0   |
+ *         --------+=+--------+       0 *          |0|0 0 0 0 |
+ *          d . d .|d|. d . d            ----------+=+--------+
+ *                                        d . . . .|d|. . . .
+ *          q->
+ *                                        q-+-+-+->
+ *
+ *   P_f(i,j) = q i mod p + pj
+ *   P_s(i,j) = floor(q i / p) - j
+ *   d[i=0..N-1] = sum_{j=0}^{floor((M - 1) / p)} {
+ *                   { f[P_f(i,j)] s[P_s(i,j)],  P_f(i,j) < M
+ *                   { 0,                        P_f(i,j) >= M. }
+ */
+
+struct PPhaseResampler {
+    void init(const uint srcRate, const uint dstRate);
+    void process(const uint inN, const double *in, const uint outN, double *out);
+
+    explicit operator bool() const noexcept { return !mF.empty(); }
+
+private:
+    uint mP, mQ, mM, mL;
+    std::vector<double> mF;
+};
+
+#endif /* POLYPHASE_RESAMPLER_H */
diff --git a/common/pragmadefs.h b/common/pragmadefs.h
new file mode 100644 (file)
index 0000000..9f0a711
--- /dev/null
@@ -0,0 +1,21 @@
+#ifndef PRAGMADEFS_H
+#define PRAGMADEFS_H
+
+#if defined(_MSC_VER)
+#define DIAGNOSTIC_PUSH __pragma(warning(push))
+#define DIAGNOSTIC_POP __pragma(warning(pop))
+#define std_pragma(...)
+#define msc_pragma __pragma
+#else
+#if defined(__GNUC__) || defined(__clang__)
+#define DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push")
+#define DIAGNOSTIC_POP _Pragma("GCC diagnostic pop")
+#else
+#define DIAGNOSTIC_PUSH
+#define DIAGNOSTIC_POP
+#endif
+#define std_pragma _Pragma
+#define msc_pragma(...)
+#endif
+
+#endif /* PRAGMADEFS_H */
diff --git a/common/ringbuffer.cpp b/common/ringbuffer.cpp
new file mode 100644 (file)
index 0000000..0aec1d4
--- /dev/null
@@ -0,0 +1,224 @@
+/**
+ * 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 "ringbuffer.h"
+
+#include <algorithm>
+#include <climits>
+#include <stdexcept>
+
+#include "almalloc.h"
+
+
+RingBufferPtr RingBuffer::Create(size_t sz, size_t elem_sz, int limit_writes)
+{
+    size_t power_of_two{0u};
+    if(sz > 0)
+    {
+        power_of_two = sz;
+        power_of_two |= power_of_two>>1;
+        power_of_two |= power_of_two>>2;
+        power_of_two |= power_of_two>>4;
+        power_of_two |= power_of_two>>8;
+        power_of_two |= power_of_two>>16;
+#if SIZE_MAX > UINT_MAX
+        power_of_two |= power_of_two>>32;
+#endif
+    }
+    ++power_of_two;
+    if(power_of_two <= sz || power_of_two > std::numeric_limits<size_t>::max()/elem_sz)
+        throw std::overflow_error{"Ring buffer size overflow"};
+
+    const size_t bufbytes{power_of_two * elem_sz};
+    RingBufferPtr rb{new(FamCount(bufbytes)) RingBuffer{bufbytes}};
+    rb->mWriteSize = limit_writes ? sz : (power_of_two-1);
+    rb->mSizeMask = power_of_two - 1;
+    rb->mElemSize = elem_sz;
+
+    return rb;
+}
+
+void RingBuffer::reset() noexcept
+{
+    mWritePtr.store(0, std::memory_order_relaxed);
+    mReadPtr.store(0, std::memory_order_relaxed);
+    std::fill_n(mBuffer.begin(), (mSizeMask+1)*mElemSize, al::byte{});
+}
+
+
+size_t RingBuffer::read(void *dest, size_t cnt) noexcept
+{
+    const size_t free_cnt{readSpace()};
+    if(free_cnt == 0) return 0;
+
+    const size_t to_read{std::min(cnt, free_cnt)};
+    size_t read_ptr{mReadPtr.load(std::memory_order_relaxed) & mSizeMask};
+
+    size_t n1, n2;
+    const size_t cnt2{read_ptr + to_read};
+    if(cnt2 > mSizeMask+1)
+    {
+        n1 = mSizeMask+1 - read_ptr;
+        n2 = cnt2 & mSizeMask;
+    }
+    else
+    {
+        n1 = to_read;
+        n2 = 0;
+    }
+
+    auto outiter = std::copy_n(mBuffer.begin() + read_ptr*mElemSize, n1*mElemSize,
+        static_cast<al::byte*>(dest));
+    read_ptr += n1;
+    if(n2 > 0)
+    {
+        std::copy_n(mBuffer.begin(), n2*mElemSize, outiter);
+        read_ptr += n2;
+    }
+    mReadPtr.store(read_ptr, std::memory_order_release);
+    return to_read;
+}
+
+size_t RingBuffer::peek(void *dest, size_t cnt) const noexcept
+{
+    const size_t free_cnt{readSpace()};
+    if(free_cnt == 0) return 0;
+
+    const size_t to_read{std::min(cnt, free_cnt)};
+    size_t read_ptr{mReadPtr.load(std::memory_order_relaxed) & mSizeMask};
+
+    size_t n1, n2;
+    const size_t cnt2{read_ptr + to_read};
+    if(cnt2 > mSizeMask+1)
+    {
+        n1 = mSizeMask+1 - read_ptr;
+        n2 = cnt2 & mSizeMask;
+    }
+    else
+    {
+        n1 = to_read;
+        n2 = 0;
+    }
+
+    auto outiter = std::copy_n(mBuffer.begin() + read_ptr*mElemSize, n1*mElemSize,
+        static_cast<al::byte*>(dest));
+    if(n2 > 0)
+        std::copy_n(mBuffer.begin(), n2*mElemSize, outiter);
+    return to_read;
+}
+
+size_t RingBuffer::write(const void *src, size_t cnt) noexcept
+{
+    const size_t free_cnt{writeSpace()};
+    if(free_cnt == 0) return 0;
+
+    const size_t to_write{std::min(cnt, free_cnt)};
+    size_t write_ptr{mWritePtr.load(std::memory_order_relaxed) & mSizeMask};
+
+    size_t n1, n2;
+    const size_t cnt2{write_ptr + to_write};
+    if(cnt2 > mSizeMask+1)
+    {
+        n1 = mSizeMask+1 - write_ptr;
+        n2 = cnt2 & mSizeMask;
+    }
+    else
+    {
+        n1 = to_write;
+        n2 = 0;
+    }
+
+    auto srcbytes = static_cast<const al::byte*>(src);
+    std::copy_n(srcbytes, n1*mElemSize, mBuffer.begin() + write_ptr*mElemSize);
+    write_ptr += n1;
+    if(n2 > 0)
+    {
+        std::copy_n(srcbytes + n1*mElemSize, n2*mElemSize, mBuffer.begin());
+        write_ptr += n2;
+    }
+    mWritePtr.store(write_ptr, std::memory_order_release);
+    return to_write;
+}
+
+
+auto RingBuffer::getReadVector() const noexcept -> DataPair
+{
+    DataPair ret;
+
+    size_t w{mWritePtr.load(std::memory_order_acquire)};
+    size_t r{mReadPtr.load(std::memory_order_acquire)};
+    w &= mSizeMask;
+    r &= mSizeMask;
+    const size_t free_cnt{(w-r) & mSizeMask};
+
+    const size_t cnt2{r + free_cnt};
+    if(cnt2 > mSizeMask+1)
+    {
+        /* Two part vector: the rest of the buffer after the current read ptr,
+         * plus some from the start of the buffer. */
+        ret.first.buf = const_cast<al::byte*>(mBuffer.data() + r*mElemSize);
+        ret.first.len = mSizeMask+1 - r;
+        ret.second.buf = const_cast<al::byte*>(mBuffer.data());
+        ret.second.len = cnt2 & mSizeMask;
+    }
+    else
+    {
+        /* Single part vector: just the rest of the buffer */
+        ret.first.buf = const_cast<al::byte*>(mBuffer.data() + r*mElemSize);
+        ret.first.len = free_cnt;
+        ret.second.buf = nullptr;
+        ret.second.len = 0;
+    }
+
+    return ret;
+}
+
+auto RingBuffer::getWriteVector() const noexcept -> DataPair
+{
+    DataPair ret;
+
+    size_t w{mWritePtr.load(std::memory_order_acquire)};
+    size_t r{mReadPtr.load(std::memory_order_acquire) + mWriteSize - mSizeMask};
+    w &= mSizeMask;
+    r &= mSizeMask;
+    const size_t free_cnt{(r-w-1) & mSizeMask};
+
+    const size_t cnt2{w + free_cnt};
+    if(cnt2 > mSizeMask+1)
+    {
+        /* Two part vector: the rest of the buffer after the current write ptr,
+         * plus some from the start of the buffer. */
+        ret.first.buf = const_cast<al::byte*>(mBuffer.data() + w*mElemSize);
+        ret.first.len = mSizeMask+1 - w;
+        ret.second.buf = const_cast<al::byte*>(mBuffer.data());
+        ret.second.len = cnt2 & mSizeMask;
+    }
+    else
+    {
+        ret.first.buf = const_cast<al::byte*>(mBuffer.data() + w*mElemSize);
+        ret.first.len = free_cnt;
+        ret.second.buf = nullptr;
+        ret.second.len = 0;
+    }
+
+    return ret;
+}
diff --git a/common/ringbuffer.h b/common/ringbuffer.h
new file mode 100644 (file)
index 0000000..2a3797b
--- /dev/null
@@ -0,0 +1,115 @@
+#ifndef RINGBUFFER_H
+#define RINGBUFFER_H
+
+#include <atomic>
+#include <memory>
+#include <stddef.h>
+#include <utility>
+
+#include "albyte.h"
+#include "almalloc.h"
+
+
+/* NOTE: This lockless ringbuffer implementation is copied from JACK, extended
+ * to include an element size. Consequently, parameters and return values for a
+ * size or count is in 'elements', not bytes. Additionally, it only supports
+ * single-consumer/single-provider operation.
+ */
+
+struct RingBuffer {
+private:
+    std::atomic<size_t> mWritePtr{0u};
+    std::atomic<size_t> mReadPtr{0u};
+    size_t mWriteSize{0u};
+    size_t mSizeMask{0u};
+    size_t mElemSize{0u};
+
+    al::FlexArray<al::byte, 16> mBuffer;
+
+public:
+    struct Data {
+        al::byte *buf;
+        size_t len;
+    };
+    using DataPair = std::pair<Data,Data>;
+
+
+    RingBuffer(const size_t count) : mBuffer{count} { }
+
+    /** Reset the read and write pointers to zero. This is not thread safe. */
+    void reset() noexcept;
+
+    /**
+     * The non-copying data reader. Returns two ringbuffer data pointers that
+     * hold the current readable data. If the readable data is in one segment
+     * the second segment has zero length.
+     */
+    DataPair getReadVector() const noexcept;
+    /**
+     * The non-copying data writer. Returns two ringbuffer data pointers that
+     * hold the current writeable data. If the writeable data is in one segment
+     * the second segment has zero length.
+     */
+    DataPair getWriteVector() const noexcept;
+
+    /**
+     * Return the number of elements available for reading. This is the number
+     * of elements in front of the read pointer and behind the write pointer.
+     */
+    size_t readSpace() const noexcept
+    {
+        const size_t w{mWritePtr.load(std::memory_order_acquire)};
+        const size_t r{mReadPtr.load(std::memory_order_acquire)};
+        return (w-r) & mSizeMask;
+    }
+
+    /**
+     * The copying data reader. Copy at most `cnt' elements into `dest'.
+     * Returns the actual number of elements copied.
+     */
+    size_t read(void *dest, size_t cnt) noexcept;
+    /**
+     * The copying data reader w/o read pointer advance. Copy at most `cnt'
+     * elements into `dest'. Returns the actual number of elements copied.
+     */
+    size_t peek(void *dest, size_t cnt) const noexcept;
+    /** Advance the read pointer `cnt' places. */
+    void readAdvance(size_t cnt) noexcept
+    { mReadPtr.fetch_add(cnt, std::memory_order_acq_rel); }
+
+
+    /**
+     * Return the number of elements available for writing. This is the number
+     * of elements in front of the write pointer and behind the read pointer.
+     */
+    size_t writeSpace() const noexcept
+    {
+        const size_t w{mWritePtr.load(std::memory_order_acquire)};
+        const size_t r{mReadPtr.load(std::memory_order_acquire) + mWriteSize - mSizeMask};
+        return (r-w-1) & mSizeMask;
+    }
+
+    /**
+     * The copying data writer. Copy at most `cnt' elements from `src'. Returns
+     * the actual number of elements copied.
+     */
+    size_t write(const void *src, size_t cnt) noexcept;
+    /** Advance the write pointer `cnt' places. */
+    void writeAdvance(size_t cnt) noexcept
+    { mWritePtr.fetch_add(cnt, std::memory_order_acq_rel); }
+
+    size_t getElemSize() const noexcept { return mElemSize; }
+
+    /**
+     * Create a new ringbuffer to hold at least `sz' elements of `elem_sz'
+     * bytes. The number of elements is rounded up to the next power of two
+     * (even if it is already a power of two, to ensure the requested amount
+     * can be written).
+     */
+    static std::unique_ptr<RingBuffer> Create(size_t sz, size_t elem_sz, int limit_writes);
+
+    DEF_FAM_NEWDEL(RingBuffer, mBuffer)
+};
+using RingBufferPtr = std::unique_ptr<RingBuffer>;
+
+#endif /* RINGBUFFER_H */
diff --git a/common/strutils.cpp b/common/strutils.cpp
new file mode 100644 (file)
index 0000000..d0418ef
--- /dev/null
@@ -0,0 +1,64 @@
+
+#include "config.h"
+
+#include "strutils.h"
+
+#include <cstdlib>
+
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+std::string wstr_to_utf8(const WCHAR *wstr)
+{
+    std::string ret;
+
+    int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);
+    if(len > 0)
+    {
+        ret.resize(len);
+        WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &ret[0], len, nullptr, nullptr);
+        ret.pop_back();
+    }
+
+    return ret;
+}
+
+std::wstring utf8_to_wstr(const char *str)
+{
+    std::wstring ret;
+
+    int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0);
+    if(len > 0)
+    {
+        ret.resize(len);
+        MultiByteToWideChar(CP_UTF8, 0, str, -1, &ret[0], len);
+        ret.pop_back();
+    }
+
+    return ret;
+}
+#endif
+
+namespace al {
+
+al::optional<std::string> getenv(const char *envname)
+{
+    const char *str{std::getenv(envname)};
+    if(str && str[0] != '\0')
+        return str;
+    return al::nullopt;
+}
+
+#ifdef _WIN32
+al::optional<std::wstring> getenv(const WCHAR *envname)
+{
+    const WCHAR *str{_wgetenv(envname)};
+    if(str && str[0] != L'\0')
+        return str;
+    return al::nullopt;
+}
+#endif
+
+} // namespace al
diff --git a/common/strutils.h b/common/strutils.h
new file mode 100644 (file)
index 0000000..0c7a0e2
--- /dev/null
@@ -0,0 +1,24 @@
+#ifndef AL_STRUTILS_H
+#define AL_STRUTILS_H
+
+#include <string>
+
+#include "aloptional.h"
+
+#ifdef _WIN32
+#include <wchar.h>
+
+std::string wstr_to_utf8(const wchar_t *wstr);
+std::wstring utf8_to_wstr(const char *str);
+#endif
+
+namespace al {
+
+al::optional<std::string> getenv(const char *envname);
+#ifdef _WIN32
+al::optional<std::wstring> getenv(const wchar_t *envname);
+#endif
+
+} // namespace al
+
+#endif /* AL_STRUTILS_H */
diff --git a/common/threads.cpp b/common/threads.cpp
new file mode 100644 (file)
index 0000000..19a6bbf
--- /dev/null
@@ -0,0 +1,197 @@
+/**
+ * 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 "opthelpers.h"
+#include "threads.h"
+
+#include <system_error>
+
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+#include <limits>
+
+void althrd_setname(const char *name)
+{
+#if defined(_MSC_VER) && !defined(_M_ARM)
+
+#define MS_VC_EXCEPTION 0x406D1388
+#pragma pack(push,8)
+    struct {
+        DWORD dwType;     // Must be 0x1000.
+        LPCSTR szName;    // Pointer to name (in user addr space).
+        DWORD dwThreadID; // Thread ID (-1=caller thread).
+        DWORD dwFlags;    // Reserved for future use, must be zero.
+    } info;
+#pragma pack(pop)
+    info.dwType = 0x1000;
+    info.szName = name;
+    info.dwThreadID = ~DWORD{0};
+    info.dwFlags = 0;
+
+    __try {
+        RaiseException(MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info);
+    }
+    __except(EXCEPTION_CONTINUE_EXECUTION) {
+    }
+#undef MS_VC_EXCEPTION
+
+#else
+
+    (void)name;
+#endif
+}
+
+namespace al {
+
+semaphore::semaphore(unsigned int initial)
+{
+    if(initial > static_cast<unsigned int>(std::numeric_limits<int>::max()))
+        throw std::system_error(std::make_error_code(std::errc::value_too_large));
+    mSem = CreateSemaphore(nullptr, initial, std::numeric_limits<int>::max(), nullptr);
+    if(mSem == nullptr)
+        throw std::system_error(std::make_error_code(std::errc::resource_unavailable_try_again));
+}
+
+semaphore::~semaphore()
+{ CloseHandle(mSem); }
+
+void semaphore::post()
+{
+    if(!ReleaseSemaphore(static_cast<HANDLE>(mSem), 1, nullptr))
+        throw std::system_error(std::make_error_code(std::errc::value_too_large));
+}
+
+void semaphore::wait() noexcept
+{ WaitForSingleObject(static_cast<HANDLE>(mSem), INFINITE); }
+
+bool semaphore::try_wait() noexcept
+{ return WaitForSingleObject(static_cast<HANDLE>(mSem), 0) == WAIT_OBJECT_0; }
+
+} // namespace al
+
+#else
+
+#include <pthread.h>
+#ifdef HAVE_PTHREAD_NP_H
+#include <pthread_np.h>
+#endif
+#include <tuple>
+
+namespace {
+
+using setname_t1 = int(*)(const char*);
+using setname_t2 = int(*)(pthread_t, const char*);
+using setname_t3 = void(*)(pthread_t, const char*);
+using setname_t4 = int(*)(pthread_t, const char*, void*);
+
+void setname_caller(setname_t1 func, const char *name)
+{ func(name); }
+
+void setname_caller(setname_t2 func, const char *name)
+{ func(pthread_self(), name); }
+
+void setname_caller(setname_t3 func, const char *name)
+{ func(pthread_self(), name); }
+
+void setname_caller(setname_t4 func, const char *name)
+{ func(pthread_self(), "%s", static_cast<void*>(const_cast<char*>(name))); }
+
+} // namespace
+
+void althrd_setname(const char *name)
+{
+#if defined(HAVE_PTHREAD_SET_NAME_NP)
+    setname_caller(pthread_set_name_np, name);
+#elif defined(HAVE_PTHREAD_SETNAME_NP)
+    setname_caller(pthread_setname_np, name);
+#endif
+    /* Avoid unused function/parameter warnings. */
+    std::ignore = name;
+    std::ignore = static_cast<void(*)(setname_t1,const char*)>(&setname_caller);
+    std::ignore = static_cast<void(*)(setname_t2,const char*)>(&setname_caller);
+    std::ignore = static_cast<void(*)(setname_t3,const char*)>(&setname_caller);
+    std::ignore = static_cast<void(*)(setname_t4,const char*)>(&setname_caller);
+}
+
+#ifdef __APPLE__
+
+namespace al {
+
+semaphore::semaphore(unsigned int initial)
+{
+    mSem = dispatch_semaphore_create(initial);
+    if(!mSem)
+        throw std::system_error(std::make_error_code(std::errc::resource_unavailable_try_again));
+}
+
+semaphore::~semaphore()
+{ dispatch_release(mSem); }
+
+void semaphore::post()
+{ dispatch_semaphore_signal(mSem); }
+
+void semaphore::wait() noexcept
+{ dispatch_semaphore_wait(mSem, DISPATCH_TIME_FOREVER); }
+
+bool semaphore::try_wait() noexcept
+{ return dispatch_semaphore_wait(mSem, DISPATCH_TIME_NOW) == 0; }
+
+} // namespace al
+
+#else /* !__APPLE__ */
+
+#include <cerrno>
+
+namespace al {
+
+semaphore::semaphore(unsigned int initial)
+{
+    if(sem_init(&mSem, 0, initial) != 0)
+        throw std::system_error(std::make_error_code(std::errc::resource_unavailable_try_again));
+}
+
+semaphore::~semaphore()
+{ sem_destroy(&mSem); }
+
+void semaphore::post()
+{
+    if(sem_post(&mSem) != 0)
+        throw std::system_error(std::make_error_code(std::errc::value_too_large));
+}
+
+void semaphore::wait() noexcept
+{
+    while(sem_wait(&mSem) == -1 && errno == EINTR) {
+    }
+}
+
+bool semaphore::try_wait() noexcept
+{ return sem_trywait(&mSem) == 0; }
+
+} // namespace al
+
+#endif /* __APPLE__ */
+
+#endif /* _WIN32 */
diff --git a/common/threads.h b/common/threads.h
new file mode 100644 (file)
index 0000000..1cdb5d8
--- /dev/null
@@ -0,0 +1,48 @@
+#ifndef AL_THREADS_H
+#define AL_THREADS_H
+
+#if defined(__GNUC__) && defined(__i386__)
+/* force_align_arg_pointer is required for proper function arguments aligning
+ * when SSE code is used. Some systems (Windows, QNX) do not guarantee our
+ * thread functions will be properly aligned on the stack, even though GCC may
+ * generate code with the assumption that it is. */
+#define FORCE_ALIGN __attribute__((force_align_arg_pointer))
+#else
+#define FORCE_ALIGN
+#endif
+
+#if defined(__APPLE__)
+#include <dispatch/dispatch.h>
+#elif !defined(_WIN32)
+#include <semaphore.h>
+#endif
+
+void althrd_setname(const char *name);
+
+namespace al {
+
+class semaphore {
+#ifdef _WIN32
+    using native_type = void*;
+#elif defined(__APPLE__)
+    using native_type = dispatch_semaphore_t;
+#else
+    using native_type = sem_t;
+#endif
+    native_type mSem;
+
+public:
+    semaphore(unsigned int initial=0);
+    semaphore(const semaphore&) = delete;
+    ~semaphore();
+
+    semaphore& operator=(const semaphore&) = delete;
+
+    void post();
+    void wait() noexcept;
+    bool try_wait() noexcept;
+};
+
+} // namespace al
+
+#endif /* AL_THREADS_H */
diff --git a/common/vecmat.h b/common/vecmat.h
new file mode 100644 (file)
index 0000000..a45f262
--- /dev/null
@@ -0,0 +1,120 @@
+#ifndef COMMON_VECMAT_H
+#define COMMON_VECMAT_H
+
+#include <array>
+#include <cmath>
+#include <cstddef>
+#include <limits>
+
+#include "alspan.h"
+
+
+namespace alu {
+
+template<typename T>
+class VectorR {
+    static_assert(std::is_floating_point<T>::value, "Must use floating-point types");
+    alignas(16) T mVals[4];
+
+public:
+    constexpr VectorR() noexcept = default;
+    constexpr VectorR(const VectorR&) noexcept = default;
+    constexpr explicit VectorR(T a, T b, T c, T d) noexcept : mVals{a, b, c, d} { }
+
+    constexpr VectorR& operator=(const VectorR&) noexcept = default;
+
+    constexpr T& operator[](size_t idx) noexcept { return mVals[idx]; }
+    constexpr const T& operator[](size_t idx) const noexcept { return mVals[idx]; }
+
+    constexpr VectorR& operator+=(const VectorR &rhs) noexcept
+    {
+        mVals[0] += rhs.mVals[0];
+        mVals[1] += rhs.mVals[1];
+        mVals[2] += rhs.mVals[2];
+        mVals[3] += rhs.mVals[3];
+        return *this;
+    }
+
+    constexpr VectorR operator-(const VectorR &rhs) const noexcept
+    {
+        return VectorR{mVals[0] - rhs.mVals[0], mVals[1] - rhs.mVals[1],
+            mVals[2] - rhs.mVals[2], mVals[3] - rhs.mVals[3]};
+    }
+
+    constexpr T normalize(T limit = std::numeric_limits<T>::epsilon())
+    {
+        limit = std::max(limit, std::numeric_limits<T>::epsilon());
+        const T length_sqr{mVals[0]*mVals[0] + mVals[1]*mVals[1] + mVals[2]*mVals[2]};
+        if(length_sqr > limit*limit)
+        {
+            const T length{std::sqrt(length_sqr)};
+            T inv_length{T{1}/length};
+            mVals[0] *= inv_length;
+            mVals[1] *= inv_length;
+            mVals[2] *= inv_length;
+            return length;
+        }
+        mVals[0] = mVals[1] = mVals[2] = T{0};
+        return T{0};
+    }
+
+    constexpr VectorR cross_product(const alu::VectorR<T> &rhs) const noexcept
+    {
+        return VectorR{
+            mVals[1]*rhs.mVals[2] - mVals[2]*rhs.mVals[1],
+            mVals[2]*rhs.mVals[0] - mVals[0]*rhs.mVals[2],
+            mVals[0]*rhs.mVals[1] - mVals[1]*rhs.mVals[0],
+            T{0}};
+    }
+
+    constexpr T dot_product(const alu::VectorR<T> &rhs) const noexcept
+    { return mVals[0]*rhs.mVals[0] + mVals[1]*rhs.mVals[1] + mVals[2]*rhs.mVals[2]; }
+};
+using Vector = VectorR<float>;
+
+template<typename T>
+class MatrixR {
+    static_assert(std::is_floating_point<T>::value, "Must use floating-point types");
+    alignas(16) T mVals[16];
+
+public:
+    constexpr MatrixR() noexcept = default;
+    constexpr MatrixR(const MatrixR&) noexcept = default;
+    constexpr explicit MatrixR(
+        T aa, T ab, T ac, T ad,
+        T ba, T bb, T bc, T bd,
+        T ca, T cb, T cc, T cd,
+        T da, T db, T dc, T dd) noexcept
+        : mVals{aa,ab,ac,ad, ba,bb,bc,bd, ca,cb,cc,cd, da,db,dc,dd}
+    { }
+
+    constexpr MatrixR& operator=(const MatrixR&) noexcept = default;
+
+    constexpr auto operator[](size_t idx) noexcept { return al::span<T,4>{&mVals[idx*4], 4}; }
+    constexpr auto operator[](size_t idx) const noexcept
+    { return al::span<const T,4>{&mVals[idx*4], 4}; }
+
+    static constexpr MatrixR Identity() noexcept
+    {
+        return MatrixR{
+            T{1}, T{0}, T{0}, T{0},
+            T{0}, T{1}, T{0}, T{0},
+            T{0}, T{0}, T{1}, T{0},
+            T{0}, T{0}, T{0}, T{1}};
+    }
+};
+using Matrix = MatrixR<float>;
+
+template<typename T>
+constexpr VectorR<T> operator*(const MatrixR<T> &mtx, const VectorR<T> &vec) noexcept
+{
+    return VectorR<T>{
+        vec[0]*mtx[0][0] + vec[1]*mtx[1][0] + vec[2]*mtx[2][0] + vec[3]*mtx[3][0],
+        vec[0]*mtx[0][1] + vec[1]*mtx[1][1] + vec[2]*mtx[2][1] + vec[3]*mtx[3][1],
+        vec[0]*mtx[0][2] + vec[1]*mtx[1][2] + vec[2]*mtx[2][2] + vec[3]*mtx[3][2],
+        vec[0]*mtx[0][3] + vec[1]*mtx[1][3] + vec[2]*mtx[2][3] + vec[3]*mtx[3][3]};
+}
+
+} // namespace alu
+
+#endif /* COMMON_VECMAT_H */
diff --git a/common/vector.h b/common/vector.h
new file mode 100644 (file)
index 0000000..1b69d6a
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef AL_VECTOR_H
+#define AL_VECTOR_H
+
+#include <vector>
+
+#include "almalloc.h"
+
+namespace al {
+
+template<typename T, size_t alignment=alignof(T)>
+using vector = std::vector<T, al::allocator<T, alignment>>;
+
+} // namespace al
+
+#endif /* AL_VECTOR_H */
diff --git a/common/win_main_utf8.h b/common/win_main_utf8.h
new file mode 100644 (file)
index 0000000..077af53
--- /dev/null
@@ -0,0 +1,117 @@
+#ifndef WIN_MAIN_UTF8_H
+#define WIN_MAIN_UTF8_H
+
+/* For Windows systems this provides a way to get UTF-8 encoded argv strings,
+ * and also overrides fopen to accept UTF-8 filenames. Working with wmain
+ * directly complicates cross-platform compatibility, while normal main() in
+ * Windows uses the current codepage (which has limited availability of
+ * characters).
+ *
+ * For MinGW, you must link with -municode
+ */
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <shellapi.h>
+#include <wchar.h>
+
+#ifdef __cplusplus
+#include <memory>
+
+#define STATIC_CAST(...) static_cast<__VA_ARGS__>
+#define REINTERPRET_CAST(...) reinterpret_cast<__VA_ARGS__>
+
+#else
+
+#define STATIC_CAST(...) (__VA_ARGS__)
+#define REINTERPRET_CAST(...) (__VA_ARGS__)
+#endif
+
+static FILE *my_fopen(const char *fname, const char *mode)
+{
+    wchar_t *wname=NULL, *wmode=NULL;
+    int namelen, modelen;
+    FILE *file = NULL;
+    errno_t err;
+
+    namelen = MultiByteToWideChar(CP_UTF8, 0, fname, -1, NULL, 0);
+    modelen = MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0);
+
+    if(namelen <= 0 || modelen <= 0)
+    {
+        fprintf(stderr, "Failed to convert UTF-8 fname \"%s\", mode \"%s\"\n", fname, mode);
+        return NULL;
+    }
+
+#ifdef __cplusplus
+    auto strbuf = std::make_unique<wchar_t[]>(static_cast<size_t>(namelen)+modelen);
+    wname = strbuf.get();
+#else
+    wname = (wchar_t*)calloc(sizeof(wchar_t), (size_t)namelen + modelen);
+#endif
+    wmode = wname + namelen;
+    MultiByteToWideChar(CP_UTF8, 0, fname, -1, wname, namelen);
+    MultiByteToWideChar(CP_UTF8, 0, mode, -1, wmode, modelen);
+
+    err = _wfopen_s(&file, wname, wmode);
+    if(err)
+    {
+        errno = err;
+        file = NULL;
+    }
+
+#ifndef __cplusplus
+    free(wname);
+#endif
+    return file;
+}
+#define fopen my_fopen
+
+
+/* SDL overrides main and provides UTF-8 args for us. */
+#if !defined(SDL_MAIN_NEEDED) && !defined(SDL_MAIN_AVAILABLE)
+int my_main(int, char**);
+#define main my_main
+
+#ifdef __cplusplus
+extern "C"
+#endif
+int wmain(int argc, wchar_t **wargv)
+{
+    char **argv;
+    size_t total;
+    int i;
+
+    total = sizeof(*argv) * STATIC_CAST(size_t)(argc);
+    for(i = 0;i < argc;i++)
+        total += STATIC_CAST(size_t)(WideCharToMultiByte(CP_UTF8, 0, wargv[i], -1, NULL, 0, NULL,
+            NULL));
+
+#ifdef __cplusplus
+    auto argbuf = std::make_unique<char[]>(total);
+    argv = reinterpret_cast<char**>(argbuf.get());
+#else
+    argv = (char**)calloc(1, total);
+#endif
+    argv[0] = REINTERPRET_CAST(char*)(argv + argc);
+    for(i = 0;i < argc-1;i++)
+    {
+        int len = WideCharToMultiByte(CP_UTF8, 0, wargv[i], -1, argv[i], 65535, NULL, NULL);
+        argv[i+1] = argv[i] + len;
+    }
+    WideCharToMultiByte(CP_UTF8, 0, wargv[i], -1, argv[i], 65535, NULL, NULL);
+
+#ifdef __cplusplus
+    return main(argc, argv);
+#else
+    i = main(argc, argv);
+
+    free(argv);
+    return i;
+#endif
+}
+#endif /* !defined(SDL_MAIN_NEEDED) && !defined(SDL_MAIN_AVAILABLE) */
+
+#endif /* _WIN32 */
+
+#endif /* WIN_MAIN_UTF8_H */
diff --git a/config.h.in b/config.h.in
new file mode 100644 (file)
index 0000000..477d8c7
--- /dev/null
@@ -0,0 +1,119 @@
+/* Define if deprecated EAX extensions are enabled */
+#cmakedefine ALSOFT_EAX
+
+/* Define if HRTF data is embedded in the library */
+#cmakedefine ALSOFT_EMBED_HRTF_DATA
+
+/* Define if we have the posix_memalign function */
+#cmakedefine HAVE_POSIX_MEMALIGN
+
+/* Define if we have the _aligned_malloc function */
+#cmakedefine HAVE__ALIGNED_MALLOC
+
+/* Define if we have the proc_pidpath function */
+#cmakedefine HAVE_PROC_PIDPATH
+
+/* Define if we have the getopt function */
+#cmakedefine HAVE_GETOPT
+
+/* Define if we have DBus/RTKit */
+#cmakedefine HAVE_RTKIT
+
+/* Define if we have SSE CPU extensions */
+#cmakedefine HAVE_SSE
+#cmakedefine HAVE_SSE2
+#cmakedefine HAVE_SSE3
+#cmakedefine HAVE_SSE4_1
+
+/* Define if we have ARM Neon CPU extensions */
+#cmakedefine HAVE_NEON
+
+/* Define if we have the ALSA backend */
+#cmakedefine HAVE_ALSA
+
+/* Define if we have the OSS backend */
+#cmakedefine HAVE_OSS
+
+/* Define if we have the PipeWire backend */
+#cmakedefine HAVE_PIPEWIRE
+
+/* Define if we have the Solaris backend */
+#cmakedefine HAVE_SOLARIS
+
+/* Define if we have the SndIO backend */
+#cmakedefine HAVE_SNDIO
+
+/* Define if we have the WASAPI backend */
+#cmakedefine HAVE_WASAPI
+
+/* Define if we have the DSound backend */
+#cmakedefine HAVE_DSOUND
+
+/* Define if we have the Windows Multimedia backend */
+#cmakedefine HAVE_WINMM
+
+/* Define if we have the PortAudio backend */
+#cmakedefine HAVE_PORTAUDIO
+
+/* Define if we have the PulseAudio backend */
+#cmakedefine HAVE_PULSEAUDIO
+
+/* Define if we have the JACK backend */
+#cmakedefine HAVE_JACK
+
+/* Define if we have the CoreAudio backend */
+#cmakedefine HAVE_COREAUDIO
+
+/* Define if we have the OpenSL backend */
+#cmakedefine HAVE_OPENSL
+
+/* Define if we have the Oboe backend */
+#cmakedefine HAVE_OBOE
+
+/* Define if we have the Wave Writer backend */
+#cmakedefine HAVE_WAVE
+
+/* Define if we have the SDL2 backend */
+#cmakedefine HAVE_SDL2
+
+/* Define if we have dlfcn.h */
+#cmakedefine HAVE_DLFCN_H
+
+/* Define if we have pthread_np.h */
+#cmakedefine HAVE_PTHREAD_NP_H
+
+/* Define if we have malloc.h */
+#cmakedefine HAVE_MALLOC_H
+
+/* Define if we have cpuid.h */
+#cmakedefine HAVE_CPUID_H
+
+/* Define if we have intrin.h */
+#cmakedefine HAVE_INTRIN_H
+
+/* Define if we have guiddef.h */
+#cmakedefine HAVE_GUIDDEF_H
+
+/* Define if we have initguid.h */
+#cmakedefine HAVE_INITGUID_H
+
+/* Define if we have GCC's __get_cpuid() */
+#cmakedefine HAVE_GCC_GET_CPUID
+
+/* Define if we have the __cpuid() intrinsic */
+#cmakedefine HAVE_CPUID_INTRINSIC
+
+/* Define if we have SSE intrinsics */
+#cmakedefine HAVE_SSE_INTRINSICS
+
+/* Define if we have pthread_setschedparam() */
+#cmakedefine HAVE_PTHREAD_SETSCHEDPARAM
+
+/* Define if we have pthread_setname_np() */
+#cmakedefine HAVE_PTHREAD_SETNAME_NP
+
+/* Define if we have pthread_set_name_np() */
+#cmakedefine HAVE_PTHREAD_SET_NAME_NP
+
+/* Define the installation data directory */
+#cmakedefine ALSOFT_INSTALL_DATADIR "@ALSOFT_INSTALL_DATADIR@"
diff --git a/core/ambdec.cpp b/core/ambdec.cpp
new file mode 100644 (file)
index 0000000..8ca182c
--- /dev/null
@@ -0,0 +1,306 @@
+
+#include "config.h"
+
+#include "ambdec.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstdarg>
+#include <cstddef>
+#include <cstdio>
+#include <iterator>
+#include <sstream>
+#include <string>
+
+#include "albit.h"
+#include "alfstream.h"
+#include "alspan.h"
+#include "opthelpers.h"
+
+
+namespace {
+
+std::string read_word(std::istream &f)
+{
+    std::string ret;
+    f >> ret;
+    return ret;
+}
+
+bool is_at_end(const std::string &buffer, std::size_t endpos)
+{
+    while(endpos < buffer.length() && std::isspace(buffer[endpos]))
+        ++endpos;
+    return !(endpos < buffer.length() && buffer[endpos] != '#');
+}
+
+
+enum class ReaderScope {
+    Global,
+    Speakers,
+    LFMatrix,
+    HFMatrix,
+};
+
+#ifdef __USE_MINGW_ANSI_STDIO
+[[gnu::format(gnu_printf,2,3)]]
+#else
+[[gnu::format(printf,2,3)]]
+#endif
+al::optional<std::string> make_error(size_t linenum, const char *fmt, ...)
+{
+    al::optional<std::string> ret;
+    auto &str = ret.emplace();
+
+    str.resize(256);
+    int printed{std::snprintf(const_cast<char*>(str.data()), str.length(), "Line %zu: ", linenum)};
+    if(printed < 0) printed = 0;
+    auto plen = std::min(static_cast<size_t>(printed), str.length());
+
+    std::va_list args, args2;
+    va_start(args, fmt);
+    va_copy(args2, args);
+    const int msglen{std::vsnprintf(&str[plen], str.size()-plen, fmt, args)};
+    if(msglen >= 0 && static_cast<size_t>(msglen) >= str.size()-plen)
+    {
+        str.resize(static_cast<size_t>(msglen) + plen + 1u);
+        std::vsnprintf(&str[plen], str.size()-plen, fmt, args2);
+    }
+    va_end(args2);
+    va_end(args);
+
+    return ret;
+}
+
+} // namespace
+
+AmbDecConf::~AmbDecConf() = default;
+
+
+al::optional<std::string> AmbDecConf::load(const char *fname) noexcept
+{
+    al::ifstream f{fname};
+    if(!f.is_open())
+        return std::string("Failed to open file \"")+fname+"\"";
+
+    ReaderScope scope{ReaderScope::Global};
+    size_t speaker_pos{0};
+    size_t lfmatrix_pos{0};
+    size_t hfmatrix_pos{0};
+    size_t linenum{0};
+
+    std::string buffer;
+    while(f.good() && std::getline(f, buffer))
+    {
+        ++linenum;
+
+        std::istringstream istr{buffer};
+        std::string command{read_word(istr)};
+        if(command.empty() || command[0] == '#')
+            continue;
+
+        if(command == "/}")
+        {
+            if(scope == ReaderScope::Global)
+                return make_error(linenum, "Unexpected /} in global scope");
+            scope = ReaderScope::Global;
+            continue;
+        }
+
+        if(scope == ReaderScope::Speakers)
+        {
+            if(command == "add_spkr")
+            {
+                if(speaker_pos == NumSpeakers)
+                    return make_error(linenum, "Too many speakers specified");
+
+                AmbDecConf::SpeakerConf &spkr = Speakers[speaker_pos++];
+                istr >> spkr.Name;
+                istr >> spkr.Distance;
+                istr >> spkr.Azimuth;
+                istr >> spkr.Elevation;
+                istr >> spkr.Connection;
+            }
+            else
+                return make_error(linenum, "Unexpected speakers command: %s", command.c_str());
+        }
+        else if(scope == ReaderScope::LFMatrix || scope == ReaderScope::HFMatrix)
+        {
+            auto &gains = (scope == ReaderScope::LFMatrix) ? LFOrderGain : HFOrderGain;
+            auto *matrix = (scope == ReaderScope::LFMatrix) ? LFMatrix : HFMatrix;
+            auto &pos = (scope == ReaderScope::LFMatrix) ? lfmatrix_pos : hfmatrix_pos;
+
+            if(command == "order_gain")
+            {
+                size_t toread{(ChanMask > Ambi3OrderMask) ? 5u : 4u};
+                std::size_t curgain{0u};
+                float value{};
+                while(toread)
+                {
+                    --toread;
+                    istr >> value;
+                    if(curgain < al::size(gains))
+                        gains[curgain++] = value;
+                }
+            }
+            else if(command == "add_row")
+            {
+                if(pos == NumSpeakers)
+                    return make_error(linenum, "Too many matrix rows specified");
+
+                unsigned int mask{ChanMask};
+
+                AmbDecConf::CoeffArray &mtxrow = matrix[pos++];
+                mtxrow.fill(0.0f);
+
+                float value{};
+                while(mask)
+                {
+                    auto idx = static_cast<unsigned>(al::countr_zero(mask));
+                    mask &= ~(1u << idx);
+
+                    istr >> value;
+                    if(idx < mtxrow.size())
+                        mtxrow[idx] = value;
+                }
+            }
+            else
+                return make_error(linenum, "Unexpected matrix command: %s", command.c_str());
+        }
+        // Global scope commands
+        else if(command == "/description")
+        {
+            while(istr.good() && std::isspace(istr.peek()))
+                istr.ignore();
+            std::getline(istr, Description);
+            while(!Description.empty() && std::isspace(Description.back()))
+                Description.pop_back();
+        }
+        else if(command == "/version")
+        {
+            if(Version)
+                return make_error(linenum, "Duplicate version definition");
+            istr >> Version;
+            if(Version != 3)
+                return make_error(linenum, "Unsupported version: %d", Version);
+        }
+        else if(command == "/dec/chan_mask")
+        {
+            if(ChanMask)
+                return make_error(linenum, "Duplicate chan_mask definition");
+            istr >> std::hex >> ChanMask >> std::dec;
+
+            if(!ChanMask || ChanMask > Ambi4OrderMask)
+                return make_error(linenum, "Invalid chan_mask: 0x%x", ChanMask);
+            if(ChanMask > Ambi3OrderMask && CoeffScale == AmbDecScale::FuMa)
+                return make_error(linenum, "FuMa not compatible with over third-order");
+        }
+        else if(command == "/dec/freq_bands")
+        {
+            if(FreqBands)
+                return make_error(linenum, "Duplicate freq_bands");
+            istr >> FreqBands;
+            if(FreqBands != 1 && FreqBands != 2)
+                return make_error(linenum, "Invalid freq_bands: %u", FreqBands);
+        }
+        else if(command == "/dec/speakers")
+        {
+            if(NumSpeakers)
+                return make_error(linenum, "Duplicate speakers");
+            istr >> NumSpeakers;
+            if(!NumSpeakers)
+                return make_error(linenum, "Invalid speakers: %zu", NumSpeakers);
+            Speakers = std::make_unique<SpeakerConf[]>(NumSpeakers);
+        }
+        else if(command == "/dec/coeff_scale")
+        {
+            if(CoeffScale != AmbDecScale::Unset)
+                return make_error(linenum, "Duplicate coeff_scale");
+
+            std::string scale{read_word(istr)};
+            if(scale == "n3d") CoeffScale = AmbDecScale::N3D;
+            else if(scale == "sn3d") CoeffScale = AmbDecScale::SN3D;
+            else if(scale == "fuma") CoeffScale = AmbDecScale::FuMa;
+            else
+                return make_error(linenum, "Unexpected coeff_scale: %s", scale.c_str());
+
+            if(ChanMask > Ambi3OrderMask && CoeffScale == AmbDecScale::FuMa)
+                return make_error(linenum, "FuMa not compatible with over third-order");
+        }
+        else if(command == "/opt/xover_freq")
+        {
+            istr >> XOverFreq;
+        }
+        else if(command == "/opt/xover_ratio")
+        {
+            istr >> XOverRatio;
+        }
+        else if(command == "/opt/input_scale" || command == "/opt/nfeff_comp"
+            || command == "/opt/delay_comp" || command == "/opt/level_comp")
+        {
+            /* Unused */
+            read_word(istr);
+        }
+        else if(command == "/speakers/{")
+        {
+            if(!NumSpeakers)
+                return make_error(linenum, "Speakers defined without a count");
+            scope = ReaderScope::Speakers;
+        }
+        else if(command == "/lfmatrix/{" || command == "/hfmatrix/{" || command == "/matrix/{")
+        {
+            if(!NumSpeakers)
+                return make_error(linenum, "Matrix defined without a speaker count");
+            if(!ChanMask)
+                return make_error(linenum, "Matrix defined without a channel mask");
+
+            if(!Matrix)
+            {
+                Matrix = std::make_unique<CoeffArray[]>(NumSpeakers * FreqBands);
+                LFMatrix = Matrix.get();
+                HFMatrix = LFMatrix + NumSpeakers*(FreqBands-1);
+            }
+
+            if(FreqBands == 1)
+            {
+                if(command != "/matrix/{")
+                    return make_error(linenum, "Unexpected \"%s\" for a single-band decoder",
+                        command.c_str());
+                scope = ReaderScope::HFMatrix;
+            }
+            else
+            {
+                if(command == "/lfmatrix/{")
+                    scope = ReaderScope::LFMatrix;
+                else if(command == "/hfmatrix/{")
+                    scope = ReaderScope::HFMatrix;
+                else
+                    return make_error(linenum, "Unexpected \"%s\" for a dual-band decoder",
+                        command.c_str());
+            }
+        }
+        else if(command == "/end")
+        {
+            const auto endpos = static_cast<std::size_t>(istr.tellg());
+            if(!is_at_end(buffer, endpos))
+                return make_error(linenum, "Extra junk on end: %s", buffer.substr(endpos).c_str());
+
+            if(speaker_pos < NumSpeakers || hfmatrix_pos < NumSpeakers
+                || (FreqBands == 2 && lfmatrix_pos < NumSpeakers))
+                return make_error(linenum, "Incomplete decoder definition");
+            if(CoeffScale == AmbDecScale::Unset)
+                return make_error(linenum, "No coefficient scaling defined");
+
+            return al::nullopt;
+        }
+        else
+            return make_error(linenum, "Unexpected command: %s", command.c_str());
+
+        istr.clear();
+        const auto endpos = static_cast<std::size_t>(istr.tellg());
+        if(!is_at_end(buffer, endpos))
+            return make_error(linenum, "Extra junk on line: %s", buffer.substr(endpos).c_str());
+        buffer.clear();
+    }
+    return make_error(linenum, "Unexpected end of file");
+}
diff --git a/core/ambdec.h b/core/ambdec.h
new file mode 100644 (file)
index 0000000..7f73978
--- /dev/null
@@ -0,0 +1,55 @@
+#ifndef CORE_AMBDEC_H
+#define CORE_AMBDEC_H
+
+#include <array>
+#include <memory>
+#include <string>
+
+#include "aloptional.h"
+#include "core/ambidefs.h"
+
+/* Helpers to read .ambdec configuration files. */
+
+enum class AmbDecScale {
+    Unset,
+    N3D,
+    SN3D,
+    FuMa,
+};
+struct AmbDecConf {
+    std::string Description;
+    int Version{0}; /* Must be 3 */
+
+    unsigned int ChanMask{0u};
+    unsigned int FreqBands{0u}; /* Must be 1 or 2 */
+    AmbDecScale CoeffScale{AmbDecScale::Unset};
+
+    float XOverFreq{0.0f};
+    float XOverRatio{0.0f};
+
+    struct SpeakerConf {
+        std::string Name;
+        float Distance{0.0f};
+        float Azimuth{0.0f};
+        float Elevation{0.0f};
+        std::string Connection;
+    };
+    size_t NumSpeakers{0};
+    std::unique_ptr<SpeakerConf[]> Speakers;
+
+    using CoeffArray = std::array<float,MaxAmbiChannels>;
+    std::unique_ptr<CoeffArray[]> Matrix;
+
+    /* Unused when FreqBands == 1 */
+    float LFOrderGain[MaxAmbiOrder+1]{};
+    CoeffArray *LFMatrix;
+
+    float HFOrderGain[MaxAmbiOrder+1]{};
+    CoeffArray *HFMatrix;
+
+    ~AmbDecConf();
+
+    al::optional<std::string> load(const char *fname) noexcept;
+};
+
+#endif /* CORE_AMBDEC_H */
diff --git a/core/ambidefs.cpp b/core/ambidefs.cpp
new file mode 100644 (file)
index 0000000..70d6f35
--- /dev/null
@@ -0,0 +1,308 @@
+
+#include "config.h"
+
+#include "ambidefs.h"
+
+#include "alnumbers.h"
+
+
+namespace {
+
+using AmbiChannelFloatArray = std::array<float,MaxAmbiChannels>;
+
+constexpr auto inv_sqrt2f = static_cast<float>(1.0/al::numbers::sqrt2);
+constexpr auto inv_sqrt3f = static_cast<float>(1.0/al::numbers::sqrt3);
+
+
+/* These HF gains are derived from the same 32-point speaker array. The scale
+ * factor between orders represents the same scale factors for any (regular)
+ * speaker array decoder. e.g. Given a first-order source and second-order
+ * output, applying an HF scale of HFScales[1][0] / HFScales[2][0] to channel 0
+ * will result in that channel being subsequently decoded for second-order as
+ * if it was a first-order decoder for that same speaker array.
+ */
+constexpr std::array<std::array<float,MaxAmbiOrder+1>,MaxAmbiOrder+1> HFScales{{
+    {{ 4.000000000e+00f, 2.309401077e+00f, 1.192569588e+00f, 7.189495850e-01f }},
+    {{ 4.000000000e+00f, 2.309401077e+00f, 1.192569588e+00f, 7.189495850e-01f }},
+    {{ 2.981423970e+00f, 2.309401077e+00f, 1.192569588e+00f, 7.189495850e-01f }},
+    {{ 2.359168820e+00f, 2.031565936e+00f, 1.444598386e+00f, 7.189495850e-01f }},
+    /* 1.947005434e+00f, 1.764337084e+00f, 1.424707344e+00f, 9.755104127e-01f, 4.784482742e-01f */
+}};
+
+/* Same as above, but using a 10-point horizontal-only speaker array. Should
+ * only be used when the device is mixing in 2D B-Format for horizontal-only
+ * output.
+ */
+constexpr std::array<std::array<float,MaxAmbiOrder+1>,MaxAmbiOrder+1> HFScales2D{{
+    {{ 2.236067977e+00f, 1.581138830e+00f, 9.128709292e-01f, 6.050756345e-01f }},
+    {{ 2.236067977e+00f, 1.581138830e+00f, 9.128709292e-01f, 6.050756345e-01f }},
+    {{ 1.825741858e+00f, 1.581138830e+00f, 9.128709292e-01f, 6.050756345e-01f }},
+    {{ 1.581138830e+00f, 1.460781803e+00f, 1.118033989e+00f, 6.050756345e-01f }},
+    /* 1.414213562e+00f, 1.344997024e+00f, 1.144122806e+00f, 8.312538756e-01f, 4.370160244e-01f */
+}};
+
+
+/* This calculates a first-order "upsampler" matrix. It combines a first-order
+ * decoder matrix with a max-order encoder matrix, creating a matrix that
+ * behaves as if the B-Format input signal is first decoded to a speaker array
+ * at first-order, then those speaker feeds are encoded to a higher-order
+ * signal. While not perfect, this should accurately encode a lower-order
+ * signal into a higher-order signal.
+ */
+constexpr std::array<std::array<float,4>,8> FirstOrderDecoder{{
+    {{ 1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f, }},
+    {{ 1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f, }},
+    {{ 1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f,  1.250000000e-01f, }},
+    {{ 1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f, }},
+    {{ 1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f, }},
+    {{ 1.250000000e-01f,  1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f, }},
+    {{ 1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f,  1.250000000e-01f, }},
+    {{ 1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f, -1.250000000e-01f, }},
+}};
+constexpr std::array<AmbiChannelFloatArray,8> FirstOrderEncoder{{
+    CalcAmbiCoeffs( inv_sqrt3f,  inv_sqrt3f,  inv_sqrt3f),
+    CalcAmbiCoeffs( inv_sqrt3f,  inv_sqrt3f, -inv_sqrt3f),
+    CalcAmbiCoeffs(-inv_sqrt3f,  inv_sqrt3f,  inv_sqrt3f),
+    CalcAmbiCoeffs(-inv_sqrt3f,  inv_sqrt3f, -inv_sqrt3f),
+    CalcAmbiCoeffs( inv_sqrt3f, -inv_sqrt3f,  inv_sqrt3f),
+    CalcAmbiCoeffs( inv_sqrt3f, -inv_sqrt3f, -inv_sqrt3f),
+    CalcAmbiCoeffs(-inv_sqrt3f, -inv_sqrt3f,  inv_sqrt3f),
+    CalcAmbiCoeffs(-inv_sqrt3f, -inv_sqrt3f, -inv_sqrt3f),
+}};
+static_assert(FirstOrderDecoder.size() == FirstOrderEncoder.size(), "First-order mismatch");
+
+/* This calculates a 2D first-order "upsampler" matrix. Same as the first-order
+ * matrix, just using a more optimized speaker array for horizontal-only
+ * content.
+ */
+constexpr std::array<std::array<float,4>,4> FirstOrder2DDecoder{{
+    {{ 2.500000000e-01f,  2.041241452e-01f, 0.0f,  2.041241452e-01f, }},
+    {{ 2.500000000e-01f,  2.041241452e-01f, 0.0f, -2.041241452e-01f, }},
+    {{ 2.500000000e-01f, -2.041241452e-01f, 0.0f,  2.041241452e-01f, }},
+    {{ 2.500000000e-01f, -2.041241452e-01f, 0.0f, -2.041241452e-01f, }},
+}};
+constexpr std::array<AmbiChannelFloatArray,4> FirstOrder2DEncoder{{
+    CalcAmbiCoeffs( inv_sqrt2f, 0.0f,  inv_sqrt2f),
+    CalcAmbiCoeffs( inv_sqrt2f, 0.0f, -inv_sqrt2f),
+    CalcAmbiCoeffs(-inv_sqrt2f, 0.0f,  inv_sqrt2f),
+    CalcAmbiCoeffs(-inv_sqrt2f, 0.0f, -inv_sqrt2f),
+}};
+static_assert(FirstOrder2DDecoder.size() == FirstOrder2DEncoder.size(), "First-order 2D mismatch");
+
+
+/* This calculates a second-order "upsampler" matrix. Same as the first-order
+ * matrix, just using a slightly more dense speaker array suitable for second-
+ * order content.
+ */
+constexpr std::array<std::array<float,9>,12> SecondOrderDecoder{{
+    {{ 8.333333333e-02f,  0.000000000e+00f, -7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f, -1.443375673e-01f,  1.167715449e-01f, }},
+    {{ 8.333333333e-02f, -1.227808683e-01f,  0.000000000e+00f,  7.588274978e-02f, -1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, }},
+    {{ 8.333333333e-02f, -7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, }},
+    {{ 8.333333333e-02f,  0.000000000e+00f,  7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f,  1.443375673e-01f,  1.167715449e-01f, }},
+    {{ 8.333333333e-02f, -1.227808683e-01f,  0.000000000e+00f, -7.588274978e-02f,  1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, }},
+    {{ 8.333333333e-02f,  7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, }},
+    {{ 8.333333333e-02f,  0.000000000e+00f, -7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f,  1.443375673e-01f,  1.167715449e-01f, }},
+    {{ 8.333333333e-02f,  1.227808683e-01f,  0.000000000e+00f, -7.588274978e-02f, -1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, }},
+    {{ 8.333333333e-02f,  7.588274978e-02f,  1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f,  1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, }},
+    {{ 8.333333333e-02f,  0.000000000e+00f,  7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f, -1.591525047e-02f, -1.443375673e-01f,  1.167715449e-01f, }},
+    {{ 8.333333333e-02f,  1.227808683e-01f,  0.000000000e+00f,  7.588274978e-02f,  1.443375673e-01f,  0.000000000e+00f, -9.316949906e-02f,  0.000000000e+00f, -7.216878365e-02f, }},
+    {{ 8.333333333e-02f, -7.588274978e-02f, -1.227808683e-01f,  0.000000000e+00f,  0.000000000e+00f,  1.443375673e-01f,  1.090847495e-01f,  0.000000000e+00f, -4.460276122e-02f, }},
+}};
+constexpr std::array<AmbiChannelFloatArray,12> SecondOrderEncoder{{
+    CalcAmbiCoeffs( 0.000000000e+00f, -5.257311121e-01f,  8.506508084e-01f),
+    CalcAmbiCoeffs(-8.506508084e-01f,  0.000000000e+00f,  5.257311121e-01f),
+    CalcAmbiCoeffs(-5.257311121e-01f,  8.506508084e-01f,  0.000000000e+00f),
+    CalcAmbiCoeffs( 0.000000000e+00f,  5.257311121e-01f,  8.506508084e-01f),
+    CalcAmbiCoeffs(-8.506508084e-01f,  0.000000000e+00f, -5.257311121e-01f),
+    CalcAmbiCoeffs( 5.257311121e-01f, -8.506508084e-01f,  0.000000000e+00f),
+    CalcAmbiCoeffs( 0.000000000e+00f, -5.257311121e-01f, -8.506508084e-01f),
+    CalcAmbiCoeffs( 8.506508084e-01f,  0.000000000e+00f, -5.257311121e-01f),
+    CalcAmbiCoeffs( 5.257311121e-01f,  8.506508084e-01f,  0.000000000e+00f),
+    CalcAmbiCoeffs( 0.000000000e+00f,  5.257311121e-01f, -8.506508084e-01f),
+    CalcAmbiCoeffs( 8.506508084e-01f,  0.000000000e+00f,  5.257311121e-01f),
+    CalcAmbiCoeffs(-5.257311121e-01f, -8.506508084e-01f,  0.000000000e+00f),
+}};
+static_assert(SecondOrderDecoder.size() == SecondOrderEncoder.size(), "Second-order mismatch");
+
+/* This calculates a 2D second-order "upsampler" matrix. Same as the second-
+ * order matrix, just using a more optimized speaker array for horizontal-only
+ * content.
+ */
+constexpr std::array<std::array<float,9>,6> SecondOrder2DDecoder{{
+    {{ 1.666666667e-01f, -9.622504486e-02f, 0.0f,  1.666666667e-01f, -1.490711985e-01f, 0.0f, 0.0f, 0.0f,  8.606629658e-02f, }},
+    {{ 1.666666667e-01f, -1.924500897e-01f, 0.0f,  0.000000000e+00f,  0.000000000e+00f, 0.0f, 0.0f, 0.0f, -1.721325932e-01f, }},
+    {{ 1.666666667e-01f, -9.622504486e-02f, 0.0f, -1.666666667e-01f,  1.490711985e-01f, 0.0f, 0.0f, 0.0f,  8.606629658e-02f, }},
+    {{ 1.666666667e-01f,  9.622504486e-02f, 0.0f, -1.666666667e-01f, -1.490711985e-01f, 0.0f, 0.0f, 0.0f,  8.606629658e-02f, }},
+    {{ 1.666666667e-01f,  1.924500897e-01f, 0.0f,  0.000000000e+00f,  0.000000000e+00f, 0.0f, 0.0f, 0.0f, -1.721325932e-01f, }},
+    {{ 1.666666667e-01f,  9.622504486e-02f, 0.0f,  1.666666667e-01f,  1.490711985e-01f, 0.0f, 0.0f, 0.0f,  8.606629658e-02f, }},
+}};
+constexpr std::array<AmbiChannelFloatArray,6> SecondOrder2DEncoder{{
+    CalcAmbiCoeffs(-0.50000000000f, 0.0f,  0.86602540379f),
+    CalcAmbiCoeffs(-1.00000000000f, 0.0f,  0.00000000000f),
+    CalcAmbiCoeffs(-0.50000000000f, 0.0f, -0.86602540379f),
+    CalcAmbiCoeffs( 0.50000000000f, 0.0f, -0.86602540379f),
+    CalcAmbiCoeffs( 1.00000000000f, 0.0f,  0.00000000000f),
+    CalcAmbiCoeffs( 0.50000000000f, 0.0f,  0.86602540379f),
+}};
+static_assert(SecondOrder2DDecoder.size() == SecondOrder2DEncoder.size(),
+    "Second-order 2D mismatch");
+
+
+/* This calculates a third-order "upsampler" matrix. Same as the first-order
+ * matrix, just using a more dense speaker array suitable for third-order
+ * content.
+ */
+constexpr std::array<std::array<float,16>,20> ThirdOrderDecoder{{
+    {{ 5.000000000e-02f,  3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f,  6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f, -1.256118221e-01f,  0.000000000e+00f,  1.126112056e-01f,  7.944389175e-02f,  0.000000000e+00f,  2.421151497e-02f,  0.000000000e+00f, }},
+    {{ 5.000000000e-02f, -3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f,  1.256118221e-01f,  0.000000000e+00f, -1.126112056e-01f,  7.944389175e-02f,  0.000000000e+00f,  2.421151497e-02f,  0.000000000e+00f, }},
+    {{ 5.000000000e-02f,  3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f, -1.256118221e-01f,  0.000000000e+00f,  1.126112056e-01f, -7.944389175e-02f,  0.000000000e+00f, -2.421151497e-02f,  0.000000000e+00f, }},
+    {{ 5.000000000e-02f, -3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f,  6.454972244e-02f,  9.045084972e-02f,  0.000000000e+00f, -1.232790000e-02f,  1.256118221e-01f,  0.000000000e+00f, -1.126112056e-01f, -7.944389175e-02f,  0.000000000e+00f, -2.421151497e-02f,  0.000000000e+00f, }},
+    {{ 5.000000000e-02f,  8.090169944e-02f,  0.000000000e+00f,  3.090169944e-02f,  6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f, -7.763237543e-02f,  0.000000000e+00f, -2.950836627e-02f,  0.000000000e+00f, -1.497759251e-01f,  0.000000000e+00f, -7.763237543e-02f, }},
+    {{ 5.000000000e-02f,  8.090169944e-02f,  0.000000000e+00f, -3.090169944e-02f, -6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f, -7.763237543e-02f,  0.000000000e+00f, -2.950836627e-02f,  0.000000000e+00f,  1.497759251e-01f,  0.000000000e+00f,  7.763237543e-02f, }},
+    {{ 5.000000000e-02f, -8.090169944e-02f,  0.000000000e+00f,  3.090169944e-02f, -6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f,  7.763237543e-02f,  0.000000000e+00f,  2.950836627e-02f,  0.000000000e+00f, -1.497759251e-01f,  0.000000000e+00f, -7.763237543e-02f, }},
+    {{ 5.000000000e-02f, -8.090169944e-02f,  0.000000000e+00f, -3.090169944e-02f,  6.454972244e-02f,  0.000000000e+00f, -5.590169944e-02f,  0.000000000e+00f, -7.216878365e-02f,  7.763237543e-02f,  0.000000000e+00f,  2.950836627e-02f,  0.000000000e+00f,  1.497759251e-01f,  0.000000000e+00f,  7.763237543e-02f, }},
+    {{ 5.000000000e-02f,  0.000000000e+00f,  3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f,  6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f,  3.034486645e-02f, -6.779013272e-02f,  1.659481923e-01f,  4.797944664e-02f, }},
+    {{ 5.000000000e-02f,  0.000000000e+00f,  3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f, -6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f,  3.034486645e-02f,  6.779013272e-02f,  1.659481923e-01f, -4.797944664e-02f, }},
+    {{ 5.000000000e-02f,  0.000000000e+00f, -3.090169944e-02f,  8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f, -6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f, -3.034486645e-02f, -6.779013272e-02f, -1.659481923e-01f,  4.797944664e-02f, }},
+    {{ 5.000000000e-02f,  0.000000000e+00f, -3.090169944e-02f, -8.090169944e-02f,  0.000000000e+00f,  0.000000000e+00f, -3.454915028e-02f,  6.454972244e-02f,  8.449668365e-02f,  0.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f, -3.034486645e-02f,  6.779013272e-02f, -1.659481923e-01f, -4.797944664e-02f, }},
+    {{ 5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f,  6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f,  6.338656910e-02f, -1.092600649e-02f, -7.364853795e-02f,  1.011266756e-01f, -7.086833869e-02f, -1.482646439e-02f, }},
+    {{ 5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f, -6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f, -6.338656910e-02f, -1.092600649e-02f, -7.364853795e-02f, -1.011266756e-01f, -7.086833869e-02f,  1.482646439e-02f, }},
+    {{ 5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f,  5.000000000e-02f, -6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f, -6.338656910e-02f,  1.092600649e-02f, -7.364853795e-02f,  1.011266756e-01f, -7.086833869e-02f, -1.482646439e-02f, }},
+    {{ 5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f,  6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f,  6.338656910e-02f,  1.092600649e-02f, -7.364853795e-02f, -1.011266756e-01f, -7.086833869e-02f,  1.482646439e-02f, }},
+    {{ 5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f,  6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f, -6.338656910e-02f, -1.092600649e-02f,  7.364853795e-02f,  1.011266756e-01f,  7.086833869e-02f, -1.482646439e-02f, }},
+    {{ 5.000000000e-02f,  5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f, -6.454972244e-02f, -6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f,  1.016220987e-01f,  6.338656910e-02f, -1.092600649e-02f,  7.364853795e-02f, -1.011266756e-01f,  7.086833869e-02f,  1.482646439e-02f, }},
+    {{ 5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f,  5.000000000e-02f, -6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f, -6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f,  6.338656910e-02f,  1.092600649e-02f,  7.364853795e-02f,  1.011266756e-01f,  7.086833869e-02f, -1.482646439e-02f, }},
+    {{ 5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f, -5.000000000e-02f,  6.454972244e-02f,  6.454972244e-02f,  0.000000000e+00f,  6.454972244e-02f,  0.000000000e+00f, -1.016220987e-01f, -6.338656910e-02f,  1.092600649e-02f,  7.364853795e-02f, -1.011266756e-01f,  7.086833869e-02f,  1.482646439e-02f, }},
+}};
+constexpr std::array<AmbiChannelFloatArray,20> ThirdOrderEncoder{{
+    CalcAmbiCoeffs( 0.35682208976f,  0.93417235897f,  0.00000000000f),
+    CalcAmbiCoeffs(-0.35682208976f,  0.93417235897f,  0.00000000000f),
+    CalcAmbiCoeffs( 0.35682208976f, -0.93417235897f,  0.00000000000f),
+    CalcAmbiCoeffs(-0.35682208976f, -0.93417235897f,  0.00000000000f),
+    CalcAmbiCoeffs( 0.93417235897f,  0.00000000000f,  0.35682208976f),
+    CalcAmbiCoeffs( 0.93417235897f,  0.00000000000f, -0.35682208976f),
+    CalcAmbiCoeffs(-0.93417235897f,  0.00000000000f,  0.35682208976f),
+    CalcAmbiCoeffs(-0.93417235897f,  0.00000000000f, -0.35682208976f),
+    CalcAmbiCoeffs( 0.00000000000f,  0.35682208976f,  0.93417235897f),
+    CalcAmbiCoeffs( 0.00000000000f,  0.35682208976f, -0.93417235897f),
+    CalcAmbiCoeffs( 0.00000000000f, -0.35682208976f,  0.93417235897f),
+    CalcAmbiCoeffs( 0.00000000000f, -0.35682208976f, -0.93417235897f),
+    CalcAmbiCoeffs(     inv_sqrt3f,      inv_sqrt3f,      inv_sqrt3f),
+    CalcAmbiCoeffs(     inv_sqrt3f,      inv_sqrt3f,     -inv_sqrt3f),
+    CalcAmbiCoeffs(    -inv_sqrt3f,      inv_sqrt3f,      inv_sqrt3f),
+    CalcAmbiCoeffs(    -inv_sqrt3f,      inv_sqrt3f,     -inv_sqrt3f),
+    CalcAmbiCoeffs(     inv_sqrt3f,     -inv_sqrt3f,      inv_sqrt3f),
+    CalcAmbiCoeffs(     inv_sqrt3f,     -inv_sqrt3f,     -inv_sqrt3f),
+    CalcAmbiCoeffs(    -inv_sqrt3f,     -inv_sqrt3f,      inv_sqrt3f),
+    CalcAmbiCoeffs(    -inv_sqrt3f,     -inv_sqrt3f,     -inv_sqrt3f),
+}};
+static_assert(ThirdOrderDecoder.size() == ThirdOrderEncoder.size(), "Third-order mismatch");
+
+/* This calculates a 2D third-order "upsampler" matrix. Same as the third-order
+ * matrix, just using a more optimized speaker array for horizontal-only
+ * content.
+ */
+constexpr std::array<std::array<float,16>,8> ThirdOrder2DDecoder{{
+    {{ 1.250000000e-01f, -5.523559567e-02f, 0.0f,  1.333505242e-01f, -9.128709292e-02f, 0.0f, 0.0f, 0.0f,  9.128709292e-02f, -1.104247249e-01f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  4.573941867e-02f, }},
+    {{ 1.250000000e-01f, -1.333505242e-01f, 0.0f,  5.523559567e-02f, -9.128709292e-02f, 0.0f, 0.0f, 0.0f, -9.128709292e-02f,  4.573941867e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.104247249e-01f, }},
+    {{ 1.250000000e-01f, -1.333505242e-01f, 0.0f, -5.523559567e-02f,  9.128709292e-02f, 0.0f, 0.0f, 0.0f, -9.128709292e-02f,  4.573941867e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  1.104247249e-01f, }},
+    {{ 1.250000000e-01f, -5.523559567e-02f, 0.0f, -1.333505242e-01f,  9.128709292e-02f, 0.0f, 0.0f, 0.0f,  9.128709292e-02f, -1.104247249e-01f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -4.573941867e-02f, }},
+    {{ 1.250000000e-01f,  5.523559567e-02f, 0.0f, -1.333505242e-01f, -9.128709292e-02f, 0.0f, 0.0f, 0.0f,  9.128709292e-02f,  1.104247249e-01f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -4.573941867e-02f, }},
+    {{ 1.250000000e-01f,  1.333505242e-01f, 0.0f, -5.523559567e-02f, -9.128709292e-02f, 0.0f, 0.0f, 0.0f, -9.128709292e-02f, -4.573941867e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  1.104247249e-01f, }},
+    {{ 1.250000000e-01f,  1.333505242e-01f, 0.0f,  5.523559567e-02f,  9.128709292e-02f, 0.0f, 0.0f, 0.0f, -9.128709292e-02f, -4.573941867e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.104247249e-01f, }},
+    {{ 1.250000000e-01f,  5.523559567e-02f, 0.0f,  1.333505242e-01f,  9.128709292e-02f, 0.0f, 0.0f, 0.0f,  9.128709292e-02f,  1.104247249e-01f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  4.573941867e-02f, }},
+}};
+constexpr std::array<AmbiChannelFloatArray,8> ThirdOrder2DEncoder{{
+    CalcAmbiCoeffs(-0.38268343237f, 0.0f,  0.92387953251f),
+    CalcAmbiCoeffs(-0.92387953251f, 0.0f,  0.38268343237f),
+    CalcAmbiCoeffs(-0.92387953251f, 0.0f, -0.38268343237f),
+    CalcAmbiCoeffs(-0.38268343237f, 0.0f, -0.92387953251f),
+    CalcAmbiCoeffs( 0.38268343237f, 0.0f, -0.92387953251f),
+    CalcAmbiCoeffs( 0.92387953251f, 0.0f, -0.38268343237f),
+    CalcAmbiCoeffs( 0.92387953251f, 0.0f,  0.38268343237f),
+    CalcAmbiCoeffs( 0.38268343237f, 0.0f,  0.92387953251f),
+}};
+static_assert(ThirdOrder2DDecoder.size() == ThirdOrder2DEncoder.size(), "Third-order 2D mismatch");
+
+
+/* This calculates a 2D fourth-order "upsampler" matrix. There is no 3D fourth-
+ * order upsampler since fourth-order is the max order we'll be supporting for
+ * the foreseeable future. This is only necessary for mixing horizontal-only
+ * fourth-order content to 3D.
+ */
+constexpr std::array<std::array<float,25>,10> FourthOrder2DDecoder{{
+    {{ 1.000000000e-01f,  3.568220898e-02f, 0.0f,  1.098185471e-01f,  6.070619982e-02f, 0.0f, 0.0f, 0.0f,  8.355491589e-02f,  7.735682057e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  5.620301997e-02f,  8.573754253e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  2.785781628e-02f, }},
+    {{ 1.000000000e-01f,  9.341723590e-02f, 0.0f,  6.787159473e-02f,  9.822469464e-02f, 0.0f, 0.0f, 0.0f, -3.191513794e-02f,  2.954767620e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -9.093839659e-02f, -5.298871540e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -7.293270986e-02f, }},
+    {{ 1.000000000e-01f,  1.154700538e-01f, 0.0f,  0.000000000e+00f,  0.000000000e+00f, 0.0f, 0.0f, 0.0f, -1.032795559e-01f, -9.561828875e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  0.000000000e+00f,  0.000000000e+00f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  9.014978717e-02f, }},
+    {{ 1.000000000e-01f,  9.341723590e-02f, 0.0f, -6.787159473e-02f, -9.822469464e-02f, 0.0f, 0.0f, 0.0f, -3.191513794e-02f,  2.954767620e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  9.093839659e-02f,  5.298871540e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -7.293270986e-02f, }},
+    {{ 1.000000000e-01f,  3.568220898e-02f, 0.0f, -1.098185471e-01f, -6.070619982e-02f, 0.0f, 0.0f, 0.0f,  8.355491589e-02f,  7.735682057e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -5.620301997e-02f, -8.573754253e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  2.785781628e-02f, }},
+    {{ 1.000000000e-01f, -3.568220898e-02f, 0.0f, -1.098185471e-01f,  6.070619982e-02f, 0.0f, 0.0f, 0.0f,  8.355491589e-02f, -7.735682057e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -5.620301997e-02f,  8.573754253e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  2.785781628e-02f, }},
+    {{ 1.000000000e-01f, -9.341723590e-02f, 0.0f, -6.787159473e-02f,  9.822469464e-02f, 0.0f, 0.0f, 0.0f, -3.191513794e-02f, -2.954767620e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  9.093839659e-02f, -5.298871540e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -7.293270986e-02f, }},
+    {{ 1.000000000e-01f, -1.154700538e-01f, 0.0f,  0.000000000e+00f,  0.000000000e+00f, 0.0f, 0.0f, 0.0f, -1.032795559e-01f,  9.561828875e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  0.000000000e+00f,  0.000000000e+00f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  9.014978717e-02f, }},
+    {{ 1.000000000e-01f, -9.341723590e-02f, 0.0f,  6.787159473e-02f, -9.822469464e-02f, 0.0f, 0.0f, 0.0f, -3.191513794e-02f, -2.954767620e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -9.093839659e-02f,  5.298871540e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -7.293270986e-02f, }},
+    {{ 1.000000000e-01f, -3.568220898e-02f, 0.0f,  1.098185471e-01f, -6.070619982e-02f, 0.0f, 0.0f, 0.0f,  8.355491589e-02f, -7.735682057e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  5.620301997e-02f, -8.573754253e-02f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,  2.785781628e-02f, }},
+}};
+constexpr std::array<AmbiChannelFloatArray,10> FourthOrder2DEncoder{{
+    CalcAmbiCoeffs( 3.090169944e-01f,  0.000000000e+00f,  9.510565163e-01f),
+    CalcAmbiCoeffs( 8.090169944e-01f,  0.000000000e+00f,  5.877852523e-01f),
+    CalcAmbiCoeffs( 1.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f),
+    CalcAmbiCoeffs( 8.090169944e-01f,  0.000000000e+00f, -5.877852523e-01f),
+    CalcAmbiCoeffs( 3.090169944e-01f,  0.000000000e+00f, -9.510565163e-01f),
+    CalcAmbiCoeffs(-3.090169944e-01f,  0.000000000e+00f, -9.510565163e-01f),
+    CalcAmbiCoeffs(-8.090169944e-01f,  0.000000000e+00f, -5.877852523e-01f),
+    CalcAmbiCoeffs(-1.000000000e+00f,  0.000000000e+00f,  0.000000000e+00f),
+    CalcAmbiCoeffs(-8.090169944e-01f,  0.000000000e+00f,  5.877852523e-01f),
+    CalcAmbiCoeffs(-3.090169944e-01f,  0.000000000e+00f,  9.510565163e-01f),
+}};
+static_assert(FourthOrder2DDecoder.size() == FourthOrder2DEncoder.size(), "Fourth-order 2D mismatch");
+
+
+template<size_t N, size_t M>
+auto CalcAmbiUpsampler(const std::array<std::array<float,N>,M> &decoder,
+    const std::array<AmbiChannelFloatArray,M> &encoder)
+{
+    std::array<AmbiChannelFloatArray,N> res{};
+
+    for(size_t i{0};i < decoder[0].size();++i)
+    {
+        for(size_t j{0};j < encoder[0].size();++j)
+        {
+            double sum{0.0};
+            for(size_t k{0};k < decoder.size();++k)
+                sum += double{decoder[k][i]} * encoder[k][j];
+            res[i][j] = static_cast<float>(sum);
+        }
+    }
+
+    return res;
+}
+
+} // namespace
+
+const std::array<AmbiChannelFloatArray,4> AmbiScale::FirstOrderUp{CalcAmbiUpsampler(FirstOrderDecoder, FirstOrderEncoder)};
+const std::array<AmbiChannelFloatArray,4> AmbiScale::FirstOrder2DUp{CalcAmbiUpsampler(FirstOrder2DDecoder, FirstOrder2DEncoder)};
+const std::array<AmbiChannelFloatArray,9> AmbiScale::SecondOrderUp{CalcAmbiUpsampler(SecondOrderDecoder, SecondOrderEncoder)};
+const std::array<AmbiChannelFloatArray,9> AmbiScale::SecondOrder2DUp{CalcAmbiUpsampler(SecondOrder2DDecoder, SecondOrder2DEncoder)};
+const std::array<AmbiChannelFloatArray,16> AmbiScale::ThirdOrderUp{CalcAmbiUpsampler(ThirdOrderDecoder, ThirdOrderEncoder)};
+const std::array<AmbiChannelFloatArray,16> AmbiScale::ThirdOrder2DUp{CalcAmbiUpsampler(ThirdOrder2DDecoder, ThirdOrder2DEncoder)};
+const std::array<AmbiChannelFloatArray,25> AmbiScale::FourthOrder2DUp{CalcAmbiUpsampler(FourthOrder2DDecoder, FourthOrder2DEncoder)};
+
+
+std::array<float,MaxAmbiOrder+1> AmbiScale::GetHFOrderScales(const uint src_order,
+    const uint dev_order, const bool horizontalOnly) noexcept
+{
+    std::array<float,MaxAmbiOrder+1> res{};
+
+    if(!horizontalOnly)
+    {
+        for(size_t i{0};i < MaxAmbiOrder+1;++i)
+            res[i] = HFScales[src_order][i] / HFScales[dev_order][i];
+    }
+    else
+    {
+        for(size_t i{0};i < MaxAmbiOrder+1;++i)
+            res[i] = HFScales2D[src_order][i] / HFScales2D[dev_order][i];
+    }
+
+    return res;
+}
diff --git a/core/ambidefs.h b/core/ambidefs.h
new file mode 100644 (file)
index 0000000..b7d2bcd
--- /dev/null
@@ -0,0 +1,250 @@
+#ifndef CORE_AMBIDEFS_H
+#define CORE_AMBIDEFS_H
+
+#include <array>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "alnumbers.h"
+
+
+using uint = unsigned int;
+
+/* The maximum number of Ambisonics channels. For a given order (o), the size
+ * needed will be (o+1)**2, thus zero-order has 1, first-order has 4, second-
+ * order has 9, third-order has 16, and fourth-order has 25.
+ */
+constexpr uint8_t MaxAmbiOrder{3};
+constexpr inline size_t AmbiChannelsFromOrder(size_t order) noexcept
+{ return (order+1) * (order+1); }
+constexpr size_t MaxAmbiChannels{AmbiChannelsFromOrder(MaxAmbiOrder)};
+
+/* A bitmask of ambisonic channels for 0 to 4th order. This only specifies up
+ * to 4th order, which is the highest order a 32-bit mask value can specify (a
+ * 64-bit mask could handle up to 7th order).
+ */
+constexpr uint Ambi0OrderMask{0x00000001};
+constexpr uint Ambi1OrderMask{0x0000000f};
+constexpr uint Ambi2OrderMask{0x000001ff};
+constexpr uint Ambi3OrderMask{0x0000ffff};
+constexpr uint Ambi4OrderMask{0x01ffffff};
+
+/* A bitmask of ambisonic channels with height information. If none of these
+ * channels are used/needed, there's no height (e.g. with most surround sound
+ * speaker setups). This is ACN ordering, with bit 0 being ACN 0, etc.
+ */
+constexpr uint AmbiPeriphonicMask{0xfe7ce4};
+
+/* The maximum number of ambisonic channels for 2D (non-periphonic)
+ * representation. This is 2 per each order above zero-order, plus 1 for zero-
+ * order. Or simply, o*2 + 1.
+ */
+constexpr inline size_t Ambi2DChannelsFromOrder(size_t order) noexcept
+{ return order*2 + 1; }
+constexpr size_t MaxAmbi2DChannels{Ambi2DChannelsFromOrder(MaxAmbiOrder)};
+
+
+/* NOTE: These are scale factors as applied to Ambisonics content. Decoder
+ * coefficients should be divided by these values to get proper scalings.
+ */
+struct AmbiScale {
+    static auto& FromN3D() noexcept
+    {
+        static constexpr const std::array<float,MaxAmbiChannels> ret{{
+            1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
+            1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f
+        }};
+        return ret;
+    }
+    static auto& FromSN3D() noexcept
+    {
+        static constexpr const std::array<float,MaxAmbiChannels> ret{{
+            1.000000000f, /* ACN  0, sqrt(1) */
+            1.732050808f, /* ACN  1, sqrt(3) */
+            1.732050808f, /* ACN  2, sqrt(3) */
+            1.732050808f, /* ACN  3, sqrt(3) */
+            2.236067978f, /* ACN  4, sqrt(5) */
+            2.236067978f, /* ACN  5, sqrt(5) */
+            2.236067978f, /* ACN  6, sqrt(5) */
+            2.236067978f, /* ACN  7, sqrt(5) */
+            2.236067978f, /* ACN  8, sqrt(5) */
+            2.645751311f, /* ACN  9, sqrt(7) */
+            2.645751311f, /* ACN 10, sqrt(7) */
+            2.645751311f, /* ACN 11, sqrt(7) */
+            2.645751311f, /* ACN 12, sqrt(7) */
+            2.645751311f, /* ACN 13, sqrt(7) */
+            2.645751311f, /* ACN 14, sqrt(7) */
+            2.645751311f, /* ACN 15, sqrt(7) */
+        }};
+        return ret;
+    }
+    static auto& FromFuMa() noexcept
+    {
+        static constexpr const std::array<float,MaxAmbiChannels> ret{{
+            1.414213562f, /* ACN  0 (W), sqrt(2) */
+            1.732050808f, /* ACN  1 (Y), sqrt(3) */
+            1.732050808f, /* ACN  2 (Z), sqrt(3) */
+            1.732050808f, /* ACN  3 (X), sqrt(3) */
+            1.936491673f, /* ACN  4 (V), sqrt(15)/2 */
+            1.936491673f, /* ACN  5 (T), sqrt(15)/2 */
+            2.236067978f, /* ACN  6 (R), sqrt(5) */
+            1.936491673f, /* ACN  7 (S), sqrt(15)/2 */
+            1.936491673f, /* ACN  8 (U), sqrt(15)/2 */
+            2.091650066f, /* ACN  9 (Q), sqrt(35/8) */
+            1.972026594f, /* ACN 10 (O), sqrt(35)/3 */
+            2.231093404f, /* ACN 11 (M), sqrt(224/45) */
+            2.645751311f, /* ACN 12 (K), sqrt(7) */
+            2.231093404f, /* ACN 13 (L), sqrt(224/45) */
+            1.972026594f, /* ACN 14 (N), sqrt(35)/3 */
+            2.091650066f, /* ACN 15 (P), sqrt(35/8) */
+        }};
+        return ret;
+    }
+    static auto& FromUHJ() noexcept
+    {
+        static constexpr const std::array<float,MaxAmbiChannels> ret{{
+            1.000000000f, /* ACN  0 (W), sqrt(1) */
+            1.224744871f, /* ACN  1 (Y), sqrt(3/2) */
+            1.224744871f, /* ACN  2 (Z), sqrt(3/2) */
+            1.224744871f, /* ACN  3 (X), sqrt(3/2) */
+            /* Higher orders not relevant for UHJ. */
+            1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
+        }};
+        return ret;
+    }
+
+    /* Retrieves per-order HF scaling factors for "upsampling" ambisonic data. */
+    static std::array<float,MaxAmbiOrder+1> GetHFOrderScales(const uint src_order,
+        const uint dev_order, const bool horizontalOnly) noexcept;
+
+    static const std::array<std::array<float,MaxAmbiChannels>,4> FirstOrderUp;
+    static const std::array<std::array<float,MaxAmbiChannels>,4> FirstOrder2DUp;
+    static const std::array<std::array<float,MaxAmbiChannels>,9> SecondOrderUp;
+    static const std::array<std::array<float,MaxAmbiChannels>,9> SecondOrder2DUp;
+    static const std::array<std::array<float,MaxAmbiChannels>,16> ThirdOrderUp;
+    static const std::array<std::array<float,MaxAmbiChannels>,16> ThirdOrder2DUp;
+    static const std::array<std::array<float,MaxAmbiChannels>,25> FourthOrder2DUp;
+};
+
+struct AmbiIndex {
+    static auto& FromFuMa() noexcept
+    {
+        static constexpr const std::array<uint8_t,MaxAmbiChannels> ret{{
+            0,  /* W */
+            3,  /* X */
+            1,  /* Y */
+            2,  /* Z */
+            6,  /* R */
+            7,  /* S */
+            5,  /* T */
+            8,  /* U */
+            4,  /* V */
+            12, /* K */
+            13, /* L */
+            11, /* M */
+            14, /* N */
+            10, /* O */
+            15, /* P */
+            9,  /* Q */
+        }};
+        return ret;
+    }
+    static auto& FromFuMa2D() noexcept
+    {
+        static constexpr const std::array<uint8_t,MaxAmbi2DChannels> ret{{
+            0,  /* W */
+            3,  /* X */
+            1,  /* Y */
+            8,  /* U */
+            4,  /* V */
+            15, /* P */
+            9,  /* Q */
+        }};
+        return ret;
+    }
+
+    static auto& FromACN() noexcept
+    {
+        static constexpr const std::array<uint8_t,MaxAmbiChannels> ret{{
+            0,  1,  2,  3,  4,  5,  6,  7,
+            8,  9, 10, 11, 12, 13, 14, 15
+        }};
+        return ret;
+    }
+    static auto& FromACN2D() noexcept
+    {
+        static constexpr const std::array<uint8_t,MaxAmbi2DChannels> ret{{
+            0, 1,3, 4,8, 9,15
+        }};
+        return ret;
+    }
+
+    static auto& OrderFromChannel() noexcept
+    {
+        static constexpr const std::array<uint8_t,MaxAmbiChannels> ret{{
+            0, 1,1,1, 2,2,2,2,2, 3,3,3,3,3,3,3,
+        }};
+        return ret;
+    }
+    static auto& OrderFrom2DChannel() noexcept
+    {
+        static constexpr const std::array<uint8_t,MaxAmbi2DChannels> ret{{
+            0, 1,1, 2,2, 3,3,
+        }};
+        return ret;
+    }
+};
+
+
+/**
+ * Calculates ambisonic encoder coefficients using the X, Y, and Z direction
+ * components, which must represent a normalized (unit length) vector.
+ *
+ * NOTE: The components use ambisonic coordinates. As a result:
+ *
+ * Ambisonic Y = OpenAL -X
+ * Ambisonic Z = OpenAL Y
+ * Ambisonic X = OpenAL -Z
+ *
+ * The components are ordered such that OpenAL's X, Y, and Z are the first,
+ * second, and third parameters respectively -- simply negate X and Z.
+ */
+constexpr auto CalcAmbiCoeffs(const float y, const float z, const float x)
+{
+    const float xx{x*x}, yy{y*y}, zz{z*z}, xy{x*y}, yz{y*z}, xz{x*z};
+
+    return std::array<float,MaxAmbiChannels>{{
+        /* Zeroth-order */
+        1.0f, /* ACN 0 = 1 */
+        /* First-order */
+        al::numbers::sqrt3_v<float> * y, /* ACN 1 = sqrt(3) * Y */
+        al::numbers::sqrt3_v<float> * z, /* ACN 2 = sqrt(3) * Z */
+        al::numbers::sqrt3_v<float> * x, /* ACN 3 = sqrt(3) * X */
+        /* Second-order */
+        3.872983346e+00f * xy,               /* ACN 4 = sqrt(15) * X * Y */
+        3.872983346e+00f * yz,               /* ACN 5 = sqrt(15) * Y * Z */
+        1.118033989e+00f * (3.0f*zz - 1.0f), /* ACN 6 = sqrt(5)/2 * (3*Z*Z - 1) */
+        3.872983346e+00f * xz,               /* ACN 7 = sqrt(15) * X * Z */
+        1.936491673e+00f * (xx - yy),        /* ACN 8 = sqrt(15)/2 * (X*X - Y*Y) */
+        /* Third-order */
+        2.091650066e+00f * (y*(3.0f*xx - yy)),   /* ACN  9 = sqrt(35/8) * Y * (3*X*X - Y*Y) */
+        1.024695076e+01f * (z*xy),               /* ACN 10 = sqrt(105) * Z * X * Y */
+        1.620185175e+00f * (y*(5.0f*zz - 1.0f)), /* ACN 11 = sqrt(21/8) * Y * (5*Z*Z - 1) */
+        1.322875656e+00f * (z*(5.0f*zz - 3.0f)), /* ACN 12 = sqrt(7)/2 * Z * (5*Z*Z - 3) */
+        1.620185175e+00f * (x*(5.0f*zz - 1.0f)), /* ACN 13 = sqrt(21/8) * X * (5*Z*Z - 1) */
+        5.123475383e+00f * (z*(xx - yy)),        /* ACN 14 = sqrt(105)/2 * Z * (X*X - Y*Y) */
+        2.091650066e+00f * (x*(xx - 3.0f*yy)),   /* ACN 15 = sqrt(35/8) * X * (X*X - 3*Y*Y) */
+        /* Fourth-order */
+        /* ACN 16 = sqrt(35)*3/2 * X * Y * (X*X - Y*Y) */
+        /* ACN 17 = sqrt(35/2)*3/2 * (3*X*X - Y*Y) * Y * Z */
+        /* ACN 18 = sqrt(5)*3/2 * X * Y * (7*Z*Z - 1) */
+        /* ACN 19 = sqrt(5/2)*3/2 * Y * Z * (7*Z*Z - 3) */
+        /* ACN 20 = 3/8 * (35*Z*Z*Z*Z - 30*Z*Z + 3) */
+        /* ACN 21 = sqrt(5/2)*3/2 * X * Z * (7*Z*Z - 3) */
+        /* ACN 22 = sqrt(5)*3/4 * (X*X - Y*Y) * (7*Z*Z - 1) */
+        /* ACN 23 = sqrt(35/2)*3/2 * (X*X - 3*Y*Y) * X * Z */
+        /* ACN 24 = sqrt(35)*3/8 * (X*X*X*X - 6*X*X*Y*Y + Y*Y*Y*Y) */
+    }};
+}
+
+#endif /* CORE_AMBIDEFS_H */
diff --git a/core/async_event.h b/core/async_event.h
new file mode 100644 (file)
index 0000000..5a2f5f9
--- /dev/null
@@ -0,0 +1,55 @@
+#ifndef CORE_EVENT_H
+#define CORE_EVENT_H
+
+#include "almalloc.h"
+
+struct EffectState;
+
+using uint = unsigned int;
+
+
+struct AsyncEvent {
+    enum : uint {
+        /* User event types. */
+        SourceStateChange,
+        BufferCompleted,
+        Disconnected,
+        UserEventCount,
+
+        /* Internal events, always processed. */
+        ReleaseEffectState = 128,
+
+        /* End event thread processing. */
+        KillThread,
+    };
+
+    enum class SrcState {
+        Reset,
+        Stop,
+        Play,
+        Pause
+    };
+
+    const uint EnumType;
+    union {
+        char dummy;
+        struct {
+            uint id;
+            SrcState state;
+        } srcstate;
+        struct {
+            uint id;
+            uint count;
+        } bufcomp;
+        struct {
+            char msg[244];
+        } disconnect;
+        EffectState *mEffectState;
+    } u{};
+
+    constexpr AsyncEvent(uint type) noexcept : EnumType{type} { }
+
+    DISABLE_ALLOC()
+};
+
+#endif
diff --git a/core/bformatdec.cpp b/core/bformatdec.cpp
new file mode 100644 (file)
index 0000000..129b997
--- /dev/null
@@ -0,0 +1,170 @@
+
+#include "config.h"
+
+#include "bformatdec.h"
+
+#include <algorithm>
+#include <array>
+#include <cmath>
+#include <utility>
+
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "filters/splitter.h"
+#include "front_stablizer.h"
+#include "mixer.h"
+#include "opthelpers.h"
+
+
+BFormatDec::BFormatDec(const size_t inchans, const al::span<const ChannelDec> coeffs,
+    const al::span<const ChannelDec> coeffslf, const float xover_f0norm,
+    std::unique_ptr<FrontStablizer> stablizer)
+    : mStablizer{std::move(stablizer)}, mDualBand{!coeffslf.empty()}, mChannelDec{inchans}
+{
+    if(!mDualBand)
+    {
+        for(size_t j{0};j < mChannelDec.size();++j)
+        {
+            float *outcoeffs{mChannelDec[j].mGains.Single};
+            for(const ChannelDec &incoeffs : coeffs)
+                *(outcoeffs++) = incoeffs[j];
+        }
+    }
+    else
+    {
+        mChannelDec[0].mXOver.init(xover_f0norm);
+        for(size_t j{1};j < mChannelDec.size();++j)
+            mChannelDec[j].mXOver = mChannelDec[0].mXOver;
+
+        for(size_t j{0};j < mChannelDec.size();++j)
+        {
+            float *outcoeffs{mChannelDec[j].mGains.Dual[sHFBand]};
+            for(const ChannelDec &incoeffs : coeffs)
+                *(outcoeffs++) = incoeffs[j];
+
+            outcoeffs = mChannelDec[j].mGains.Dual[sLFBand];
+            for(const ChannelDec &incoeffs : coeffslf)
+                *(outcoeffs++) = incoeffs[j];
+        }
+    }
+}
+
+
+void BFormatDec::process(const al::span<FloatBufferLine> OutBuffer,
+    const FloatBufferLine *InSamples, const size_t SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    if(mDualBand)
+    {
+        const al::span<float> hfSamples{mSamples[sHFBand].data(), SamplesToDo};
+        const al::span<float> lfSamples{mSamples[sLFBand].data(), SamplesToDo};
+        for(auto &chandec : mChannelDec)
+        {
+            chandec.mXOver.process({InSamples->data(), SamplesToDo}, hfSamples.data(),
+                lfSamples.data());
+            MixSamples(hfSamples, OutBuffer, chandec.mGains.Dual[sHFBand],
+                chandec.mGains.Dual[sHFBand], 0, 0);
+            MixSamples(lfSamples, OutBuffer, chandec.mGains.Dual[sLFBand],
+                chandec.mGains.Dual[sLFBand], 0, 0);
+            ++InSamples;
+        }
+    }
+    else
+    {
+        for(auto &chandec : mChannelDec)
+        {
+            MixSamples({InSamples->data(), SamplesToDo}, OutBuffer, chandec.mGains.Single,
+                chandec.mGains.Single, 0, 0);
+            ++InSamples;
+        }
+    }
+}
+
+void BFormatDec::processStablize(const al::span<FloatBufferLine> OutBuffer,
+    const FloatBufferLine *InSamples, const size_t lidx, const size_t ridx, const size_t cidx,
+    const size_t SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    /* Move the existing direct L/R signal out so it doesn't get processed by
+     * the stablizer.
+     */
+    float *RESTRICT mid{al::assume_aligned<16>(mStablizer->MidDirect.data())};
+    float *RESTRICT side{al::assume_aligned<16>(mStablizer->Side.data())};
+    for(size_t i{0};i < SamplesToDo;++i)
+    {
+        mid[i] = OutBuffer[lidx][i] + OutBuffer[ridx][i];
+        side[i] = OutBuffer[lidx][i] - OutBuffer[ridx][i];
+    }
+    std::fill_n(OutBuffer[lidx].begin(), SamplesToDo, 0.0f);
+    std::fill_n(OutBuffer[ridx].begin(), SamplesToDo, 0.0f);
+
+    /* Decode the B-Format input to OutBuffer. */
+    process(OutBuffer, InSamples, SamplesToDo);
+
+    /* Include the decoded side signal with the direct side signal. */
+    for(size_t i{0};i < SamplesToDo;++i)
+        side[i] += OutBuffer[lidx][i] - OutBuffer[ridx][i];
+
+    /* Get the decoded mid signal and band-split it. */
+    std::transform(OutBuffer[lidx].cbegin(), OutBuffer[lidx].cbegin()+SamplesToDo,
+        OutBuffer[ridx].cbegin(), mStablizer->Temp.begin(),
+        [](const float l, const float r) noexcept { return l + r; });
+
+    mStablizer->MidFilter.process({mStablizer->Temp.data(), SamplesToDo}, mStablizer->MidHF.data(),
+        mStablizer->MidLF.data());
+
+    /* Apply an all-pass to all channels to match the band-splitter's phase
+     * shift. This is to keep the phase synchronized between the existing
+     * signal and the split mid signal.
+     */
+    const size_t NumChannels{OutBuffer.size()};
+    for(size_t i{0u};i < NumChannels;i++)
+    {
+        /* Skip the left and right channels, which are going to get overwritten,
+         * and substitute the direct mid signal and direct+decoded side signal.
+         */
+        if(i == lidx)
+            mStablizer->ChannelFilters[i].processAllPass({mid, SamplesToDo});
+        else if(i == ridx)
+            mStablizer->ChannelFilters[i].processAllPass({side, SamplesToDo});
+        else
+            mStablizer->ChannelFilters[i].processAllPass({OutBuffer[i].data(), SamplesToDo});
+    }
+
+    /* This pans the separate low- and high-frequency signals between being on
+     * the center channel and the left+right channels. The low-frequency signal
+     * is panned 1/3rd toward center and the high-frequency signal is panned
+     * 1/4th toward center. These values can be tweaked.
+     */
+    const float cos_lf{std::cos(1.0f/3.0f * (al::numbers::pi_v<float>*0.5f))};
+    const float cos_hf{std::cos(1.0f/4.0f * (al::numbers::pi_v<float>*0.5f))};
+    const float sin_lf{std::sin(1.0f/3.0f * (al::numbers::pi_v<float>*0.5f))};
+    const float sin_hf{std::sin(1.0f/4.0f * (al::numbers::pi_v<float>*0.5f))};
+    for(size_t i{0};i < SamplesToDo;i++)
+    {
+        /* Add the direct mid signal to the processed mid signal so it can be
+         * properly combined with the direct+decoded side signal.
+         */
+        const float m{mStablizer->MidLF[i]*cos_lf + mStablizer->MidHF[i]*cos_hf + mid[i]};
+        const float c{mStablizer->MidLF[i]*sin_lf + mStablizer->MidHF[i]*sin_hf};
+        const float s{side[i]};
+
+        /* The generated center channel signal adds to the existing signal,
+         * while the modified left and right channels replace.
+         */
+        OutBuffer[lidx][i] = (m + s) * 0.5f;
+        OutBuffer[ridx][i] = (m - s) * 0.5f;
+        OutBuffer[cidx][i] += c * 0.5f;
+    }
+}
+
+
+std::unique_ptr<BFormatDec> BFormatDec::Create(const size_t inchans,
+    const al::span<const ChannelDec> coeffs, const al::span<const ChannelDec> coeffslf,
+    const float xover_f0norm, std::unique_ptr<FrontStablizer> stablizer)
+{
+    return std::make_unique<BFormatDec>(inchans, coeffs, coeffslf, xover_f0norm,
+        std::move(stablizer));
+}
diff --git a/core/bformatdec.h b/core/bformatdec.h
new file mode 100644 (file)
index 0000000..7a27a5a
--- /dev/null
@@ -0,0 +1,71 @@
+#ifndef CORE_BFORMATDEC_H
+#define CORE_BFORMATDEC_H
+
+#include <array>
+#include <cstddef>
+#include <memory>
+
+#include "almalloc.h"
+#include "alspan.h"
+#include "ambidefs.h"
+#include "bufferline.h"
+#include "devformat.h"
+#include "filters/splitter.h"
+#include "vector.h"
+
+struct FrontStablizer;
+
+
+using ChannelDec = std::array<float,MaxAmbiChannels>;
+
+class BFormatDec {
+    static constexpr size_t sHFBand{0};
+    static constexpr size_t sLFBand{1};
+    static constexpr size_t sNumBands{2};
+
+    struct ChannelDecoder {
+        union MatrixU {
+            float Dual[sNumBands][MAX_OUTPUT_CHANNELS];
+            float Single[MAX_OUTPUT_CHANNELS];
+        } mGains{};
+
+        /* NOTE: BandSplitter filter is unused with single-band decoding. */
+        BandSplitter mXOver;
+    };
+
+    alignas(16) std::array<FloatBufferLine,2> mSamples;
+
+    const std::unique_ptr<FrontStablizer> mStablizer;
+    const bool mDualBand{false};
+
+    /* TODO: This should ideally be a FlexArray, since ChannelDecoder is rather
+     * small and only a few are needed (3, 4, 5, 7, typically). But that can
+     * only be used in a standard layout struct, and a std::unique_ptr member
+     * (mStablizer) causes GCC and Clang to warn it's not.
+     */
+    al::vector<ChannelDecoder> mChannelDec;
+
+public:
+    BFormatDec(const size_t inchans, const al::span<const ChannelDec> coeffs,
+        const al::span<const ChannelDec> coeffslf, const float xover_f0norm,
+        std::unique_ptr<FrontStablizer> stablizer);
+
+    bool hasStablizer() const noexcept { return mStablizer != nullptr; }
+
+    /* Decodes the ambisonic input to the given output channels. */
+    void process(const al::span<FloatBufferLine> OutBuffer, const FloatBufferLine *InSamples,
+        const size_t SamplesToDo);
+
+    /* Decodes the ambisonic input to the given output channels with stablization. */
+    void processStablize(const al::span<FloatBufferLine> OutBuffer,
+        const FloatBufferLine *InSamples, const size_t lidx, const size_t ridx, const size_t cidx,
+        const size_t SamplesToDo);
+
+    static std::unique_ptr<BFormatDec> Create(const size_t inchans,
+        const al::span<const ChannelDec> coeffs, const al::span<const ChannelDec> coeffslf,
+        const float xover_f0norm, std::unique_ptr<FrontStablizer> stablizer);
+
+    DEF_NEWDEL(BFormatDec)
+};
+
+#endif /* CORE_BFORMATDEC_H */
diff --git a/core/bs2b.cpp b/core/bs2b.cpp
new file mode 100644 (file)
index 0000000..303bf9b
--- /dev/null
@@ -0,0 +1,183 @@
+/*-
+ * Copyright (c) 2005 Boris Mikhaylov
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iterator>
+
+#include "alnumbers.h"
+#include "bs2b.h"
+
+
+/* Set up all data. */
+static void init(struct bs2b *bs2b)
+{
+    float Fc_lo, Fc_hi;
+    float G_lo, G_hi;
+    float x, g;
+
+    switch(bs2b->level)
+    {
+    case BS2B_LOW_CLEVEL: /* Low crossfeed level */
+        Fc_lo = 360.0f;
+        Fc_hi = 501.0f;
+        G_lo  = 0.398107170553497f;
+        G_hi  = 0.205671765275719f;
+        break;
+
+    case BS2B_MIDDLE_CLEVEL: /* Middle crossfeed level */
+        Fc_lo = 500.0f;
+        Fc_hi = 711.0f;
+        G_lo  = 0.459726988530872f;
+        G_hi  = 0.228208484414988f;
+        break;
+
+    case BS2B_HIGH_CLEVEL: /* High crossfeed level (virtual speakers are closer to itself) */
+        Fc_lo = 700.0f;
+        Fc_hi = 1021.0f;
+        G_lo  = 0.530884444230988f;
+        G_hi  = 0.250105790667544f;
+        break;
+
+    case BS2B_LOW_ECLEVEL: /* Low easy crossfeed level */
+        Fc_lo = 360.0f;
+        Fc_hi = 494.0f;
+        G_lo  = 0.316227766016838f;
+        G_hi  = 0.168236228897329f;
+        break;
+
+    case BS2B_MIDDLE_ECLEVEL: /* Middle easy crossfeed level */
+        Fc_lo = 500.0f;
+        Fc_hi = 689.0f;
+        G_lo  = 0.354813389233575f;
+        G_hi  = 0.187169483835901f;
+        break;
+
+    default: /* High easy crossfeed level */
+        bs2b->level = BS2B_HIGH_ECLEVEL;
+
+        Fc_lo = 700.0f;
+        Fc_hi = 975.0f;
+        G_lo  = 0.398107170553497f;
+        G_hi  = 0.205671765275719f;
+        break;
+    } /* switch */
+
+    g = 1.0f / (1.0f - G_hi + G_lo);
+
+    /* $fc = $Fc / $s;
+     * $d  = 1 / 2 / pi / $fc;
+     * $x  = exp(-1 / $d);
+     */
+    x           = std::exp(-al::numbers::pi_v<float>*2.0f*Fc_lo/static_cast<float>(bs2b->srate));
+    bs2b->b1_lo = x;
+    bs2b->a0_lo = G_lo * (1.0f - x) * g;
+
+    x           = std::exp(-al::numbers::pi_v<float>*2.0f*Fc_hi/static_cast<float>(bs2b->srate));
+    bs2b->b1_hi = x;
+    bs2b->a0_hi = (1.0f - G_hi * (1.0f - x)) * g;
+    bs2b->a1_hi = -x * g;
+} /* init */
+
+
+/* Exported functions.
+ * See descriptions in "bs2b.h"
+ */
+
+void bs2b_set_params(struct bs2b *bs2b, int level, int srate)
+{
+    if(srate <= 0) srate = 1;
+
+    bs2b->level = level;
+    bs2b->srate = srate;
+    init(bs2b);
+} /* bs2b_set_params */
+
+int bs2b_get_level(struct bs2b *bs2b)
+{
+    return bs2b->level;
+} /* bs2b_get_level */
+
+int bs2b_get_srate(struct bs2b *bs2b)
+{
+    return bs2b->srate;
+} /* bs2b_get_srate */
+
+void bs2b_clear(struct bs2b *bs2b)
+{
+    std::fill(std::begin(bs2b->history), std::end(bs2b->history), bs2b::t_last_sample{});
+} /* bs2b_clear */
+
+void bs2b_cross_feed(struct bs2b *bs2b, float *Left, float *Right, size_t SamplesToDo)
+{
+    const float a0_lo{bs2b->a0_lo};
+    const float b1_lo{bs2b->b1_lo};
+    const float a0_hi{bs2b->a0_hi};
+    const float a1_hi{bs2b->a1_hi};
+    const float b1_hi{bs2b->b1_hi};
+    float lsamples[128][2];
+    float rsamples[128][2];
+
+    for(size_t base{0};base < SamplesToDo;)
+    {
+        const size_t todo{std::min<size_t>(128, SamplesToDo-base)};
+
+        /* Process left input */
+        float z_lo{bs2b->history[0].lo};
+        float z_hi{bs2b->history[0].hi};
+        for(size_t i{0};i < todo;i++)
+        {
+            lsamples[i][0] = a0_lo*Left[i] + z_lo;
+            z_lo = b1_lo*lsamples[i][0];
+
+            lsamples[i][1] = a0_hi*Left[i] + z_hi;
+            z_hi = a1_hi*Left[i] + b1_hi*lsamples[i][1];
+        }
+        bs2b->history[0].lo = z_lo;
+        bs2b->history[0].hi = z_hi;
+
+        /* Process right input */
+        z_lo = bs2b->history[1].lo;
+        z_hi = bs2b->history[1].hi;
+        for(size_t i{0};i < todo;i++)
+        {
+            rsamples[i][0] = a0_lo*Right[i] + z_lo;
+            z_lo = b1_lo*rsamples[i][0];
+
+            rsamples[i][1] = a0_hi*Right[i] + z_hi;
+            z_hi = a1_hi*Right[i] + b1_hi*rsamples[i][1];
+        }
+        bs2b->history[1].lo = z_lo;
+        bs2b->history[1].hi = z_hi;
+
+        /* Crossfeed */
+        for(size_t i{0};i < todo;i++)
+            *(Left++) = lsamples[i][1] + rsamples[i][0];
+        for(size_t i{0};i < todo;i++)
+            *(Right++) = rsamples[i][1] + lsamples[i][0];
+
+        base += todo;
+    }
+} /* bs2b_cross_feed */
diff --git a/core/bs2b.h b/core/bs2b.h
new file mode 100644 (file)
index 0000000..4d0b9dd
--- /dev/null
@@ -0,0 +1,89 @@
+/*-
+ * Copyright (c) 2005 Boris Mikhaylov
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef CORE_BS2B_H
+#define CORE_BS2B_H
+
+#include "almalloc.h"
+
+/* Number of crossfeed levels */
+#define BS2B_CLEVELS           3
+
+/* Normal crossfeed levels */
+#define BS2B_HIGH_CLEVEL       3
+#define BS2B_MIDDLE_CLEVEL     2
+#define BS2B_LOW_CLEVEL        1
+
+/* Easy crossfeed levels */
+#define BS2B_HIGH_ECLEVEL      BS2B_HIGH_CLEVEL    + BS2B_CLEVELS
+#define BS2B_MIDDLE_ECLEVEL    BS2B_MIDDLE_CLEVEL  + BS2B_CLEVELS
+#define BS2B_LOW_ECLEVEL       BS2B_LOW_CLEVEL     + BS2B_CLEVELS
+
+/* Default crossfeed levels */
+#define BS2B_DEFAULT_CLEVEL    BS2B_HIGH_ECLEVEL
+/* Default sample rate (Hz) */
+#define BS2B_DEFAULT_SRATE     44100
+
+struct bs2b {
+    int level;  /* Crossfeed level */
+    int srate;   /* Sample rate (Hz) */
+
+    /* Lowpass IIR filter coefficients */
+    float a0_lo;
+    float b1_lo;
+
+    /* Highboost IIR filter coefficients */
+    float a0_hi;
+    float a1_hi;
+    float b1_hi;
+
+    /* Buffer of filter history
+     * [0] - first channel, [1] - second channel
+     */
+    struct t_last_sample {
+        float lo;
+        float hi;
+    } history[2];
+
+    DEF_NEWDEL(bs2b)
+};
+
+/* Clear buffers and set new coefficients with new crossfeed level and sample
+ * rate values.
+ * level - crossfeed level of *LEVEL values.
+ * srate - sample rate by Hz.
+ */
+void bs2b_set_params(bs2b *bs2b, int level, int srate);
+
+/* Return current crossfeed level value */
+int bs2b_get_level(bs2b *bs2b);
+
+/* Return current sample rate value */
+int bs2b_get_srate(bs2b *bs2b);
+
+/* Clear buffer */
+void bs2b_clear(bs2b *bs2b);
+
+void bs2b_cross_feed(bs2b *bs2b, float *Left, float *Right, size_t SamplesToDo);
+
+#endif /* CORE_BS2B_H */
diff --git a/core/bsinc_defs.h b/core/bsinc_defs.h
new file mode 100644 (file)
index 0000000..01bd3c2
--- /dev/null
@@ -0,0 +1,12 @@
+#ifndef CORE_BSINC_DEFS_H
+#define CORE_BSINC_DEFS_H
+
+/* The number of distinct scale and phase intervals within the bsinc filter
+ * tables.
+ */
+constexpr unsigned int BSincScaleBits{4};
+constexpr unsigned int BSincScaleCount{1 << BSincScaleBits};
+constexpr unsigned int BSincPhaseBits{5};
+constexpr unsigned int BSincPhaseCount{1 << BSincPhaseBits};
+
+#endif /* CORE_BSINC_DEFS_H */
diff --git a/core/bsinc_tables.cpp b/core/bsinc_tables.cpp
new file mode 100644 (file)
index 0000000..693645f
--- /dev/null
@@ -0,0 +1,295 @@
+
+#include "bsinc_tables.h"
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <cmath>
+#include <limits>
+#include <memory>
+#include <stdexcept>
+
+#include "alnumbers.h"
+#include "core/mixer/defs.h"
+
+
+namespace {
+
+using uint = unsigned int;
+
+
+/* This is the normalized cardinal sine (sinc) function.
+ *
+ *   sinc(x) = { 1,                   x = 0
+ *             { sin(pi x) / (pi x),  otherwise.
+ */
+constexpr double Sinc(const double x)
+{
+    constexpr double epsilon{std::numeric_limits<double>::epsilon()};
+    if(!(x > epsilon || x < -epsilon))
+        return 1.0;
+    return std::sin(al::numbers::pi*x) / (al::numbers::pi*x);
+}
+
+/* The zero-order modified Bessel function of the first kind, used for the
+ * Kaiser window.
+ *
+ *   I_0(x) = sum_{k=0}^inf (1 / k!)^2 (x / 2)^(2 k)
+ *          = sum_{k=0}^inf ((x / 2)^k / k!)^2
+ */
+constexpr double BesselI_0(const double x) noexcept
+{
+    /* Start at k=1 since k=0 is trivial. */
+    const double x2{x / 2.0};
+    double term{1.0};
+    double sum{1.0};
+    double last_sum{};
+    int k{1};
+
+    /* Let the integration converge until the term of the sum is no longer
+     * significant.
+     */
+    do {
+        const double y{x2 / k};
+        ++k;
+        last_sum = sum;
+        term *= y * y;
+        sum += term;
+    } while(sum != last_sum);
+
+    return sum;
+}
+
+/* Calculate a Kaiser window from the given beta value and a normalized k
+ * [-1, 1].
+ *
+ *   w(k) = { I_0(B sqrt(1 - k^2)) / I_0(B),  -1 <= k <= 1
+ *          { 0,                              elsewhere.
+ *
+ * Where k can be calculated as:
+ *
+ *   k = i / l,         where -l <= i <= l.
+ *
+ * or:
+ *
+ *   k = 2 i / M - 1,   where 0 <= i <= M.
+ */
+constexpr double Kaiser(const double beta, const double k, const double besseli_0_beta)
+{
+    if(!(k >= -1.0 && k <= 1.0))
+        return 0.0;
+    return BesselI_0(beta * std::sqrt(1.0 - k*k)) / besseli_0_beta;
+}
+
+/* Calculates the (normalized frequency) transition width of the Kaiser window.
+ * Rejection is in dB.
+ */
+constexpr double CalcKaiserWidth(const double rejection, const uint order) noexcept
+{
+    if(rejection > 21.19)
+        return (rejection - 7.95) / (2.285 * al::numbers::pi*2.0 * order);
+    /* This enforces a minimum rejection of just above 21.18dB */
+    return 5.79 / (al::numbers::pi*2.0 * order);
+}
+
+/* Calculates the beta value of the Kaiser window. Rejection is in dB. */
+constexpr double CalcKaiserBeta(const double rejection)
+{
+    if(rejection > 50.0)
+        return 0.1102 * (rejection-8.7);
+    else if(rejection >= 21.0)
+        return (0.5842 * std::pow(rejection-21.0, 0.4)) + (0.07886 * (rejection-21.0));
+    return 0.0;
+}
+
+
+struct BSincHeader {
+    double width{};
+    double beta{};
+    double scaleBase{};
+    double scaleRange{};
+    double besseli_0_beta{};
+
+    uint a[BSincScaleCount]{};
+    uint total_size{};
+
+    constexpr BSincHeader(uint Rejection, uint Order) noexcept
+    {
+        width = CalcKaiserWidth(Rejection, Order);
+        beta = CalcKaiserBeta(Rejection);
+        scaleBase = width / 2.0;
+        scaleRange = 1.0 - scaleBase;
+        besseli_0_beta = BesselI_0(beta);
+
+        uint num_points{Order+1};
+        for(uint si{0};si < BSincScaleCount;++si)
+        {
+            const double scale{scaleBase + (scaleRange * (si+1) / BSincScaleCount)};
+            const uint a_{std::min(static_cast<uint>(num_points / 2.0 / scale), num_points)};
+            const uint m{2 * a_};
+
+            a[si] = a_;
+            total_size += 4 * BSincPhaseCount * ((m+3) & ~3u);
+        }
+    }
+};
+
+/* 11th and 23rd order filters (12 and 24-point respectively) with a 60dB drop
+ * at nyquist. Each filter will scale up the order when downsampling, to 23rd
+ * and 47th order respectively.
+ */
+constexpr BSincHeader bsinc12_hdr{60, 11};
+constexpr BSincHeader bsinc24_hdr{60, 23};
+
+
+/* NOTE: GCC 5 has an issue with BSincHeader objects being in an anonymous
+ * namespace while also being used as non-type template parameters.
+ */
+#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 6
+
+/* The number of sample points is double the a value (rounded up to a multiple
+ * of 4), and scale index 0 includes the doubling for downsampling. bsinc24 is
+ * currently the highest quality filter, and will use the most sample points.
+ */
+constexpr uint BSincPointsMax{(bsinc24_hdr.a[0]*2 + 3) & ~3u};
+static_assert(BSincPointsMax <= MaxResamplerPadding, "MaxResamplerPadding is too small");
+
+template<size_t total_size>
+struct BSincFilterArray {
+    alignas(16) std::array<float, total_size> mTable;
+    const BSincHeader &hdr;
+
+    BSincFilterArray(const BSincHeader &hdr_) : hdr{hdr_}
+    {
+#else
+template<const BSincHeader &hdr>
+struct BSincFilterArray {
+    alignas(16) std::array<float, hdr.total_size> mTable{};
+
+    BSincFilterArray()
+    {
+        constexpr uint BSincPointsMax{(hdr.a[0]*2 + 3) & ~3u};
+        static_assert(BSincPointsMax <= MaxResamplerPadding, "MaxResamplerPadding is too small");
+#endif
+        using filter_type = double[BSincPhaseCount+1][BSincPointsMax];
+        auto filter = std::make_unique<filter_type[]>(BSincScaleCount);
+
+        /* Calculate the Kaiser-windowed Sinc filter coefficients for each
+         * scale and phase index.
+         */
+        for(uint si{0};si < BSincScaleCount;++si)
+        {
+            const uint m{hdr.a[si] * 2};
+            const size_t o{(BSincPointsMax-m) / 2};
+            const double scale{hdr.scaleBase + (hdr.scaleRange * (si+1) / BSincScaleCount)};
+            const double cutoff{scale - (hdr.scaleBase * std::max(1.0, scale*2.0))};
+            const auto a = static_cast<double>(hdr.a[si]);
+            const double l{a - 1.0/BSincPhaseCount};
+
+            /* Do one extra phase index so that the phase delta has a proper
+             * target for its last index.
+             */
+            for(uint pi{0};pi <= BSincPhaseCount;++pi)
+            {
+                const double phase{std::floor(l) + (pi/double{BSincPhaseCount})};
+
+                for(uint i{0};i < m;++i)
+                {
+                    const double x{i - phase};
+                    filter[si][pi][o+i] = Kaiser(hdr.beta, x/l, hdr.besseli_0_beta) * cutoff *
+                        Sinc(cutoff*x);
+                }
+            }
+        }
+
+        size_t idx{0};
+        for(size_t si{0};si < BSincScaleCount;++si)
+        {
+            const size_t m{((hdr.a[si]*2) + 3) & ~3u};
+            const size_t o{(BSincPointsMax-m) / 2};
+
+            /* Write out each phase index's filter and phase delta for this
+             * quality scale.
+             */
+            for(size_t pi{0};pi < BSincPhaseCount;++pi)
+            {
+                for(size_t i{0};i < m;++i)
+                    mTable[idx++] = static_cast<float>(filter[si][pi][o+i]);
+
+                /* Linear interpolation between phases is simplified by pre-
+                 * calculating the delta (b - a) in: x = a + f (b - a)
+                 */
+                for(size_t i{0};i < m;++i)
+                {
+                    const double phDelta{filter[si][pi+1][o+i] - filter[si][pi][o+i]};
+                    mTable[idx++] = static_cast<float>(phDelta);
+                }
+            }
+            /* Calculate and write out each phase index's filter quality scale
+             * deltas. The last scale index doesn't have any scale or scale-
+             * phase deltas.
+             */
+            if(si == BSincScaleCount-1)
+            {
+                for(size_t i{0};i < BSincPhaseCount*m*2;++i)
+                    mTable[idx++] = 0.0f;
+            }
+            else for(size_t pi{0};pi < BSincPhaseCount;++pi)
+            {
+                /* Linear interpolation between scales is also simplified.
+                 *
+                 * Given a difference in the number of points between scales,
+                 * the destination points will be 0, thus: x = a + f (-a)
+                 */
+                for(size_t i{0};i < m;++i)
+                {
+                    const double scDelta{filter[si+1][pi][o+i] - filter[si][pi][o+i]};
+                    mTable[idx++] = static_cast<float>(scDelta);
+                }
+
+                /* This last simplification is done to complete the bilinear
+                 * equation for the combination of phase and scale.
+                 */
+                for(size_t i{0};i < m;++i)
+                {
+                    const double spDelta{(filter[si+1][pi+1][o+i] - filter[si+1][pi][o+i]) -
+                        (filter[si][pi+1][o+i] - filter[si][pi][o+i])};
+                    mTable[idx++] = static_cast<float>(spDelta);
+                }
+            }
+        }
+        assert(idx == hdr.total_size);
+    }
+
+    constexpr const BSincHeader &getHeader() const noexcept { return hdr; }
+    constexpr const float *getTable() const noexcept { return &mTable.front(); }
+};
+
+#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 6
+const BSincFilterArray<bsinc12_hdr.total_size> bsinc12_filter{bsinc12_hdr};
+const BSincFilterArray<bsinc24_hdr.total_size> bsinc24_filter{bsinc24_hdr};
+#else
+const BSincFilterArray<bsinc12_hdr> bsinc12_filter{};
+const BSincFilterArray<bsinc24_hdr> bsinc24_filter{};
+#endif
+
+template<typename T>
+constexpr BSincTable GenerateBSincTable(const T &filter)
+{
+    BSincTable ret{};
+    const BSincHeader &hdr = filter.getHeader();
+    ret.scaleBase = static_cast<float>(hdr.scaleBase);
+    ret.scaleRange = static_cast<float>(1.0 / hdr.scaleRange);
+    for(size_t i{0};i < BSincScaleCount;++i)
+        ret.m[i] = ((hdr.a[i]*2) + 3) & ~3u;
+    ret.filterOffset[0] = 0;
+    for(size_t i{1};i < BSincScaleCount;++i)
+        ret.filterOffset[i] = ret.filterOffset[i-1] + ret.m[i-1]*4*BSincPhaseCount;
+    ret.Tab = filter.getTable();
+    return ret;
+}
+
+} // namespace
+
+const BSincTable gBSinc12{GenerateBSincTable(bsinc12_filter)};
+const BSincTable gBSinc24{GenerateBSincTable(bsinc24_filter)};
diff --git a/core/bsinc_tables.h b/core/bsinc_tables.h
new file mode 100644 (file)
index 0000000..aca4b27
--- /dev/null
@@ -0,0 +1,17 @@
+#ifndef CORE_BSINC_TABLES_H
+#define CORE_BSINC_TABLES_H
+
+#include "bsinc_defs.h"
+
+
+struct BSincTable {
+    float scaleBase, scaleRange;
+    unsigned int m[BSincScaleCount];
+    unsigned int filterOffset[BSincScaleCount];
+    const float *Tab;
+};
+
+extern const BSincTable gBSinc12;
+extern const BSincTable gBSinc24;
+
+#endif /* CORE_BSINC_TABLES_H */
diff --git a/core/buffer_storage.cpp b/core/buffer_storage.cpp
new file mode 100644 (file)
index 0000000..98ca2c1
--- /dev/null
@@ -0,0 +1,81 @@
+
+#include "config.h"
+
+#include "buffer_storage.h"
+
+#include <stdint.h>
+
+
+const char *NameFromFormat(FmtType type) noexcept
+{
+    switch(type)
+    {
+    case FmtUByte: return "UInt8";
+    case FmtShort: return "Int16";
+    case FmtFloat: return "Float";
+    case FmtDouble: return "Double";
+    case FmtMulaw: return "muLaw";
+    case FmtAlaw: return "aLaw";
+    case FmtIMA4: return "IMA4 ADPCM";
+    case FmtMSADPCM: return "MS ADPCM";
+    }
+    return "<internal error>";
+}
+
+const char *NameFromFormat(FmtChannels channels) noexcept
+{
+    switch(channels)
+    {
+    case FmtMono: return "Mono";
+    case FmtStereo: return "Stereo";
+    case FmtRear: return "Rear";
+    case FmtQuad: return "Quadraphonic";
+    case FmtX51: return "Surround 5.1";
+    case FmtX61: return "Surround 6.1";
+    case FmtX71: return "Surround 7.1";
+    case FmtBFormat2D: return "B-Format 2D";
+    case FmtBFormat3D: return "B-Format 3D";
+    case FmtUHJ2: return "UHJ2";
+    case FmtUHJ3: return "UHJ3";
+    case FmtUHJ4: return "UHJ4";
+    case FmtSuperStereo: return "Super Stereo";
+    }
+    return "<internal error>";
+}
+
+uint BytesFromFmt(FmtType type) noexcept
+{
+    switch(type)
+    {
+    case FmtUByte: return sizeof(uint8_t);
+    case FmtShort: return sizeof(int16_t);
+    case FmtFloat: return sizeof(float);
+    case FmtDouble: return sizeof(double);
+    case FmtMulaw: return sizeof(uint8_t);
+    case FmtAlaw: return sizeof(uint8_t);
+    case FmtIMA4: break;
+    case FmtMSADPCM: break;
+    }
+    return 0;
+}
+
+uint ChannelsFromFmt(FmtChannels chans, uint ambiorder) noexcept
+{
+    switch(chans)
+    {
+    case FmtMono: return 1;
+    case FmtStereo: return 2;
+    case FmtRear: return 2;
+    case FmtQuad: return 4;
+    case FmtX51: return 6;
+    case FmtX61: return 7;
+    case FmtX71: return 8;
+    case FmtBFormat2D: return (ambiorder*2) + 1;
+    case FmtBFormat3D: return (ambiorder+1) * (ambiorder+1);
+    case FmtUHJ2: return 2;
+    case FmtUHJ3: return 3;
+    case FmtUHJ4: return 4;
+    case FmtSuperStereo: return 2;
+    }
+    return 0;
+}
diff --git a/core/buffer_storage.h b/core/buffer_storage.h
new file mode 100644 (file)
index 0000000..282d5b5
--- /dev/null
@@ -0,0 +1,115 @@
+#ifndef CORE_BUFFER_STORAGE_H
+#define CORE_BUFFER_STORAGE_H
+
+#include <atomic>
+
+#include "albyte.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "ambidefs.h"
+
+
+using uint = unsigned int;
+
+/* Storable formats */
+enum FmtType : unsigned char {
+    FmtUByte,
+    FmtShort,
+    FmtFloat,
+    FmtDouble,
+    FmtMulaw,
+    FmtAlaw,
+    FmtIMA4,
+    FmtMSADPCM,
+};
+enum FmtChannels : unsigned char {
+    FmtMono,
+    FmtStereo,
+    FmtRear,
+    FmtQuad,
+    FmtX51, /* (WFX order) */
+    FmtX61, /* (WFX order) */
+    FmtX71, /* (WFX order) */
+    FmtBFormat2D,
+    FmtBFormat3D,
+    FmtUHJ2, /* 2-channel UHJ, aka "BHJ", stereo-compatible */
+    FmtUHJ3, /* 3-channel UHJ, aka "THJ" */
+    FmtUHJ4, /* 4-channel UHJ, aka "PHJ" */
+    FmtSuperStereo, /* Stereo processed with Super Stereo. */
+};
+
+enum class AmbiLayout : unsigned char {
+    FuMa,
+    ACN,
+};
+enum class AmbiScaling : unsigned char {
+    FuMa,
+    SN3D,
+    N3D,
+    UHJ,
+};
+
+const char *NameFromFormat(FmtType type) noexcept;
+const char *NameFromFormat(FmtChannels channels) noexcept;
+
+uint BytesFromFmt(FmtType type) noexcept;
+uint ChannelsFromFmt(FmtChannels chans, uint ambiorder) noexcept;
+inline uint FrameSizeFromFmt(FmtChannels chans, FmtType type, uint ambiorder) noexcept
+{ return ChannelsFromFmt(chans, ambiorder) * BytesFromFmt(type); }
+
+constexpr bool IsBFormat(FmtChannels chans) noexcept
+{ return chans == FmtBFormat2D || chans == FmtBFormat3D; }
+
+/* Super Stereo is considered part of the UHJ family here, since it goes
+ * through similar processing as UHJ, both result in a B-Format signal, and
+ * needs the same consideration as BHJ (three channel result with only two
+ * channel input).
+ */
+constexpr bool IsUHJ(FmtChannels chans) noexcept
+{ return chans == FmtUHJ2 || chans == FmtUHJ3 || chans == FmtUHJ4 || chans == FmtSuperStereo; }
+
+/** Ambisonic formats are either B-Format or UHJ formats. */
+constexpr bool IsAmbisonic(FmtChannels chans) noexcept
+{ return IsBFormat(chans) || IsUHJ(chans); }
+
+constexpr bool Is2DAmbisonic(FmtChannels chans) noexcept
+{
+    return chans == FmtBFormat2D || chans == FmtUHJ2 || chans == FmtUHJ3
+        || chans == FmtSuperStereo;
+}
+
+
+using CallbackType = int(*)(void*, void*, int);
+
+struct BufferStorage {
+    CallbackType mCallback{nullptr};
+    void *mUserData{nullptr};
+
+    al::span<al::byte> mData;
+
+    uint mSampleRate{0u};
+    FmtChannels mChannels{FmtMono};
+    FmtType mType{FmtShort};
+    uint mSampleLen{0u};
+    uint mBlockAlign{0u};
+
+    AmbiLayout mAmbiLayout{AmbiLayout::FuMa};
+    AmbiScaling mAmbiScaling{AmbiScaling::FuMa};
+    uint mAmbiOrder{0u};
+
+    inline uint bytesFromFmt() const noexcept { return BytesFromFmt(mType); }
+    inline uint channelsFromFmt() const noexcept
+    { return ChannelsFromFmt(mChannels, mAmbiOrder); }
+    inline uint frameSizeFromFmt() const noexcept { return channelsFromFmt() * bytesFromFmt(); }
+
+    inline uint blockSizeFromFmt() const noexcept
+    {
+        if(mType == FmtIMA4) return ((mBlockAlign-1)/2 + 4) * channelsFromFmt();
+        if(mType == FmtMSADPCM) return ((mBlockAlign-2)/2 + 7) * channelsFromFmt();
+        return frameSizeFromFmt();
+    };
+
+    inline bool isBFormat() const noexcept { return IsBFormat(mChannels); }
+};
+
+#endif /* CORE_BUFFER_STORAGE_H */
diff --git a/core/bufferline.h b/core/bufferline.h
new file mode 100644 (file)
index 0000000..8b445f3
--- /dev/null
@@ -0,0 +1,17 @@
+#ifndef CORE_BUFFERLINE_H
+#define CORE_BUFFERLINE_H
+
+#include <array>
+
+#include "alspan.h"
+
+/* Size for temporary storage of buffer data, in floats. Larger values need
+ * more memory and are harder on cache, while smaller values may need more
+ * iterations for mixing.
+ */
+constexpr int BufferLineSize{1024};
+
+using FloatBufferLine = std::array<float,BufferLineSize>;
+using FloatBufferSpan = al::span<float,BufferLineSize>;
+
+#endif /* CORE_BUFFERLINE_H */
diff --git a/core/context.cpp b/core/context.cpp
new file mode 100644 (file)
index 0000000..d68d832
--- /dev/null
@@ -0,0 +1,164 @@
+
+#include "config.h"
+
+#include <cassert>
+#include <memory>
+
+#include "async_event.h"
+#include "context.h"
+#include "device.h"
+#include "effectslot.h"
+#include "logging.h"
+#include "ringbuffer.h"
+#include "voice.h"
+#include "voice_change.h"
+
+
+#ifdef __cpp_lib_atomic_is_always_lock_free
+static_assert(std::atomic<ContextBase::AsyncEventBitset>::is_always_lock_free, "atomic<bitset> isn't lock-free");
+#endif
+
+ContextBase::ContextBase(DeviceBase *device) : mDevice{device}
+{ assert(mEnabledEvts.is_lock_free()); }
+
+ContextBase::~ContextBase()
+{
+    size_t count{0};
+    ContextProps *cprops{mParams.ContextUpdate.exchange(nullptr, std::memory_order_relaxed)};
+    if(cprops)
+    {
+        ++count;
+        delete cprops;
+    }
+    cprops = mFreeContextProps.exchange(nullptr, std::memory_order_acquire);
+    while(cprops)
+    {
+        std::unique_ptr<ContextProps> old{cprops};
+        cprops = old->next.load(std::memory_order_relaxed);
+        ++count;
+    }
+    TRACE("Freed %zu context property object%s\n", count, (count==1)?"":"s");
+
+    count = 0;
+    EffectSlotProps *eprops{mFreeEffectslotProps.exchange(nullptr, std::memory_order_acquire)};
+    while(eprops)
+    {
+        std::unique_ptr<EffectSlotProps> old{eprops};
+        eprops = old->next.load(std::memory_order_relaxed);
+        ++count;
+    }
+    TRACE("Freed %zu AuxiliaryEffectSlot property object%s\n", count, (count==1)?"":"s");
+
+    if(EffectSlotArray *curarray{mActiveAuxSlots.exchange(nullptr, std::memory_order_relaxed)})
+    {
+        al::destroy_n(curarray->end(), curarray->size());
+        delete curarray;
+    }
+
+    delete mVoices.exchange(nullptr, std::memory_order_relaxed);
+
+    if(mAsyncEvents)
+    {
+        count = 0;
+        auto evt_vec = mAsyncEvents->getReadVector();
+        if(evt_vec.first.len > 0)
+        {
+            al::destroy_n(reinterpret_cast<AsyncEvent*>(evt_vec.first.buf), evt_vec.first.len);
+            count += evt_vec.first.len;
+        }
+        if(evt_vec.second.len > 0)
+        {
+            al::destroy_n(reinterpret_cast<AsyncEvent*>(evt_vec.second.buf), evt_vec.second.len);
+            count += evt_vec.second.len;
+        }
+        if(count > 0)
+            TRACE("Destructed %zu orphaned event%s\n", count, (count==1)?"":"s");
+        mAsyncEvents->readAdvance(count);
+    }
+}
+
+
+void ContextBase::allocVoiceChanges()
+{
+    constexpr size_t clustersize{128};
+
+    VoiceChangeCluster cluster{std::make_unique<VoiceChange[]>(clustersize)};
+    for(size_t i{1};i < clustersize;++i)
+        cluster[i-1].mNext.store(std::addressof(cluster[i]), std::memory_order_relaxed);
+    cluster[clustersize-1].mNext.store(mVoiceChangeTail, std::memory_order_relaxed);
+
+    mVoiceChangeClusters.emplace_back(std::move(cluster));
+    mVoiceChangeTail = mVoiceChangeClusters.back().get();
+}
+
+void ContextBase::allocVoiceProps()
+{
+    constexpr size_t clustersize{32};
+
+    TRACE("Increasing allocated voice properties to %zu\n",
+        (mVoicePropClusters.size()+1) * clustersize);
+
+    VoicePropsCluster cluster{std::make_unique<VoicePropsItem[]>(clustersize)};
+    for(size_t i{1};i < clustersize;++i)
+        cluster[i-1].next.store(std::addressof(cluster[i]), std::memory_order_relaxed);
+    mVoicePropClusters.emplace_back(std::move(cluster));
+
+    VoicePropsItem *oldhead{mFreeVoiceProps.load(std::memory_order_acquire)};
+    do {
+        mVoicePropClusters.back()[clustersize-1].next.store(oldhead, std::memory_order_relaxed);
+    } while(mFreeVoiceProps.compare_exchange_weak(oldhead, mVoicePropClusters.back().get(),
+        std::memory_order_acq_rel, std::memory_order_acquire) == false);
+}
+
+void ContextBase::allocVoices(size_t addcount)
+{
+    constexpr size_t clustersize{32};
+    /* Convert element count to cluster count. */
+    addcount = (addcount+(clustersize-1)) / clustersize;
+
+    if(addcount >= std::numeric_limits<int>::max()/clustersize - mVoiceClusters.size())
+        throw std::runtime_error{"Allocating too many voices"};
+    const size_t totalcount{(mVoiceClusters.size()+addcount) * clustersize};
+    TRACE("Increasing allocated voices to %zu\n", totalcount);
+
+    auto newarray = VoiceArray::Create(totalcount);
+    while(addcount)
+    {
+        mVoiceClusters.emplace_back(std::make_unique<Voice[]>(clustersize));
+        --addcount;
+    }
+
+    auto voice_iter = newarray->begin();
+    for(VoiceCluster &cluster : mVoiceClusters)
+    {
+        for(size_t i{0};i < clustersize;++i)
+            *(voice_iter++) = &cluster[i];
+    }
+
+    if(auto *oldvoices = mVoices.exchange(newarray.release(), std::memory_order_acq_rel))
+    {
+        mDevice->waitForMix();
+        delete oldvoices;
+    }
+}
+
+
+EffectSlot *ContextBase::getEffectSlot()
+{
+    for(auto& cluster : mEffectSlotClusters)
+    {
+        for(size_t i{0};i < EffectSlotClusterSize;++i)
+        {
+            if(!cluster[i].InUse)
+                return &cluster[i];
+        }
+    }
+
+    if(1 >= std::numeric_limits<int>::max()/EffectSlotClusterSize - mEffectSlotClusters.size())
+        throw std::runtime_error{"Allocating too many effect slots"};
+    const size_t totalcount{(mEffectSlotClusters.size()+1) * EffectSlotClusterSize};
+    TRACE("Increasing allocated effect slots to %zu\n", totalcount);
+
+    mEffectSlotClusters.emplace_back(std::make_unique<EffectSlot[]>(EffectSlotClusterSize));
+    return getEffectSlot();
+}
diff --git a/core/context.h b/core/context.h
new file mode 100644 (file)
index 0000000..9723eac
--- /dev/null
@@ -0,0 +1,171 @@
+#ifndef CORE_CONTEXT_H
+#define CORE_CONTEXT_H
+
+#include <array>
+#include <atomic>
+#include <bitset>
+#include <cstddef>
+#include <memory>
+#include <thread>
+
+#include "almalloc.h"
+#include "alspan.h"
+#include "async_event.h"
+#include "atomic.h"
+#include "bufferline.h"
+#include "threads.h"
+#include "vecmat.h"
+#include "vector.h"
+
+struct DeviceBase;
+struct EffectSlot;
+struct EffectSlotProps;
+struct RingBuffer;
+struct Voice;
+struct VoiceChange;
+struct VoicePropsItem;
+
+using uint = unsigned int;
+
+
+constexpr float SpeedOfSoundMetersPerSec{343.3f};
+
+constexpr float AirAbsorbGainHF{0.99426f}; /* -0.05dB */
+
+enum class DistanceModel : unsigned char {
+    Disable,
+    Inverse, InverseClamped,
+    Linear, LinearClamped,
+    Exponent, ExponentClamped,
+
+    Default = InverseClamped
+};
+
+
+struct ContextProps {
+    std::array<float,3> Position;
+    std::array<float,3> Velocity;
+    std::array<float,3> OrientAt;
+    std::array<float,3> OrientUp;
+    float Gain;
+    float MetersPerUnit;
+    float AirAbsorptionGainHF;
+
+    float DopplerFactor;
+    float DopplerVelocity;
+    float SpeedOfSound;
+    bool SourceDistanceModel;
+    DistanceModel mDistanceModel;
+
+    std::atomic<ContextProps*> next;
+
+    DEF_NEWDEL(ContextProps)
+};
+
+struct ContextParams {
+    /* Pointer to the most recent property values that are awaiting an update. */
+    std::atomic<ContextProps*> ContextUpdate{nullptr};
+
+    alu::Vector Position{};
+    alu::Matrix Matrix{alu::Matrix::Identity()};
+    alu::Vector Velocity{};
+
+    float Gain{1.0f};
+    float MetersPerUnit{1.0f};
+    float AirAbsorptionGainHF{AirAbsorbGainHF};
+
+    float DopplerFactor{1.0f};
+    float SpeedOfSound{SpeedOfSoundMetersPerSec}; /* in units per sec! */
+
+    bool SourceDistanceModel{false};
+    DistanceModel mDistanceModel{};
+};
+
+struct ContextBase {
+    DeviceBase *const 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};
+    std::atomic<bool> mStopVoicesOnDisconnect{true};
+
+    float mGainBoost{1.0f};
+
+    /* Linked lists of unused property containers, free to use for future
+     * updates.
+     */
+    std::atomic<ContextProps*> mFreeContextProps{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();
+    void allocVoiceProps();
+
+
+    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;
+    using AsyncEventBitset = std::bitset<AsyncEvent::UserEventCount>;
+    std::atomic<AsyncEventBitset> 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;
+
+    using VoicePropsCluster = std::unique_ptr<VoicePropsItem[]>;
+    al::vector<VoicePropsCluster> mVoicePropClusters;
+
+
+    static constexpr size_t EffectSlotClusterSize{4};
+    EffectSlot *getEffectSlot();
+
+    using EffectSlotCluster = std::unique_ptr<EffectSlot[]>;
+    al::vector<EffectSlotCluster> mEffectSlotClusters;
+
+
+    ContextBase(DeviceBase *device);
+    ContextBase(const ContextBase&) = delete;
+    ContextBase& operator=(const ContextBase&) = delete;
+    ~ContextBase();
+};
+
+#endif /* CORE_CONTEXT_H */
diff --git a/core/converter.cpp b/core/converter.cpp
new file mode 100644 (file)
index 0000000..a514144
--- /dev/null
@@ -0,0 +1,346 @@
+
+#include "config.h"
+
+#include "converter.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <cstdint>
+#include <iterator>
+#include <limits.h>
+
+#include "albit.h"
+#include "albyte.h"
+#include "alnumeric.h"
+#include "fpu_ctrl.h"
+
+
+namespace {
+
+constexpr uint MaxPitch{10};
+
+static_assert((BufferLineSize-1)/MaxPitch > 0, "MaxPitch is too large for BufferLineSize!");
+static_assert((INT_MAX>>MixerFracBits)/MaxPitch > BufferLineSize,
+    "MaxPitch and/or BufferLineSize are too large for MixerFracBits!");
+
+/* Base template left undefined. Should be marked =delete, but Clang 3.8.1
+ * chokes on that given the inline specializations.
+ */
+template<DevFmtType T>
+inline float LoadSample(DevFmtType_t<T> val) noexcept;
+
+template<> inline float LoadSample<DevFmtByte>(DevFmtType_t<DevFmtByte> val) noexcept
+{ return val * (1.0f/128.0f); }
+template<> inline float LoadSample<DevFmtShort>(DevFmtType_t<DevFmtShort> val) noexcept
+{ return val * (1.0f/32768.0f); }
+template<> inline float LoadSample<DevFmtInt>(DevFmtType_t<DevFmtInt> val) noexcept
+{ return static_cast<float>(val) * (1.0f/2147483648.0f); }
+template<> inline float LoadSample<DevFmtFloat>(DevFmtType_t<DevFmtFloat> val) noexcept
+{ return val; }
+
+template<> inline float LoadSample<DevFmtUByte>(DevFmtType_t<DevFmtUByte> val) noexcept
+{ return LoadSample<DevFmtByte>(static_cast<int8_t>(val - 128)); }
+template<> inline float LoadSample<DevFmtUShort>(DevFmtType_t<DevFmtUShort> val) noexcept
+{ return LoadSample<DevFmtShort>(static_cast<int16_t>(val - 32768)); }
+template<> inline float LoadSample<DevFmtUInt>(DevFmtType_t<DevFmtUInt> val) noexcept
+{ return LoadSample<DevFmtInt>(static_cast<int32_t>(val - 2147483648u)); }
+
+
+template<DevFmtType T>
+inline void LoadSampleArray(float *RESTRICT dst, const void *src, const size_t srcstep,
+    const size_t samples) noexcept
+{
+    const DevFmtType_t<T> *ssrc = static_cast<const DevFmtType_t<T>*>(src);
+    for(size_t i{0u};i < samples;i++)
+        dst[i] = LoadSample<T>(ssrc[i*srcstep]);
+}
+
+void LoadSamples(float *dst, const void *src, const size_t srcstep, const DevFmtType srctype,
+    const size_t samples) noexcept
+{
+#define HANDLE_FMT(T)                                                         \
+    case T: LoadSampleArray<T>(dst, src, srcstep, samples); break
+    switch(srctype)
+    {
+        HANDLE_FMT(DevFmtByte);
+        HANDLE_FMT(DevFmtUByte);
+        HANDLE_FMT(DevFmtShort);
+        HANDLE_FMT(DevFmtUShort);
+        HANDLE_FMT(DevFmtInt);
+        HANDLE_FMT(DevFmtUInt);
+        HANDLE_FMT(DevFmtFloat);
+    }
+#undef HANDLE_FMT
+}
+
+
+template<DevFmtType T>
+inline DevFmtType_t<T> StoreSample(float) noexcept;
+
+template<> inline float StoreSample<DevFmtFloat>(float val) noexcept
+{ return val; }
+template<> inline int32_t StoreSample<DevFmtInt>(float val) noexcept
+{ return fastf2i(clampf(val*2147483648.0f, -2147483648.0f, 2147483520.0f)); }
+template<> inline int16_t StoreSample<DevFmtShort>(float val) noexcept
+{ return static_cast<int16_t>(fastf2i(clampf(val*32768.0f, -32768.0f, 32767.0f))); }
+template<> inline int8_t StoreSample<DevFmtByte>(float val) noexcept
+{ return static_cast<int8_t>(fastf2i(clampf(val*128.0f, -128.0f, 127.0f))); }
+
+/* Define unsigned output variations. */
+template<> inline uint32_t StoreSample<DevFmtUInt>(float val) noexcept
+{ return static_cast<uint32_t>(StoreSample<DevFmtInt>(val)) + 2147483648u; }
+template<> inline uint16_t StoreSample<DevFmtUShort>(float val) noexcept
+{ return static_cast<uint16_t>(StoreSample<DevFmtShort>(val) + 32768); }
+template<> inline uint8_t StoreSample<DevFmtUByte>(float val) noexcept
+{ return static_cast<uint8_t>(StoreSample<DevFmtByte>(val) + 128); }
+
+template<DevFmtType T>
+inline void StoreSampleArray(void *dst, const float *RESTRICT src, const size_t dststep,
+    const size_t samples) noexcept
+{
+    DevFmtType_t<T> *sdst = static_cast<DevFmtType_t<T>*>(dst);
+    for(size_t i{0u};i < samples;i++)
+        sdst[i*dststep] = StoreSample<T>(src[i]);
+}
+
+
+void StoreSamples(void *dst, const float *src, const size_t dststep, const DevFmtType dsttype,
+    const size_t samples) noexcept
+{
+#define HANDLE_FMT(T)                                                         \
+    case T: StoreSampleArray<T>(dst, src, dststep, samples); break
+    switch(dsttype)
+    {
+        HANDLE_FMT(DevFmtByte);
+        HANDLE_FMT(DevFmtUByte);
+        HANDLE_FMT(DevFmtShort);
+        HANDLE_FMT(DevFmtUShort);
+        HANDLE_FMT(DevFmtInt);
+        HANDLE_FMT(DevFmtUInt);
+        HANDLE_FMT(DevFmtFloat);
+    }
+#undef HANDLE_FMT
+}
+
+
+template<DevFmtType T>
+void Mono2Stereo(float *RESTRICT dst, const void *src, const size_t frames) noexcept
+{
+    const DevFmtType_t<T> *ssrc = static_cast<const DevFmtType_t<T>*>(src);
+    for(size_t i{0u};i < frames;i++)
+        dst[i*2 + 1] = dst[i*2 + 0] = LoadSample<T>(ssrc[i]) * 0.707106781187f;
+}
+
+template<DevFmtType T>
+void Multi2Mono(uint chanmask, const size_t step, const float scale, float *RESTRICT dst,
+    const void *src, const size_t frames) noexcept
+{
+    const DevFmtType_t<T> *ssrc = static_cast<const DevFmtType_t<T>*>(src);
+    std::fill_n(dst, frames, 0.0f);
+    for(size_t c{0};chanmask;++c)
+    {
+        if((chanmask&1)) LIKELY
+        {
+            for(size_t i{0u};i < frames;i++)
+                dst[i] += LoadSample<T>(ssrc[i*step + c]);
+        }
+        chanmask >>= 1;
+    }
+    for(size_t i{0u};i < frames;i++)
+        dst[i] *= scale;
+}
+
+} // namespace
+
+SampleConverterPtr SampleConverter::Create(DevFmtType srcType, DevFmtType dstType, size_t numchans,
+    uint srcRate, uint dstRate, Resampler resampler)
+{
+    if(numchans < 1 || srcRate < 1 || dstRate < 1)
+        return nullptr;
+
+    SampleConverterPtr converter{new(FamCount(numchans)) SampleConverter{numchans}};
+    converter->mSrcType = srcType;
+    converter->mDstType = dstType;
+    converter->mSrcTypeSize = BytesFromDevFmt(srcType);
+    converter->mDstTypeSize = BytesFromDevFmt(dstType);
+
+    converter->mSrcPrepCount = MaxResamplerPadding;
+    converter->mFracOffset = 0;
+    for(auto &chan : converter->mChan)
+    {
+        const al::span<float> buffer{chan.PrevSamples};
+        std::fill(buffer.begin(), buffer.end(), 0.0f);
+    }
+
+    /* Have to set the mixer FPU mode since that's what the resampler code expects. */
+    FPUCtl mixer_mode{};
+    auto step = static_cast<uint>(
+        mind(srcRate*double{MixerFracOne}/dstRate + 0.5, MaxPitch*MixerFracOne));
+    converter->mIncrement = maxu(step, 1);
+    if(converter->mIncrement == MixerFracOne)
+        converter->mResample = [](const InterpState*, const float *RESTRICT src, uint, const uint,
+            const al::span<float> dst) { std::copy_n(src, dst.size(), dst.begin()); };
+    else
+        converter->mResample = PrepareResampler(resampler, converter->mIncrement,
+            &converter->mState);
+
+    return converter;
+}
+
+uint SampleConverter::availableOut(uint srcframes) const
+{
+    if(srcframes < 1)
+    {
+        /* No output samples if there's no input samples. */
+        return 0;
+    }
+
+    const uint prepcount{mSrcPrepCount};
+    if(prepcount < MaxResamplerPadding && MaxResamplerPadding - prepcount >= srcframes)
+    {
+        /* Not enough input samples to generate an output sample. */
+        return 0;
+    }
+
+    uint64_t DataSize64{prepcount};
+    DataSize64 += srcframes;
+    DataSize64 -= MaxResamplerPadding;
+    DataSize64 <<= MixerFracBits;
+    DataSize64 -= mFracOffset;
+
+    /* If we have a full prep, we can generate at least one sample. */
+    return static_cast<uint>(clampu64((DataSize64 + mIncrement-1)/mIncrement, 1,
+        std::numeric_limits<int>::max()));
+}
+
+uint SampleConverter::convert(const void **src, uint *srcframes, void *dst, uint dstframes)
+{
+    const uint SrcFrameSize{static_cast<uint>(mChan.size()) * mSrcTypeSize};
+    const uint DstFrameSize{static_cast<uint>(mChan.size()) * mDstTypeSize};
+    const uint increment{mIncrement};
+    auto SamplesIn = static_cast<const al::byte*>(*src);
+    uint NumSrcSamples{*srcframes};
+
+    FPUCtl mixer_mode{};
+    uint pos{0};
+    while(pos < dstframes && NumSrcSamples > 0)
+    {
+        const uint prepcount{mSrcPrepCount};
+        const uint readable{minu(NumSrcSamples, BufferLineSize - prepcount)};
+
+        if(prepcount < MaxResamplerPadding && MaxResamplerPadding-prepcount >= readable)
+        {
+            /* Not enough input samples to generate an output sample. Store
+             * what we're given for later.
+             */
+            for(size_t chan{0u};chan < mChan.size();chan++)
+                LoadSamples(&mChan[chan].PrevSamples[prepcount], SamplesIn + mSrcTypeSize*chan,
+                    mChan.size(), mSrcType, readable);
+
+            mSrcPrepCount = prepcount + readable;
+            NumSrcSamples = 0;
+            break;
+        }
+
+        float *RESTRICT SrcData{mSrcSamples};
+        float *RESTRICT DstData{mDstSamples};
+        uint DataPosFrac{mFracOffset};
+        uint64_t DataSize64{prepcount};
+        DataSize64 += readable;
+        DataSize64 -= MaxResamplerPadding;
+        DataSize64 <<= MixerFracBits;
+        DataSize64 -= DataPosFrac;
+
+        /* If we have a full prep, we can generate at least one sample. */
+        auto DstSize = static_cast<uint>(
+            clampu64((DataSize64 + increment-1)/increment, 1, BufferLineSize));
+        DstSize = minu(DstSize, dstframes-pos);
+
+        const uint DataPosEnd{DstSize*increment + DataPosFrac};
+        const uint SrcDataEnd{DataPosEnd>>MixerFracBits};
+
+        assert(prepcount+readable >= SrcDataEnd);
+        const uint nextprep{minu(prepcount + readable - SrcDataEnd, MaxResamplerPadding)};
+
+        for(size_t chan{0u};chan < mChan.size();chan++)
+        {
+            const al::byte *SrcSamples{SamplesIn + mSrcTypeSize*chan};
+            al::byte *DstSamples = static_cast<al::byte*>(dst) + mDstTypeSize*chan;
+
+            /* Load the previous samples into the source data first, then the
+             * new samples from the input buffer.
+             */
+            std::copy_n(mChan[chan].PrevSamples, prepcount, SrcData);
+            LoadSamples(SrcData + prepcount, SrcSamples, mChan.size(), mSrcType, readable);
+
+            /* Store as many prep samples for next time as possible, given the
+             * number of output samples being generated.
+             */
+            std::copy_n(SrcData+SrcDataEnd, nextprep, mChan[chan].PrevSamples);
+            std::fill(std::begin(mChan[chan].PrevSamples)+nextprep,
+                std::end(mChan[chan].PrevSamples), 0.0f);
+
+            /* Now resample, and store the result in the output buffer. */
+            mResample(&mState, SrcData+MaxResamplerEdge, DataPosFrac, increment,
+                {DstData, DstSize});
+
+            StoreSamples(DstSamples, DstData, mChan.size(), mDstType, DstSize);
+        }
+
+        /* Update the number of prep samples still available, as well as the
+         * fractional offset.
+         */
+        mSrcPrepCount = nextprep;
+        mFracOffset = DataPosEnd & MixerFracMask;
+
+        /* Update the src and dst pointers in case there's still more to do. */
+        const uint srcread{minu(NumSrcSamples, SrcDataEnd + mSrcPrepCount - prepcount)};
+        SamplesIn += SrcFrameSize*srcread;
+        NumSrcSamples -= srcread;
+
+        dst = static_cast<al::byte*>(dst) + DstFrameSize*DstSize;
+        pos += DstSize;
+    }
+
+    *src = SamplesIn;
+    *srcframes = NumSrcSamples;
+
+    return pos;
+}
+
+
+void ChannelConverter::convert(const void *src, float *dst, uint frames) const
+{
+    if(mDstChans == DevFmtMono)
+    {
+        const float scale{std::sqrt(1.0f / static_cast<float>(al::popcount(mChanMask)))};
+        switch(mSrcType)
+        {
+#define HANDLE_FMT(T) case T: Multi2Mono<T>(mChanMask, mSrcStep, scale, dst, src, frames); break
+        HANDLE_FMT(DevFmtByte);
+        HANDLE_FMT(DevFmtUByte);
+        HANDLE_FMT(DevFmtShort);
+        HANDLE_FMT(DevFmtUShort);
+        HANDLE_FMT(DevFmtInt);
+        HANDLE_FMT(DevFmtUInt);
+        HANDLE_FMT(DevFmtFloat);
+#undef HANDLE_FMT
+        }
+    }
+    else if(mChanMask == 0x1 && mDstChans == DevFmtStereo)
+    {
+        switch(mSrcType)
+        {
+#define HANDLE_FMT(T) case T: Mono2Stereo<T>(dst, src, frames); break
+        HANDLE_FMT(DevFmtByte);
+        HANDLE_FMT(DevFmtUByte);
+        HANDLE_FMT(DevFmtShort);
+        HANDLE_FMT(DevFmtUShort);
+        HANDLE_FMT(DevFmtInt);
+        HANDLE_FMT(DevFmtUInt);
+        HANDLE_FMT(DevFmtFloat);
+#undef HANDLE_FMT
+        }
+    }
+}
diff --git a/core/converter.h b/core/converter.h
new file mode 100644 (file)
index 0000000..01becea
--- /dev/null
@@ -0,0 +1,66 @@
+#ifndef CORE_CONVERTER_H
+#define CORE_CONVERTER_H
+
+#include <chrono>
+#include <cstddef>
+#include <memory>
+
+#include "almalloc.h"
+#include "devformat.h"
+#include "mixer/defs.h"
+
+using uint = unsigned int;
+
+
+struct SampleConverter {
+    DevFmtType mSrcType{};
+    DevFmtType mDstType{};
+    uint mSrcTypeSize{};
+    uint mDstTypeSize{};
+
+    uint mSrcPrepCount{};
+
+    uint mFracOffset{};
+    uint mIncrement{};
+    InterpState mState{};
+    ResamplerFunc mResample{};
+
+    alignas(16) float mSrcSamples[BufferLineSize]{};
+    alignas(16) float mDstSamples[BufferLineSize]{};
+
+    struct ChanSamples {
+        alignas(16) float PrevSamples[MaxResamplerPadding];
+    };
+    al::FlexArray<ChanSamples> mChan;
+
+    SampleConverter(size_t numchans) : mChan{numchans} { }
+
+    uint convert(const void **src, uint *srcframes, void *dst, uint dstframes);
+    uint availableOut(uint srcframes) const;
+
+    using SampleOffset = std::chrono::duration<int64_t, std::ratio<1,MixerFracOne>>;
+    SampleOffset currentInputDelay() const noexcept
+    {
+        const int64_t prep{int64_t{mSrcPrepCount} - MaxResamplerEdge};
+        return SampleOffset{(prep<<MixerFracBits) + mFracOffset};
+    }
+
+    static std::unique_ptr<SampleConverter> Create(DevFmtType srcType, DevFmtType dstType,
+        size_t numchans, uint srcRate, uint dstRate, Resampler resampler);
+
+    DEF_FAM_NEWDEL(SampleConverter, mChan)
+};
+using SampleConverterPtr = std::unique_ptr<SampleConverter>;
+
+struct ChannelConverter {
+    DevFmtType mSrcType{};
+    uint mSrcStep{};
+    uint mChanMask{};
+    DevFmtChannels mDstChans{};
+
+    bool is_active() const noexcept { return mChanMask != 0; }
+
+    void convert(const void *src, float *dst, uint frames) const;
+};
+
+#endif /* CORE_CONVERTER_H */
diff --git a/core/cpu_caps.cpp b/core/cpu_caps.cpp
new file mode 100644 (file)
index 0000000..d4b4d86
--- /dev/null
@@ -0,0 +1,141 @@
+
+#include "config.h"
+
+#include "cpu_caps.h"
+
+#if defined(_WIN32) && (defined(_M_ARM) || defined(_M_ARM64))
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#ifndef PF_ARM_NEON_INSTRUCTIONS_AVAILABLE
+#define PF_ARM_NEON_INSTRUCTIONS_AVAILABLE 19
+#endif
+#endif
+
+#if defined(HAVE_CPUID_H)
+#include <cpuid.h>
+#elif defined(HAVE_INTRIN_H)
+#include <intrin.h>
+#endif
+
+#include <array>
+#include <cctype>
+#include <string>
+
+
+int CPUCapFlags{0};
+
+namespace {
+
+#if defined(HAVE_GCC_GET_CPUID) \
+    && (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64))
+using reg_type = unsigned int;
+inline std::array<reg_type,4> get_cpuid(unsigned int f)
+{
+    std::array<reg_type,4> ret{};
+    __get_cpuid(f, ret.data(), &ret[1], &ret[2], &ret[3]);
+    return ret;
+}
+#define CAN_GET_CPUID
+#elif defined(HAVE_CPUID_INTRINSIC) \
+    && (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64))
+using reg_type = int;
+inline std::array<reg_type,4> get_cpuid(unsigned int f)
+{
+    std::array<reg_type,4> ret{};
+    (__cpuid)(ret.data(), f);
+    return ret;
+}
+#define CAN_GET_CPUID
+#endif
+
+} // namespace
+
+al::optional<CPUInfo> GetCPUInfo()
+{
+    CPUInfo ret;
+
+#ifdef CAN_GET_CPUID
+    auto cpuregs = get_cpuid(0);
+    if(cpuregs[0] == 0)
+        return al::nullopt;
+
+    const reg_type maxfunc{cpuregs[0]};
+
+    cpuregs = get_cpuid(0x80000000);
+    const reg_type maxextfunc{cpuregs[0]};
+
+    ret.mVendor.append(reinterpret_cast<char*>(&cpuregs[1]), 4);
+    ret.mVendor.append(reinterpret_cast<char*>(&cpuregs[3]), 4);
+    ret.mVendor.append(reinterpret_cast<char*>(&cpuregs[2]), 4);
+    auto iter_end = std::remove(ret.mVendor.begin(), ret.mVendor.end(), '\0');
+    iter_end = std::unique(ret.mVendor.begin(), iter_end,
+        [](auto&& c0, auto&& c1) { return std::isspace(c0) && std::isspace(c1); });
+    ret.mVendor.erase(iter_end, ret.mVendor.end());
+    if(!ret.mVendor.empty() && std::isspace(ret.mVendor.back()))
+        ret.mVendor.pop_back();
+    if(!ret.mVendor.empty() && std::isspace(ret.mVendor.front()))
+        ret.mVendor.erase(ret.mVendor.begin());
+
+    if(maxextfunc >= 0x80000004)
+    {
+        cpuregs = get_cpuid(0x80000002);
+        ret.mName.append(reinterpret_cast<char*>(cpuregs.data()), 16);
+        cpuregs = get_cpuid(0x80000003);
+        ret.mName.append(reinterpret_cast<char*>(cpuregs.data()), 16);
+        cpuregs = get_cpuid(0x80000004);
+        ret.mName.append(reinterpret_cast<char*>(cpuregs.data()), 16);
+        iter_end = std::remove(ret.mName.begin(), ret.mName.end(), '\0');
+        iter_end = std::unique(ret.mName.begin(), iter_end,
+            [](auto&& c0, auto&& c1) { return std::isspace(c0) && std::isspace(c1); });
+        ret.mName.erase(iter_end, ret.mName.end());
+        if(!ret.mName.empty() && std::isspace(ret.mName.back()))
+            ret.mName.pop_back();
+        if(!ret.mName.empty() && std::isspace(ret.mName.front()))
+            ret.mName.erase(ret.mName.begin());
+    }
+
+    if(maxfunc >= 1)
+    {
+        cpuregs = get_cpuid(1);
+        if((cpuregs[3]&(1<<25)))
+            ret.mCaps |= CPU_CAP_SSE;
+        if((ret.mCaps&CPU_CAP_SSE) && (cpuregs[3]&(1<<26)))
+            ret.mCaps |= CPU_CAP_SSE2;
+        if((ret.mCaps&CPU_CAP_SSE2) && (cpuregs[2]&(1<<0)))
+            ret.mCaps |= CPU_CAP_SSE3;
+        if((ret.mCaps&CPU_CAP_SSE3) && (cpuregs[2]&(1<<19)))
+            ret.mCaps |= CPU_CAP_SSE4_1;
+    }
+
+#else
+
+    /* Assume support for whatever's supported if we can't check for it */
+#if defined(HAVE_SSE4_1)
+#warning "Assuming SSE 4.1 run-time support!"
+    ret.mCaps |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3 | CPU_CAP_SSE4_1;
+#elif defined(HAVE_SSE3)
+#warning "Assuming SSE 3 run-time support!"
+    ret.mCaps |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3;
+#elif defined(HAVE_SSE2)
+#warning "Assuming SSE 2 run-time support!"
+    ret.mCaps |= CPU_CAP_SSE | CPU_CAP_SSE2;
+#elif defined(HAVE_SSE)
+#warning "Assuming SSE run-time support!"
+    ret.mCaps |= CPU_CAP_SSE;
+#endif
+#endif /* CAN_GET_CPUID */
+
+#ifdef HAVE_NEON
+#ifdef __ARM_NEON
+    ret.mCaps |= CPU_CAP_NEON;
+#elif defined(_WIN32) && (defined(_M_ARM) || defined(_M_ARM64))
+    if(IsProcessorFeaturePresent(PF_ARM_NEON_INSTRUCTIONS_AVAILABLE))
+        ret.mCaps |= CPU_CAP_NEON;
+#else
+#warning "Assuming NEON run-time support!"
+    ret.mCaps |= CPU_CAP_NEON;
+#endif
+#endif
+
+    return ret;
+}
diff --git a/core/cpu_caps.h b/core/cpu_caps.h
new file mode 100644 (file)
index 0000000..ffd671d
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef CORE_CPU_CAPS_H
+#define CORE_CPU_CAPS_H
+
+#include <string>
+
+#include "aloptional.h"
+
+
+extern int CPUCapFlags;
+enum {
+    CPU_CAP_SSE    = 1<<0,
+    CPU_CAP_SSE2   = 1<<1,
+    CPU_CAP_SSE3   = 1<<2,
+    CPU_CAP_SSE4_1 = 1<<3,
+    CPU_CAP_NEON   = 1<<4,
+};
+
+struct CPUInfo {
+    std::string mVendor;
+    std::string mName;
+    int mCaps{0};
+};
+
+al::optional<CPUInfo> GetCPUInfo();
+
+#endif /* CORE_CPU_CAPS_H */
diff --git a/core/cubic_defs.h b/core/cubic_defs.h
new file mode 100644 (file)
index 0000000..33751c9
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef CORE_CUBIC_DEFS_H
+#define CORE_CUBIC_DEFS_H
+
+/* The number of distinct phase intervals within the cubic filter tables. */
+constexpr unsigned int CubicPhaseBits{5};
+constexpr unsigned int CubicPhaseCount{1 << CubicPhaseBits};
+
+struct CubicCoefficients {
+    float mCoeffs[4];
+    float mDeltas[4];
+};
+
+#endif /* CORE_CUBIC_DEFS_H */
diff --git a/core/cubic_tables.cpp b/core/cubic_tables.cpp
new file mode 100644 (file)
index 0000000..73ec6b3
--- /dev/null
@@ -0,0 +1,59 @@
+
+#include "cubic_tables.h"
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <cmath>
+#include <limits>
+#include <memory>
+#include <stdexcept>
+
+#include "alnumbers.h"
+#include "core/mixer/defs.h"
+
+
+namespace {
+
+using uint = unsigned int;
+
+struct SplineFilterArray {
+    alignas(16) CubicCoefficients mTable[CubicPhaseCount]{};
+
+    constexpr SplineFilterArray()
+    {
+        /* Fill in the main coefficients. */
+        for(size_t pi{0};pi < CubicPhaseCount;++pi)
+        {
+            const double mu{static_cast<double>(pi) / CubicPhaseCount};
+            const double mu2{mu*mu}, mu3{mu2*mu};
+            mTable[pi].mCoeffs[0] = static_cast<float>(-0.5*mu3 +      mu2 + -0.5*mu);
+            mTable[pi].mCoeffs[1] = static_cast<float>( 1.5*mu3 + -2.5*mu2           + 1.0);
+            mTable[pi].mCoeffs[2] = static_cast<float>(-1.5*mu3 +  2.0*mu2 +  0.5*mu);
+            mTable[pi].mCoeffs[3] = static_cast<float>( 0.5*mu3 + -0.5*mu2);
+        }
+
+        /* Fill in the coefficient deltas. */
+        for(size_t pi{0};pi < CubicPhaseCount-1;++pi)
+        {
+            mTable[pi].mDeltas[0] = mTable[pi+1].mCoeffs[0] - mTable[pi].mCoeffs[0];
+            mTable[pi].mDeltas[1] = mTable[pi+1].mCoeffs[1] - mTable[pi].mCoeffs[1];
+            mTable[pi].mDeltas[2] = mTable[pi+1].mCoeffs[2] - mTable[pi].mCoeffs[2];
+            mTable[pi].mDeltas[3] = mTable[pi+1].mCoeffs[3] - mTable[pi].mCoeffs[3];
+        }
+
+        const size_t pi{CubicPhaseCount - 1};
+        mTable[pi].mDeltas[0] = -mTable[pi].mCoeffs[0];
+        mTable[pi].mDeltas[1] = -mTable[pi].mCoeffs[1];
+        mTable[pi].mDeltas[2] = 1.0f - mTable[pi].mCoeffs[2];
+        mTable[pi].mDeltas[3] = -mTable[pi].mCoeffs[3];
+    }
+
+    constexpr auto getTable() const noexcept { return al::as_span(mTable); }
+};
+
+constexpr SplineFilterArray SplineFilter{};
+
+} // namespace
+
+const CubicTable gCubicSpline{SplineFilter.getTable()};
diff --git a/core/cubic_tables.h b/core/cubic_tables.h
new file mode 100644 (file)
index 0000000..88097ae
--- /dev/null
@@ -0,0 +1,17 @@
+#ifndef CORE_CUBIC_TABLES_H
+#define CORE_CUBIC_TABLES_H
+
+#include "alspan.h"
+#include "cubic_defs.h"
+
+
+struct CubicTable {
+    al::span<const CubicCoefficients,CubicPhaseCount> Tab;
+};
+
+/* A Catmull-Rom spline. The spline passes through the center two samples,
+ * ensuring no discontinuity while moving through a series of samples.
+ */
+extern const CubicTable gCubicSpline;
+
+#endif /* CORE_CUBIC_TABLES_H */
diff --git a/core/dbus_wrap.cpp b/core/dbus_wrap.cpp
new file mode 100644 (file)
index 0000000..7f22170
--- /dev/null
@@ -0,0 +1,46 @@
+
+#include "config.h"
+
+#include "dbus_wrap.h"
+
+#ifdef HAVE_DYNLOAD
+
+#include <mutex>
+#include <type_traits>
+
+#include "logging.h"
+
+
+void *dbus_handle{nullptr};
+#define DECL_FUNC(x) decltype(p##x) p##x{};
+DBUS_FUNCTIONS(DECL_FUNC)
+#undef DECL_FUNC
+
+void PrepareDBus()
+{
+    static constexpr char libname[] = "libdbus-1.so.3";
+
+    auto load_func = [](auto &f, const char *name) -> void
+    { f = reinterpret_cast<std::remove_reference_t<decltype(f)>>(GetSymbol(dbus_handle, name)); };
+#define LOAD_FUNC(x) do {                         \
+    load_func(p##x, #x);                          \
+    if(!p##x)                                     \
+    {                                             \
+        WARN("Failed to load function %s\n", #x); \
+        CloseLib(dbus_handle);                    \
+        dbus_handle = nullptr;                    \
+        return;                                   \
+    }                                             \
+} while(0);
+
+    dbus_handle = LoadLib(libname);
+    if(!dbus_handle)
+    {
+        WARN("Failed to load %s\n", libname);
+        return;
+    }
+
+DBUS_FUNCTIONS(LOAD_FUNC)
+#undef LOAD_FUNC
+}
+#endif
diff --git a/core/dbus_wrap.h b/core/dbus_wrap.h
new file mode 100644 (file)
index 0000000..09eaacf
--- /dev/null
@@ -0,0 +1,87 @@
+#ifndef CORE_DBUS_WRAP_H
+#define CORE_DBUS_WRAP_H
+
+#include <memory>
+
+#include <dbus/dbus.h>
+
+#include "dynload.h"
+
+#ifdef HAVE_DYNLOAD
+
+#include <mutex>
+
+#define DBUS_FUNCTIONS(MAGIC) \
+MAGIC(dbus_error_init) \
+MAGIC(dbus_error_free) \
+MAGIC(dbus_bus_get) \
+MAGIC(dbus_connection_set_exit_on_disconnect) \
+MAGIC(dbus_connection_unref) \
+MAGIC(dbus_connection_send_with_reply_and_block) \
+MAGIC(dbus_message_unref) \
+MAGIC(dbus_message_new_method_call) \
+MAGIC(dbus_message_append_args) \
+MAGIC(dbus_message_iter_init) \
+MAGIC(dbus_message_iter_next) \
+MAGIC(dbus_message_iter_recurse) \
+MAGIC(dbus_message_iter_get_arg_type) \
+MAGIC(dbus_message_iter_get_basic) \
+MAGIC(dbus_set_error_from_message)
+
+extern void *dbus_handle;
+#define DECL_FUNC(x) extern decltype(x) *p##x;
+DBUS_FUNCTIONS(DECL_FUNC)
+#undef DECL_FUNC
+
+#ifndef IN_IDE_PARSER
+#define dbus_error_init (*pdbus_error_init)
+#define dbus_error_free (*pdbus_error_free)
+#define dbus_bus_get (*pdbus_bus_get)
+#define dbus_connection_set_exit_on_disconnect (*pdbus_connection_set_exit_on_disconnect)
+#define dbus_connection_unref (*pdbus_connection_unref)
+#define dbus_connection_send_with_reply_and_block (*pdbus_connection_send_with_reply_and_block)
+#define dbus_message_unref (*pdbus_message_unref)
+#define dbus_message_new_method_call (*pdbus_message_new_method_call)
+#define dbus_message_append_args (*pdbus_message_append_args)
+#define dbus_message_iter_init (*pdbus_message_iter_init)
+#define dbus_message_iter_next (*pdbus_message_iter_next)
+#define dbus_message_iter_recurse (*pdbus_message_iter_recurse)
+#define dbus_message_iter_get_arg_type (*pdbus_message_iter_get_arg_type)
+#define dbus_message_iter_get_basic (*pdbus_message_iter_get_basic)
+#define dbus_set_error_from_message (*pdbus_set_error_from_message)
+#endif
+
+void PrepareDBus();
+
+inline auto HasDBus()
+{
+    static std::once_flag init_dbus{};
+    std::call_once(init_dbus, []{ PrepareDBus(); });
+    return dbus_handle;
+}
+
+#else
+
+constexpr bool HasDBus() noexcept { return true; }
+#endif /* HAVE_DYNLOAD */
+
+
+namespace dbus {
+
+struct Error {
+    Error() { dbus_error_init(&mError); }
+    ~Error() { dbus_error_free(&mError); }
+    DBusError* operator->() { return &mError; }
+    DBusError &get() { return mError; }
+private:
+    DBusError mError{};
+};
+
+struct ConnectionDeleter {
+    void operator()(DBusConnection *c) { dbus_connection_unref(c); }
+};
+using ConnectionPtr = std::unique_ptr<DBusConnection,ConnectionDeleter>;
+
+} // namespace dbus
+
+#endif /* CORE_DBUS_WRAP_H */
diff --git a/core/devformat.cpp b/core/devformat.cpp
new file mode 100644 (file)
index 0000000..acdabc4
--- /dev/null
@@ -0,0 +1,67 @@
+
+#include "config.h"
+
+#include "devformat.h"
+
+
+uint BytesFromDevFmt(DevFmtType type) noexcept
+{
+    switch(type)
+    {
+    case DevFmtByte: return sizeof(int8_t);
+    case DevFmtUByte: return sizeof(uint8_t);
+    case DevFmtShort: return sizeof(int16_t);
+    case DevFmtUShort: return sizeof(uint16_t);
+    case DevFmtInt: return sizeof(int32_t);
+    case DevFmtUInt: return sizeof(uint32_t);
+    case DevFmtFloat: return sizeof(float);
+    }
+    return 0;
+}
+uint ChannelsFromDevFmt(DevFmtChannels chans, uint ambiorder) noexcept
+{
+    switch(chans)
+    {
+    case DevFmtMono: return 1;
+    case DevFmtStereo: return 2;
+    case DevFmtQuad: return 4;
+    case DevFmtX51: return 6;
+    case DevFmtX61: return 7;
+    case DevFmtX71: return 8;
+    case DevFmtX714: return 12;
+    case DevFmtX3D71: return 8;
+    case DevFmtAmbi3D: return (ambiorder+1) * (ambiorder+1);
+    }
+    return 0;
+}
+
+const char *DevFmtTypeString(DevFmtType type) noexcept
+{
+    switch(type)
+    {
+    case DevFmtByte: return "Int8";
+    case DevFmtUByte: return "UInt8";
+    case DevFmtShort: return "Int16";
+    case DevFmtUShort: return "UInt16";
+    case DevFmtInt: return "Int32";
+    case DevFmtUInt: return "UInt32";
+    case DevFmtFloat: return "Float32";
+    }
+    return "(unknown type)";
+}
+const char *DevFmtChannelsString(DevFmtChannels chans) noexcept
+{
+    switch(chans)
+    {
+    case DevFmtMono: return "Mono";
+    case DevFmtStereo: return "Stereo";
+    case DevFmtQuad: return "Quadraphonic";
+    case DevFmtX51: return "5.1 Surround";
+    case DevFmtX61: return "6.1 Surround";
+    case DevFmtX71: return "7.1 Surround";
+    case DevFmtX714: return "7.1.4 Surround";
+    case DevFmtX3D71: return "3D7.1 Surround";
+    case DevFmtAmbi3D: return "Ambisonic 3D";
+    }
+    return "(unknown channels)";
+}
diff --git a/core/devformat.h b/core/devformat.h
new file mode 100644 (file)
index 0000000..485826a
--- /dev/null
@@ -0,0 +1,122 @@
+#ifndef CORE_DEVFORMAT_H
+#define CORE_DEVFORMAT_H
+
+#include <cstdint>
+
+
+using uint = unsigned int;
+
+enum Channel : unsigned char {
+    FrontLeft = 0,
+    FrontRight,
+    FrontCenter,
+    LFE,
+    BackLeft,
+    BackRight,
+    BackCenter,
+    SideLeft,
+    SideRight,
+
+    TopCenter,
+    TopFrontLeft,
+    TopFrontCenter,
+    TopFrontRight,
+    TopBackLeft,
+    TopBackCenter,
+    TopBackRight,
+
+    Aux0,
+    Aux1,
+    Aux2,
+    Aux3,
+    Aux4,
+    Aux5,
+    Aux6,
+    Aux7,
+    Aux8,
+    Aux9,
+    Aux10,
+    Aux11,
+    Aux12,
+    Aux13,
+    Aux14,
+    Aux15,
+
+    MaxChannels
+};
+
+
+/* Device formats */
+enum DevFmtType : unsigned char {
+    DevFmtByte,
+    DevFmtUByte,
+    DevFmtShort,
+    DevFmtUShort,
+    DevFmtInt,
+    DevFmtUInt,
+    DevFmtFloat,
+
+    DevFmtTypeDefault = DevFmtFloat
+};
+enum DevFmtChannels : unsigned char {
+    DevFmtMono,
+    DevFmtStereo,
+    DevFmtQuad,
+    DevFmtX51,
+    DevFmtX61,
+    DevFmtX71,
+    DevFmtX714,
+    DevFmtX3D71,
+    DevFmtAmbi3D,
+
+    DevFmtChannelsDefault = DevFmtStereo
+};
+#define MAX_OUTPUT_CHANNELS  16
+
+/* DevFmtType traits, providing the type, etc given a DevFmtType. */
+template<DevFmtType T>
+struct DevFmtTypeTraits { };
+
+template<>
+struct DevFmtTypeTraits<DevFmtByte> { using Type = int8_t; };
+template<>
+struct DevFmtTypeTraits<DevFmtUByte> { using Type = uint8_t; };
+template<>
+struct DevFmtTypeTraits<DevFmtShort> { using Type = int16_t; };
+template<>
+struct DevFmtTypeTraits<DevFmtUShort> { using Type = uint16_t; };
+template<>
+struct DevFmtTypeTraits<DevFmtInt> { using Type = int32_t; };
+template<>
+struct DevFmtTypeTraits<DevFmtUInt> { using Type = uint32_t; };
+template<>
+struct DevFmtTypeTraits<DevFmtFloat> { using Type = float; };
+
+template<DevFmtType T>
+using DevFmtType_t = typename DevFmtTypeTraits<T>::Type;
+
+
+uint BytesFromDevFmt(DevFmtType type) noexcept;
+uint ChannelsFromDevFmt(DevFmtChannels chans, uint ambiorder) noexcept;
+inline uint FrameSizeFromDevFmt(DevFmtChannels chans, DevFmtType type, uint ambiorder) noexcept
+{ return ChannelsFromDevFmt(chans, ambiorder) * BytesFromDevFmt(type); }
+
+const char *DevFmtTypeString(DevFmtType type) noexcept;
+const char *DevFmtChannelsString(DevFmtChannels chans) noexcept;
+
+enum class DevAmbiLayout : bool {
+    FuMa,
+    ACN,
+
+    Default = ACN
+};
+
+enum class DevAmbiScaling : unsigned char {
+    FuMa,
+    SN3D,
+    N3D,
+
+    Default = SN3D
+};
+
+#endif /* CORE_DEVFORMAT_H */
diff --git a/core/device.cpp b/core/device.cpp
new file mode 100644 (file)
index 0000000..2766c5e
--- /dev/null
@@ -0,0 +1,23 @@
+
+#include "config.h"
+
+#include "bformatdec.h"
+#include "bs2b.h"
+#include "device.h"
+#include "front_stablizer.h"
+#include "hrtf.h"
+#include "mastering.h"
+
+
+al::FlexArray<ContextBase*> DeviceBase::sEmptyContextArray{0u};
+
+
+DeviceBase::DeviceBase(DeviceType type) : Type{type}, mContexts{&sEmptyContextArray}
+{
+}
+
+DeviceBase::~DeviceBase()
+{
+    auto *oldarray = mContexts.exchange(nullptr, std::memory_order_relaxed);
+    if(oldarray != &sEmptyContextArray) delete oldarray;
+}
diff --git a/core/device.h b/core/device.h
new file mode 100644 (file)
index 0000000..9aaf7ad
--- /dev/null
@@ -0,0 +1,345 @@
+#ifndef CORE_DEVICE_H
+#define CORE_DEVICE_H
+
+#include <stddef.h>
+
+#include <array>
+#include <atomic>
+#include <bitset>
+#include <chrono>
+#include <memory>
+#include <mutex>
+#include <string>
+
+#include "almalloc.h"
+#include "alspan.h"
+#include "ambidefs.h"
+#include "atomic.h"
+#include "bufferline.h"
+#include "devformat.h"
+#include "filters/nfc.h"
+#include "intrusive_ptr.h"
+#include "mixer/hrtfdefs.h"
+#include "opthelpers.h"
+#include "resampler_limits.h"
+#include "uhjfilter.h"
+#include "vector.h"
+
+class BFormatDec;
+struct bs2b;
+struct Compressor;
+struct ContextBase;
+struct DirectHrtfState;
+struct HrtfStore;
+
+using uint = unsigned int;
+
+
+#define MIN_OUTPUT_RATE      8000
+#define MAX_OUTPUT_RATE      192000
+#define DEFAULT_OUTPUT_RATE  48000
+
+#define DEFAULT_UPDATE_SIZE  960 /* 20ms */
+#define DEFAULT_NUM_UPDATES  3
+
+
+enum class DeviceType : unsigned char {
+    Playback,
+    Capture,
+    Loopback
+};
+
+
+enum class RenderMode : unsigned char {
+    Normal,
+    Pairwise,
+    Hrtf
+};
+
+enum class StereoEncoding : unsigned char {
+    Basic,
+    Uhj,
+    Hrtf,
+
+    Default = Basic
+};
+
+
+struct InputRemixMap {
+    struct TargetMix { Channel channel; float mix; };
+
+    Channel channel;
+    al::span<const TargetMix> targets;
+};
+
+
+struct DistanceComp {
+    /* Maximum delay in samples for speaker distance compensation. */
+    static constexpr uint MaxDelay{1024};
+
+    struct ChanData {
+        float Gain{1.0f};
+        uint Length{0u}; /* Valid range is [0...MaxDelay). */
+        float *Buffer{nullptr};
+    };
+
+    std::array<ChanData,MAX_OUTPUT_CHANNELS> mChannels;
+    al::FlexArray<float,16> mSamples;
+
+    DistanceComp(size_t count) : mSamples{count} { }
+
+    static std::unique_ptr<DistanceComp> Create(size_t numsamples)
+    { return std::unique_ptr<DistanceComp>{new(FamCount(numsamples)) DistanceComp{numsamples}}; }
+
+    DEF_FAM_NEWDEL(DistanceComp, mSamples)
+};
+
+
+constexpr uint InvalidChannelIndex{~0u};
+
+struct BFChannelConfig {
+    float Scale;
+    uint Index;
+};
+
+struct MixParams {
+    /* Coefficient channel mapping for mixing to the buffer. */
+    std::array<BFChannelConfig,MaxAmbiChannels> AmbiMap{};
+
+    al::span<FloatBufferLine> Buffer;
+
+    /**
+     * Helper to set an identity/pass-through panning for ambisonic mixing. The
+     * source is expected to be a 3D ACN/N3D ambisonic buffer, and for each
+     * channel [0...count), the given functor is called with the source channel
+     * index, destination channel index, and the gain for that channel. If the
+     * destination channel is INVALID_CHANNEL_INDEX, the given source channel
+     * is not used for output.
+     */
+    template<typename F>
+    void setAmbiMixParams(const MixParams &inmix, const float gainbase, F func) const
+    {
+        const size_t numIn{inmix.Buffer.size()};
+        const size_t numOut{Buffer.size()};
+        for(size_t i{0};i < numIn;++i)
+        {
+            auto idx = InvalidChannelIndex;
+            auto gain = 0.0f;
+
+            for(size_t j{0};j < numOut;++j)
+            {
+                if(AmbiMap[j].Index == inmix.AmbiMap[i].Index)
+                {
+                    idx = static_cast<uint>(j);
+                    gain = AmbiMap[j].Scale * gainbase;
+                    break;
+                }
+            }
+            func(i, idx, gain);
+        }
+    }
+};
+
+struct RealMixParams {
+    al::span<const InputRemixMap> RemixMap;
+    std::array<uint,MaxChannels> ChannelIndex{};
+
+    al::span<FloatBufferLine> Buffer;
+};
+
+using AmbiRotateMatrix = std::array<std::array<float,MaxAmbiChannels>,MaxAmbiChannels>;
+
+enum {
+    // Frequency was requested by the app or config file
+    FrequencyRequest,
+    // Channel configuration was requested by the app or config file
+    ChannelsRequest,
+    // Sample type was requested by the config file
+    SampleTypeRequest,
+
+    // Specifies if the DSP is paused at user request
+    DevicePaused,
+    // Specifies if the device is currently running
+    DeviceRunning,
+
+    // Specifies if the output plays directly on/in ears (headphones, headset,
+    // ear buds, etc).
+    DirectEar,
+
+    DeviceFlagsCount
+};
+
+struct DeviceBase {
+    /* To avoid extraneous allocations, a 0-sized FlexArray<ContextBase*> is
+     * defined globally as a sharable object.
+     */
+    static al::FlexArray<ContextBase*> sEmptyContextArray;
+
+    std::atomic<bool> Connected{true};
+    const DeviceType Type{};
+
+    uint Frequency{};
+    uint UpdateSize{};
+    uint BufferSize{};
+
+    DevFmtChannels FmtChans{};
+    DevFmtType FmtType{};
+    uint mAmbiOrder{0};
+    float mXOverFreq{400.0f};
+    /* If the main device mix is horizontal/2D only. */
+    bool m2DMixing{false};
+    /* For DevFmtAmbi* output only, specifies the channel order and
+     * normalization.
+     */
+    DevAmbiLayout mAmbiLayout{DevAmbiLayout::Default};
+    DevAmbiScaling mAmbiScale{DevAmbiScaling::Default};
+
+    std::string DeviceName;
+
+    // Device flags
+    std::bitset<DeviceFlagsCount> Flags{};
+
+    uint NumAuxSends{};
+
+    /* Rendering mode. */
+    RenderMode mRenderMode{RenderMode::Normal};
+
+    /* The average speaker distance as determined by the ambdec configuration,
+     * HRTF data set, or the NFC-HOA reference delay. Only used for NFC.
+     */
+    float AvgSpeakerDist{0.0f};
+
+    /* The default NFC filter. Not used directly, but is pre-initialized with
+     * the control distance from AvgSpeakerDist.
+     */
+    NfcFilter mNFCtrlFilter{};
+
+    uint SamplesDone{0u};
+    std::chrono::nanoseconds ClockBase{0};
+    std::chrono::nanoseconds FixedLatency{0};
+
+    AmbiRotateMatrix mAmbiRotateMatrix{};
+    AmbiRotateMatrix mAmbiRotateMatrix2{};
+
+    /* Temp storage used for mixer processing. */
+    static constexpr size_t MixerLineSize{BufferLineSize + DecoderBase::sMaxPadding};
+    static constexpr size_t MixerChannelsMax{16};
+    using MixerBufferLine = std::array<float,MixerLineSize>;
+    alignas(16) std::array<MixerBufferLine,MixerChannelsMax> mSampleData;
+    alignas(16) std::array<float,MixerLineSize+MaxResamplerPadding> mResampleData;
+
+    alignas(16) float FilteredData[BufferLineSize];
+    union {
+        alignas(16) float HrtfSourceData[BufferLineSize + HrtfHistoryLength];
+        alignas(16) float NfcSampleData[BufferLineSize];
+    };
+
+    /* Persistent storage for HRTF mixing. */
+    alignas(16) float2 HrtfAccumData[BufferLineSize + HrirLength];
+
+    /* Mixing buffer used by the Dry mix and Real output. */
+    al::vector<FloatBufferLine, 16> MixBuffer;
+
+    /* The "dry" path corresponds to the main output. */
+    MixParams Dry;
+    uint NumChannelsPerOrder[MaxAmbiOrder+1]{};
+
+    /* "Real" output, which will be written to the device buffer. May alias the
+     * dry buffer.
+     */
+    RealMixParams RealOut;
+
+    /* HRTF state and info */
+    std::unique_ptr<DirectHrtfState> mHrtfState;
+    al::intrusive_ptr<HrtfStore> mHrtf;
+    uint mIrSize{0};
+
+    /* Ambisonic-to-UHJ encoder */
+    std::unique_ptr<UhjEncoderBase> mUhjEncoder;
+
+    /* Ambisonic decoder for speakers */
+    std::unique_ptr<BFormatDec> AmbiDecoder;
+
+    /* Stereo-to-binaural filter */
+    std::unique_ptr<bs2b> Bs2b;
+
+    using PostProc = void(DeviceBase::*)(const size_t SamplesToDo);
+    PostProc PostProcess{nullptr};
+
+    std::unique_ptr<Compressor> Limiter;
+
+    /* Delay buffers used to compensate for speaker distances. */
+    std::unique_ptr<DistanceComp> ChannelDelays;
+
+    /* Dithering control. */
+    float DitherDepth{0.0f};
+    uint DitherSeed{0u};
+
+    /* Running count of the mixer invocations, in 31.1 fixed point. This
+     * actually increments *twice* when mixing, first at the start and then at
+     * the end, so the bottom bit indicates if the device is currently mixing
+     * and the upper bits indicates how many mixes have been done.
+     */
+    RefCount MixCount{0u};
+
+    // Contexts created on this device
+    std::atomic<al::FlexArray<ContextBase*>*> mContexts{nullptr};
+
+
+    DeviceBase(DeviceType type);
+    DeviceBase(const DeviceBase&) = delete;
+    DeviceBase& operator=(const DeviceBase&) = delete;
+    ~DeviceBase();
+
+    uint bytesFromFmt() const noexcept { return BytesFromDevFmt(FmtType); }
+    uint channelsFromFmt() const noexcept { return ChannelsFromDevFmt(FmtChans, mAmbiOrder); }
+    uint frameSizeFromFmt() const noexcept { return bytesFromFmt() * channelsFromFmt(); }
+
+    uint waitForMix() const noexcept
+    {
+        uint refcount;
+        while((refcount=MixCount.load(std::memory_order_acquire))&1) {
+        }
+        return refcount;
+    }
+
+    void ProcessHrtf(const size_t SamplesToDo);
+    void ProcessAmbiDec(const size_t SamplesToDo);
+    void ProcessAmbiDecStablized(const size_t SamplesToDo);
+    void ProcessUhj(const size_t SamplesToDo);
+    void ProcessBs2b(const size_t SamplesToDo);
+
+    inline void postProcess(const size_t SamplesToDo)
+    { if(PostProcess) LIKELY (this->*PostProcess)(SamplesToDo); }
+
+    void renderSamples(const al::span<float*> outBuffers, const uint numSamples);
+    void renderSamples(void *outBuffer, const uint numSamples, const size_t frameStep);
+
+    /* Caller must lock the device state, and the mixer must not be running. */
+#ifdef __USE_MINGW_ANSI_STDIO
+    [[gnu::format(gnu_printf,2,3)]]
+#else
+    [[gnu::format(printf,2,3)]]
+#endif
+    void handleDisconnect(const char *msg, ...);
+
+    /**
+     * Returns the index for the given channel name (e.g. FrontCenter), or
+     * INVALID_CHANNEL_INDEX if it doesn't exist.
+     */
+    uint channelIdxByName(Channel chan) const noexcept
+    { return RealOut.ChannelIndex[chan]; }
+
+    DISABLE_ALLOC()
+
+private:
+    uint renderSamples(const uint numSamples);
+};
+
+/* Must be less than 15 characters (16 including terminating null) for
+ * compatibility with pthread_setname_np limitations. */
+#define MIXER_THREAD_NAME "alsoft-mixer"
+
+#define RECORD_THREAD_NAME "alsoft-record"
+
+#endif /* CORE_DEVICE_H */
diff --git a/core/effects/base.h b/core/effects/base.h
new file mode 100644 (file)
index 0000000..4ee19f3
--- /dev/null
@@ -0,0 +1,197 @@
+#ifndef CORE_EFFECTS_BASE_H
+#define CORE_EFFECTS_BASE_H
+
+#include <stddef.h>
+
+#include "albyte.h"
+#include "almalloc.h"
+#include "alspan.h"
+#include "atomic.h"
+#include "core/bufferline.h"
+#include "intrusive_ptr.h"
+
+struct BufferStorage;
+struct ContextBase;
+struct DeviceBase;
+struct EffectSlot;
+struct MixParams;
+struct RealMixParams;
+
+
+/** Target gain for the reverb decay feedback reaching the decay time. */
+constexpr float ReverbDecayGain{0.001f}; /* -60 dB */
+
+constexpr float ReverbMaxReflectionsDelay{0.3f};
+constexpr float ReverbMaxLateReverbDelay{0.1f};
+
+enum class ChorusWaveform {
+    Sinusoid,
+    Triangle
+};
+
+constexpr float ChorusMaxDelay{0.016f};
+constexpr float FlangerMaxDelay{0.004f};
+
+constexpr float EchoMaxDelay{0.207f};
+constexpr float EchoMaxLRDelay{0.404f};
+
+enum class FShifterDirection {
+    Down,
+    Up,
+    Off
+};
+
+enum class ModulatorWaveform {
+    Sinusoid,
+    Sawtooth,
+    Square
+};
+
+enum class VMorpherPhenome {
+    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 class VMorpherWaveform {
+    Sinusoid,
+    Triangle,
+    Sawtooth
+};
+
+union EffectProps {
+    struct {
+        float Density;
+        float Diffusion;
+        float Gain;
+        float GainHF;
+        float GainLF;
+        float DecayTime;
+        float DecayHFRatio;
+        float DecayLFRatio;
+        float ReflectionsGain;
+        float ReflectionsDelay;
+        float ReflectionsPan[3];
+        float LateReverbGain;
+        float LateReverbDelay;
+        float LateReverbPan[3];
+        float EchoTime;
+        float EchoDepth;
+        float ModulationTime;
+        float ModulationDepth;
+        float AirAbsorptionGainHF;
+        float HFReference;
+        float LFReference;
+        float RoomRolloffFactor;
+        bool DecayHFLimit;
+    } Reverb;
+
+    struct {
+        float AttackTime;
+        float ReleaseTime;
+        float Resonance;
+        float PeakGain;
+    } Autowah;
+
+    struct {
+        ChorusWaveform Waveform;
+        int Phase;
+        float Rate;
+        float Depth;
+        float Feedback;
+        float Delay;
+    } Chorus; /* Also Flanger */
+
+    struct {
+        bool OnOff;
+    } Compressor;
+
+    struct {
+        float Edge;
+        float Gain;
+        float LowpassCutoff;
+        float EQCenter;
+        float EQBandwidth;
+    } Distortion;
+
+    struct {
+        float Delay;
+        float LRDelay;
+
+        float Damping;
+        float Feedback;
+
+        float Spread;
+    } Echo;
+
+    struct {
+        float LowCutoff;
+        float LowGain;
+        float Mid1Center;
+        float Mid1Gain;
+        float Mid1Width;
+        float Mid2Center;
+        float Mid2Gain;
+        float Mid2Width;
+        float HighCutoff;
+        float HighGain;
+    } Equalizer;
+
+    struct {
+        float Frequency;
+        FShifterDirection LeftDirection;
+        FShifterDirection RightDirection;
+    } Fshifter;
+
+    struct {
+        float Frequency;
+        float HighPassCutoff;
+        ModulatorWaveform Waveform;
+    } Modulator;
+
+    struct {
+        int CoarseTune;
+        int FineTune;
+    } Pshifter;
+
+    struct {
+        float Rate;
+        VMorpherPhenome PhonemeA;
+        VMorpherPhenome PhonemeB;
+        int PhonemeACoarseTuning;
+        int PhonemeBCoarseTuning;
+        VMorpherWaveform Waveform;
+    } Vmorpher;
+
+    struct {
+        float Gain;
+    } Dedicated;
+};
+
+
+struct EffectTarget {
+    MixParams *Main;
+    RealMixParams *RealOut;
+};
+
+struct EffectState : public al::intrusive_ref<EffectState> {
+    al::span<FloatBufferLine> mOutTarget;
+
+
+    virtual ~EffectState() = default;
+
+    virtual void deviceUpdate(const DeviceBase *device, const BufferStorage *buffer) = 0;
+    virtual void update(const ContextBase *context, const EffectSlot *slot,
+        const EffectProps *props, const EffectTarget target) = 0;
+    virtual void process(const size_t samplesToDo, const al::span<const FloatBufferLine> samplesIn,
+        const al::span<FloatBufferLine> samplesOut) = 0;
+};
+
+
+struct EffectStateFactory {
+    virtual ~EffectStateFactory() = default;
+
+    virtual al::intrusive_ptr<EffectState> create() = 0;
+};
+
+#endif /* CORE_EFFECTS_BASE_H */
diff --git a/core/effectslot.cpp b/core/effectslot.cpp
new file mode 100644 (file)
index 0000000..db8aa07
--- /dev/null
@@ -0,0 +1,19 @@
+
+#include "config.h"
+
+#include "effectslot.h"
+
+#include <stddef.h>
+
+#include "almalloc.h"
+#include "context.h"
+
+
+EffectSlotArray *EffectSlot::CreatePtrArray(size_t count) noexcept
+{
+    /* Allocate space for twice as many pointers, so the mixer has scratch
+     * space to store a sorted list during mixing.
+     */
+    void *ptr{al_calloc(alignof(EffectSlotArray), EffectSlotArray::Sizeof(count*2))};
+    return al::construct_at(static_cast<EffectSlotArray*>(ptr), count);
+}
diff --git a/core/effectslot.h b/core/effectslot.h
new file mode 100644 (file)
index 0000000..2624ae5
--- /dev/null
@@ -0,0 +1,89 @@
+#ifndef CORE_EFFECTSLOT_H
+#define CORE_EFFECTSLOT_H
+
+#include <atomic>
+
+#include "almalloc.h"
+#include "device.h"
+#include "effects/base.h"
+#include "intrusive_ptr.h"
+
+struct EffectSlot;
+struct WetBuffer;
+
+using EffectSlotArray = al::FlexArray<EffectSlot*>;
+
+
+enum class EffectSlotType : unsigned char {
+    None,
+    Reverb,
+    Chorus,
+    Distortion,
+    Echo,
+    Flanger,
+    FrequencyShifter,
+    VocalMorpher,
+    PitchShifter,
+    RingModulator,
+    Autowah,
+    Compressor,
+    Equalizer,
+    EAXReverb,
+    DedicatedLFE,
+    DedicatedDialog,
+    Convolution
+};
+
+struct EffectSlotProps {
+    float Gain;
+    bool  AuxSendAuto;
+    EffectSlot *Target;
+
+    EffectSlotType Type;
+    EffectProps Props;
+
+    al::intrusive_ptr<EffectState> State;
+
+    std::atomic<EffectSlotProps*> next;
+
+    DEF_NEWDEL(EffectSlotProps)
+};
+
+
+struct EffectSlot {
+    bool InUse{false};
+
+    std::atomic<EffectSlotProps*> Update{nullptr};
+
+    /* Wet buffer configuration is ACN channel order with N3D scaling.
+     * Consequently, effects that only want to work with mono input can use
+     * channel 0 by itself. Effects that want multichannel can process the
+     * ambisonics signal and make a B-Format source pan.
+     */
+    MixParams Wet;
+
+    float Gain{1.0f};
+    bool  AuxSendAuto{true};
+    EffectSlot *Target{nullptr};
+
+    EffectSlotType EffectType{EffectSlotType::None};
+    EffectProps mEffectProps{};
+    al::intrusive_ptr<EffectState> mEffectState;
+
+    float RoomRolloff{0.0f}; /* Added to the source's room rolloff, not multiplied. */
+    float DecayTime{0.0f};
+    float DecayLFRatio{0.0f};
+    float DecayHFRatio{0.0f};
+    bool DecayHFLimit{false};
+    float AirAbsorptionGainHF{1.0f};
+
+    /* Mixing buffer used by the Wet mix. */
+    al::vector<FloatBufferLine,16> mWetBuffer;
+
+
+    static EffectSlotArray *CreatePtrArray(size_t count) noexcept;
+
+    DEF_NEWDEL(EffectSlot)
+};
+
+#endif /* CORE_EFFECTSLOT_H */
diff --git a/core/except.cpp b/core/except.cpp
new file mode 100644 (file)
index 0000000..45fd4eb
--- /dev/null
@@ -0,0 +1,30 @@
+
+#include "config.h"
+
+#include "except.h"
+
+#include <cstdio>
+#include <cstdarg>
+
+#include "opthelpers.h"
+
+
+namespace al {
+
+base_exception::~base_exception() = default;
+
+void base_exception::setMessage(const char* msg, std::va_list args)
+{
+    std::va_list args2;
+    va_copy(args2, args);
+    int msglen{std::vsnprintf(nullptr, 0, msg, args)};
+    if(msglen > 0) LIKELY
+    {
+        mMessage.resize(static_cast<size_t>(msglen)+1);
+        std::vsnprintf(const_cast<char*>(mMessage.data()), mMessage.length(), msg, args2);
+        mMessage.pop_back();
+    }
+    va_end(args2);
+}
+
+} // namespace al
diff --git a/core/except.h b/core/except.h
new file mode 100644 (file)
index 0000000..0e28e9d
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef CORE_EXCEPT_H
+#define CORE_EXCEPT_H
+
+#include <cstdarg>
+#include <exception>
+#include <string>
+#include <utility>
+
+
+namespace al {
+
+class base_exception : public std::exception {
+    std::string mMessage;
+
+protected:
+    base_exception() = default;
+    virtual ~base_exception();
+
+    void setMessage(const char *msg, std::va_list args);
+
+public:
+    const char *what() const noexcept override { return mMessage.c_str(); }
+};
+
+} // namespace al
+
+#define START_API_FUNC try
+
+#define END_API_FUNC catch(...) { std::terminate(); }
+
+#endif /* CORE_EXCEPT_H */
diff --git a/core/filters/biquad.cpp b/core/filters/biquad.cpp
new file mode 100644 (file)
index 0000000..a0a62eb
--- /dev/null
@@ -0,0 +1,168 @@
+
+#include "config.h"
+
+#include "biquad.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+
+#include "alnumbers.h"
+#include "opthelpers.h"
+
+
+template<typename Real>
+void BiquadFilterR<Real>::setParams(BiquadType type, Real f0norm, Real gain, Real rcpQ)
+{
+    /* HACK: Limit gain to -100dB. This shouldn't ever happen, all callers
+     * already clamp to minimum of 0.001, or have a limited range of values
+     * that don't go below 0.126. But it seems to with some callers. This needs
+     * to be investigated.
+     */
+    gain = std::max(gain, Real(0.00001));
+
+    const Real w0{al::numbers::pi_v<Real>*2.0f * f0norm};
+    const Real sin_w0{std::sin(w0)};
+    const Real cos_w0{std::cos(w0)};
+    const Real alpha{sin_w0/2.0f * rcpQ};
+
+    Real sqrtgain_alpha_2;
+    Real a[3]{ 1.0f, 0.0f, 0.0f };
+    Real b[3]{ 1.0f, 0.0f, 0.0f };
+
+    /* Calculate filter coefficients depending on filter type */
+    switch(type)
+    {
+        case BiquadType::HighShelf:
+            sqrtgain_alpha_2 = 2.0f * std::sqrt(gain) * alpha;
+            b[0] =       gain*((gain+1.0f) + (gain-1.0f)*cos_w0 + sqrtgain_alpha_2);
+            b[1] = -2.0f*gain*((gain-1.0f) + (gain+1.0f)*cos_w0                   );
+            b[2] =       gain*((gain+1.0f) + (gain-1.0f)*cos_w0 - sqrtgain_alpha_2);
+            a[0] =             (gain+1.0f) - (gain-1.0f)*cos_w0 + sqrtgain_alpha_2;
+            a[1] =  2.0f*     ((gain-1.0f) - (gain+1.0f)*cos_w0                   );
+            a[2] =             (gain+1.0f) - (gain-1.0f)*cos_w0 - sqrtgain_alpha_2;
+            break;
+        case BiquadType::LowShelf:
+            sqrtgain_alpha_2 = 2.0f * std::sqrt(gain) * alpha;
+            b[0] =       gain*((gain+1.0f) - (gain-1.0f)*cos_w0 + sqrtgain_alpha_2);
+            b[1] =  2.0f*gain*((gain-1.0f) - (gain+1.0f)*cos_w0                   );
+            b[2] =       gain*((gain+1.0f) - (gain-1.0f)*cos_w0 - sqrtgain_alpha_2);
+            a[0] =             (gain+1.0f) + (gain-1.0f)*cos_w0 + sqrtgain_alpha_2;
+            a[1] = -2.0f*     ((gain-1.0f) + (gain+1.0f)*cos_w0                   );
+            a[2] =             (gain+1.0f) + (gain-1.0f)*cos_w0 - sqrtgain_alpha_2;
+            break;
+        case BiquadType::Peaking:
+            b[0] =  1.0f + alpha * gain;
+            b[1] = -2.0f * cos_w0;
+            b[2] =  1.0f - alpha * gain;
+            a[0] =  1.0f + alpha / gain;
+            a[1] = -2.0f * cos_w0;
+            a[2] =  1.0f - alpha / gain;
+            break;
+
+        case BiquadType::LowPass:
+            b[0] = (1.0f - cos_w0) / 2.0f;
+            b[1] =  1.0f - cos_w0;
+            b[2] = (1.0f - cos_w0) / 2.0f;
+            a[0] =  1.0f + alpha;
+            a[1] = -2.0f * cos_w0;
+            a[2] =  1.0f - alpha;
+            break;
+        case BiquadType::HighPass:
+            b[0] =  (1.0f + cos_w0) / 2.0f;
+            b[1] = -(1.0f + cos_w0);
+            b[2] =  (1.0f + cos_w0) / 2.0f;
+            a[0] =   1.0f + alpha;
+            a[1] =  -2.0f * cos_w0;
+            a[2] =   1.0f - alpha;
+            break;
+        case BiquadType::BandPass:
+            b[0] =  alpha;
+            b[1] =  0.0f;
+            b[2] = -alpha;
+            a[0] =  1.0f + alpha;
+            a[1] = -2.0f * cos_w0;
+            a[2] =  1.0f - alpha;
+            break;
+    }
+
+    mA1 = a[1] / a[0];
+    mA2 = a[2] / a[0];
+    mB0 = b[0] / a[0];
+    mB1 = b[1] / a[0];
+    mB2 = b[2] / a[0];
+}
+
+template<typename Real>
+void BiquadFilterR<Real>::process(const al::span<const Real> src, Real *dst)
+{
+    const Real b0{mB0};
+    const Real b1{mB1};
+    const Real b2{mB2};
+    const Real a1{mA1};
+    const Real a2{mA2};
+    Real z1{mZ1};
+    Real z2{mZ2};
+
+    /* Processing loop is Transposed Direct Form II. This requires less storage
+     * compared to Direct Form I (only two delay components, instead of a four-
+     * sample history; the last two inputs and outputs), and works better for
+     * floating-point which favors summing similarly-sized values while being
+     * less bothered by overflow.
+     *
+     * See: http://www.earlevel.com/main/2003/02/28/biquads/
+     */
+    auto proc_sample = [b0,b1,b2,a1,a2,&z1,&z2](Real input) noexcept -> Real
+    {
+        const Real output{input*b0 + z1};
+        z1 = input*b1 - output*a1 + z2;
+        z2 = input*b2 - output*a2;
+        return output;
+    };
+    std::transform(src.cbegin(), src.cend(), dst, proc_sample);
+
+    mZ1 = z1;
+    mZ2 = z2;
+}
+
+template<typename Real>
+void BiquadFilterR<Real>::dualProcess(BiquadFilterR &other, const al::span<const Real> src,
+    Real *dst)
+{
+    const Real b00{mB0};
+    const Real b01{mB1};
+    const Real b02{mB2};
+    const Real a01{mA1};
+    const Real a02{mA2};
+    const Real b10{other.mB0};
+    const Real b11{other.mB1};
+    const Real b12{other.mB2};
+    const Real a11{other.mA1};
+    const Real a12{other.mA2};
+    Real z01{mZ1};
+    Real z02{mZ2};
+    Real z11{other.mZ1};
+    Real z12{other.mZ2};
+
+    auto proc_sample = [b00,b01,b02,a01,a02,b10,b11,b12,a11,a12,&z01,&z02,&z11,&z12](Real input) noexcept -> Real
+    {
+        const Real tmpout{input*b00 + z01};
+        z01 = input*b01 - tmpout*a01 + z02;
+        z02 = input*b02 - tmpout*a02;
+        input = tmpout;
+
+        const Real output{input*b10 + z11};
+        z11 = input*b11 - output*a11 + z12;
+        z12 = input*b12 - output*a12;
+        return output;
+    };
+    std::transform(src.cbegin(), src.cend(), dst, proc_sample);
+
+    mZ1 = z01;
+    mZ2 = z02;
+    other.mZ1 = z11;
+    other.mZ2 = z12;
+}
+
+template class BiquadFilterR<float>;
+template class BiquadFilterR<double>;
diff --git a/core/filters/biquad.h b/core/filters/biquad.h
new file mode 100644 (file)
index 0000000..75a4009
--- /dev/null
@@ -0,0 +1,144 @@
+#ifndef CORE_FILTERS_BIQUAD_H
+#define CORE_FILTERS_BIQUAD_H
+
+#include <algorithm>
+#include <cmath>
+#include <cstddef>
+#include <utility>
+
+#include "alnumbers.h"
+#include "alspan.h"
+
+
+/* Filters implementation is based on the "Cookbook formulae for audio
+ * EQ biquad filter coefficients" by Robert Bristow-Johnson
+ * http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
+ */
+/* Implementation note: For the shelf and peaking filters, the specified gain
+ * is for the centerpoint of the transition band. This better fits EFX filter
+ * behavior, which expects the shelf's reference frequency to reach the given
+ * gain. To set the gain for the shelf or peak itself, use the square root of
+ * the desired linear gain (or halve the dB gain).
+ */
+
+enum class BiquadType {
+    /** EFX-style low-pass filter, specifying a gain and reference frequency. */
+    HighShelf,
+    /** EFX-style high-pass filter, specifying a gain and reference frequency. */
+    LowShelf,
+    /** Peaking filter, specifying a gain and reference frequency. */
+    Peaking,
+
+    /** Low-pass cut-off filter, specifying a cut-off frequency. */
+    LowPass,
+    /** High-pass cut-off filter, specifying a cut-off frequency. */
+    HighPass,
+    /** Band-pass filter, specifying a center frequency. */
+    BandPass,
+};
+
+template<typename Real>
+class BiquadFilterR {
+    /* Last two delayed components for direct form II. */
+    Real mZ1{0}, mZ2{0};
+    /* Transfer function coefficients "b" (numerator) */
+    Real mB0{1}, mB1{0}, mB2{0};
+    /* Transfer function coefficients "a" (denominator; a0 is pre-applied). */
+    Real mA1{0}, mA2{0};
+
+    void setParams(BiquadType type, Real f0norm, Real gain, Real rcpQ);
+
+    /**
+     * Calculates the rcpQ (i.e. 1/Q) coefficient for shelving filters, using
+     * the reference gain and shelf slope parameter.
+     * \param gain 0 < gain
+     * \param slope 0 < slope <= 1
+     */
+    static Real rcpQFromSlope(Real gain, Real slope)
+    { return std::sqrt((gain + Real{1}/gain)*(Real{1}/slope - Real{1}) + Real{2}); }
+
+    /**
+     * Calculates the rcpQ (i.e. 1/Q) coefficient for filters, using the
+     * normalized reference frequency and bandwidth.
+     * \param f0norm 0 < f0norm < 0.5.
+     * \param bandwidth 0 < bandwidth
+     */
+    static Real rcpQFromBandwidth(Real f0norm, Real bandwidth)
+    {
+        const Real w0{al::numbers::pi_v<Real>*Real{2} * f0norm};
+        return 2.0f*std::sinh(std::log(Real{2})/Real{2}*bandwidth*w0/std::sin(w0));
+    }
+
+public:
+    void clear() noexcept { mZ1 = mZ2 = Real{0}; }
+
+    /**
+     * Sets the filter state for the specified filter type and its parameters.
+     *
+     * \param type The type of filter to apply.
+     * \param f0norm The normalized reference frequency (ref / sample_rate).
+     * This is the center point for the Shelf, Peaking, and BandPass filter
+     * types, or the cutoff frequency for the LowPass and HighPass filter
+     * types.
+     * \param gain The gain for the reference frequency response. Only used by
+     * the Shelf and Peaking filter types.
+     * \param slope Slope steepness of the transition band.
+     */
+    void setParamsFromSlope(BiquadType type, Real f0norm, Real gain, Real slope)
+    {
+        gain = std::max<Real>(gain, 0.001f); /* Limit -60dB */
+        setParams(type, f0norm, gain, rcpQFromSlope(gain, slope));
+    }
+
+    /**
+     * Sets the filter state for the specified filter type and its parameters.
+     *
+     * \param type The type of filter to apply.
+     * \param f0norm The normalized reference frequency (ref / sample_rate).
+     * This is the center point for the Shelf, Peaking, and BandPass filter
+     * types, or the cutoff frequency for the LowPass and HighPass filter
+     * types.
+     * \param gain The gain for the reference frequency response. Only used by
+     * the Shelf and Peaking filter types.
+     * \param bandwidth Normalized bandwidth of the transition band.
+     */
+    void setParamsFromBandwidth(BiquadType type, Real f0norm, Real gain, Real bandwidth)
+    { setParams(type, f0norm, gain, rcpQFromBandwidth(f0norm, bandwidth)); }
+
+    void copyParamsFrom(const BiquadFilterR &other)
+    {
+        mB0 = other.mB0;
+        mB1 = other.mB1;
+        mB2 = other.mB2;
+        mA1 = other.mA1;
+        mA2 = other.mA2;
+    }
+
+    void process(const al::span<const Real> src, Real *dst);
+    /** Processes this filter and the other at the same time. */
+    void dualProcess(BiquadFilterR &other, const al::span<const Real> src, Real *dst);
+
+    /* Rather hacky. It's just here to support "manual" processing. */
+    std::pair<Real,Real> getComponents() const noexcept { return {mZ1, mZ2}; }
+    void setComponents(Real z1, Real z2) noexcept { mZ1 = z1; mZ2 = z2; }
+    Real processOne(const Real in, Real &z1, Real &z2) const noexcept
+    {
+        const Real out{in*mB0 + z1};
+        z1 = in*mB1 - out*mA1 + z2;
+        z2 = in*mB2 - out*mA2;
+        return out;
+    }
+};
+
+template<typename Real>
+struct DualBiquadR {
+    BiquadFilterR<Real> &f0, &f1;
+
+    void process(const al::span<const Real> src, Real *dst)
+    { f0.dualProcess(f1, src, dst); }
+};
+
+using BiquadFilter = BiquadFilterR<float>;
+using DualBiquad = DualBiquadR<float>;
+
+#endif /* CORE_FILTERS_BIQUAD_H */
diff --git a/core/filters/nfc.cpp b/core/filters/nfc.cpp
new file mode 100644 (file)
index 0000000..aa64c61
--- /dev/null
@@ -0,0 +1,367 @@
+
+#include "config.h"
+
+#include "nfc.h"
+
+#include <algorithm>
+
+#include "opthelpers.h"
+
+
+/* Near-field control filters are the basis for handling the near-field effect.
+ * The near-field effect is a bass-boost present in the directional components
+ * of a recorded signal, created as a result of the wavefront curvature (itself
+ * a function of sound distance). Proper reproduction dictates this be
+ * compensated for using a bass-cut given the playback speaker distance, to
+ * avoid excessive bass in the playback.
+ *
+ * For real-time rendered audio, emulating the near-field effect based on the
+ * sound source's distance, and subsequently compensating for it at output
+ * based on the speaker distances, can create a more realistic perception of
+ * sound distance beyond a simple 1/r attenuation.
+ *
+ * These filters do just that. Each one applies a low-shelf filter, created as
+ * the combination of a bass-boost for a given sound source distance (near-
+ * field emulation) along with a bass-cut for a given control/speaker distance
+ * (near-field compensation).
+ *
+ * Note that it is necessary to apply a cut along with the boost, since the
+ * boost alone is unstable in higher-order ambisonics as it causes an infinite
+ * DC gain (even first-order ambisonics requires there to be no DC offset for
+ * the boost to work). Consequently, ambisonics requires a control parameter to
+ * be used to avoid an unstable boost-only filter. NFC-HOA defines this control
+ * as a reference delay, calculated with:
+ *
+ * reference_delay = control_distance / speed_of_sound
+ *
+ * This means w0 (for input) or w1 (for output) should be set to:
+ *
+ * wN = 1 / (reference_delay * sample_rate)
+ *
+ * when dealing with NFC-HOA content. For FOA input content, which does not
+ * specify a reference_delay variable, w0 should be set to 0 to apply only
+ * near-field compensation for output. It's important that w1 be a finite,
+ * positive, non-0 value or else the bass-boost will become unstable again.
+ * Also, w0 should not be too large compared to w1, to avoid excessively loud
+ * low frequencies.
+ */
+
+namespace {
+
+constexpr float B[5][4] = {
+    {    0.0f                             },
+    {    1.0f                             },
+    {    3.0f,     3.0f                   },
+    { 3.6778f,  6.4595f, 2.3222f          },
+    { 4.2076f, 11.4877f, 5.7924f, 9.1401f }
+};
+
+NfcFilter1 NfcFilterCreate1(const float w0, const float w1) noexcept
+{
+    NfcFilter1 nfc{};
+    float b_00, g_0;
+    float r;
+
+    /* Calculate bass-cut coefficients. */
+    r = 0.5f * w1;
+    b_00 = B[1][0] * r;
+    g_0 = 1.0f + b_00;
+
+    nfc.base_gain = 1.0f / g_0;
+    nfc.a1 = 2.0f * b_00 / g_0;
+
+    /* Calculate bass-boost coefficients. */
+    r = 0.5f * w0;
+    b_00 = B[1][0] * r;
+    g_0 = 1.0f + b_00;
+
+    nfc.gain = nfc.base_gain * g_0;
+    nfc.b1 = 2.0f * b_00 / g_0;
+
+    return nfc;
+}
+
+void NfcFilterAdjust1(NfcFilter1 *nfc, const float w0) noexcept
+{
+    const float r{0.5f * w0};
+    const float b_00{B[1][0] * r};
+    const float g_0{1.0f + b_00};
+
+    nfc->gain = nfc->base_gain * g_0;
+    nfc->b1 = 2.0f * b_00 / g_0;
+}
+
+
+NfcFilter2 NfcFilterCreate2(const float w0, const float w1) noexcept
+{
+    NfcFilter2 nfc{};
+    float b_10, b_11, g_1;
+    float r;
+
+    /* Calculate bass-cut coefficients. */
+    r = 0.5f * w1;
+    b_10 = B[2][0] * r;
+    b_11 = B[2][1] * r * r;
+    g_1 = 1.0f + b_10 + b_11;
+
+    nfc.base_gain = 1.0f / g_1;
+    nfc.a1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc.a2 = 4.0f * b_11 / g_1;
+
+    /* Calculate bass-boost coefficients. */
+    r = 0.5f * w0;
+    b_10 = B[2][0] * r;
+    b_11 = B[2][1] * r * r;
+    g_1 = 1.0f + b_10 + b_11;
+
+    nfc.gain = nfc.base_gain * g_1;
+    nfc.b1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc.b2 = 4.0f * b_11 / g_1;
+
+    return nfc;
+}
+
+void NfcFilterAdjust2(NfcFilter2 *nfc, const float w0) noexcept
+{
+    const float r{0.5f * w0};
+    const float b_10{B[2][0] * r};
+    const float b_11{B[2][1] * r * r};
+    const float g_1{1.0f + b_10 + b_11};
+
+    nfc->gain = nfc->base_gain * g_1;
+    nfc->b1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc->b2 = 4.0f * b_11 / g_1;
+}
+
+
+NfcFilter3 NfcFilterCreate3(const float w0, const float w1) noexcept
+{
+    NfcFilter3 nfc{};
+    float b_10, b_11, g_1;
+    float b_00, g_0;
+    float r;
+
+    /* Calculate bass-cut coefficients. */
+    r = 0.5f * w1;
+    b_10 = B[3][0] * r;
+    b_11 = B[3][1] * r * r;
+    b_00 = B[3][2] * r;
+    g_1 = 1.0f + b_10 + b_11;
+    g_0 = 1.0f + b_00;
+
+    nfc.base_gain = 1.0f / (g_1 * g_0);
+    nfc.a1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc.a2 = 4.0f * b_11 / g_1;
+    nfc.a3 = 2.0f * b_00 / g_0;
+
+    /* Calculate bass-boost coefficients. */
+    r = 0.5f * w0;
+    b_10 = B[3][0] * r;
+    b_11 = B[3][1] * r * r;
+    b_00 = B[3][2] * r;
+    g_1 = 1.0f + b_10 + b_11;
+    g_0 = 1.0f + b_00;
+
+    nfc.gain = nfc.base_gain * (g_1 * g_0);
+    nfc.b1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc.b2 = 4.0f * b_11 / g_1;
+    nfc.b3 = 2.0f * b_00 / g_0;
+
+    return nfc;
+}
+
+void NfcFilterAdjust3(NfcFilter3 *nfc, const float w0) noexcept
+{
+    const float r{0.5f * w0};
+    const float b_10{B[3][0] * r};
+    const float b_11{B[3][1] * r * r};
+    const float b_00{B[3][2] * r};
+    const float g_1{1.0f + b_10 + b_11};
+    const float g_0{1.0f + b_00};
+
+    nfc->gain = nfc->base_gain * (g_1 * g_0);
+    nfc->b1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc->b2 = 4.0f * b_11 / g_1;
+    nfc->b3 = 2.0f * b_00 / g_0;
+}
+
+
+NfcFilter4 NfcFilterCreate4(const float w0, const float w1) noexcept
+{
+    NfcFilter4 nfc{};
+    float b_10, b_11, g_1;
+    float b_00, b_01, g_0;
+    float r;
+
+    /* Calculate bass-cut coefficients. */
+    r = 0.5f * w1;
+    b_10 = B[4][0] * r;
+    b_11 = B[4][1] * r * r;
+    b_00 = B[4][2] * r;
+    b_01 = B[4][3] * r * r;
+    g_1 = 1.0f + b_10 + b_11;
+    g_0 = 1.0f + b_00 + b_01;
+
+    nfc.base_gain = 1.0f / (g_1 * g_0);
+    nfc.a1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc.a2 = 4.0f * b_11 / g_1;
+    nfc.a3 = (2.0f*b_00 + 4.0f*b_01) / g_0;
+    nfc.a4 = 4.0f * b_01 / g_0;
+
+    /* Calculate bass-boost coefficients. */
+    r = 0.5f * w0;
+    b_10 = B[4][0] * r;
+    b_11 = B[4][1] * r * r;
+    b_00 = B[4][2] * r;
+    b_01 = B[4][3] * r * r;
+    g_1 = 1.0f + b_10 + b_11;
+    g_0 = 1.0f + b_00 + b_01;
+
+    nfc.gain = nfc.base_gain * (g_1 * g_0);
+    nfc.b1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc.b2 = 4.0f * b_11 / g_1;
+    nfc.b3 = (2.0f*b_00 + 4.0f*b_01) / g_0;
+    nfc.b4 = 4.0f * b_01 / g_0;
+
+    return nfc;
+}
+
+void NfcFilterAdjust4(NfcFilter4 *nfc, const float w0) noexcept
+{
+    const float r{0.5f * w0};
+    const float b_10{B[4][0] * r};
+    const float b_11{B[4][1] * r * r};
+    const float b_00{B[4][2] * r};
+    const float b_01{B[4][3] * r * r};
+    const float g_1{1.0f + b_10 + b_11};
+    const float g_0{1.0f + b_00 + b_01};
+
+    nfc->gain = nfc->base_gain * (g_1 * g_0);
+    nfc->b1 = (2.0f*b_10 + 4.0f*b_11) / g_1;
+    nfc->b2 = 4.0f * b_11 / g_1;
+    nfc->b3 = (2.0f*b_00 + 4.0f*b_01) / g_0;
+    nfc->b4 = 4.0f * b_01 / g_0;
+}
+
+} // namespace
+
+void NfcFilter::init(const float w1) noexcept
+{
+    first = NfcFilterCreate1(0.0f, w1);
+    second = NfcFilterCreate2(0.0f, w1);
+    third = NfcFilterCreate3(0.0f, w1);
+    fourth = NfcFilterCreate4(0.0f, w1);
+}
+
+void NfcFilter::adjust(const float w0) noexcept
+{
+    NfcFilterAdjust1(&first, w0);
+    NfcFilterAdjust2(&second, w0);
+    NfcFilterAdjust3(&third, w0);
+    NfcFilterAdjust4(&fourth, w0);
+}
+
+
+void NfcFilter::process1(const al::span<const float> src, float *RESTRICT dst)
+{
+    const float gain{first.gain};
+    const float b1{first.b1};
+    const float a1{first.a1};
+    float z1{first.z[0]};
+    auto proc_sample = [gain,b1,a1,&z1](const float in) noexcept -> float
+    {
+        const float y{in*gain - a1*z1};
+        const float out{y + b1*z1};
+        z1 += y;
+        return out;
+    };
+    std::transform(src.cbegin(), src.cend(), dst, proc_sample);
+    first.z[0] = z1;
+}
+
+void NfcFilter::process2(const al::span<const float> src, float *RESTRICT dst)
+{
+    const float gain{second.gain};
+    const float b1{second.b1};
+    const float b2{second.b2};
+    const float a1{second.a1};
+    const float a2{second.a2};
+    float z1{second.z[0]};
+    float z2{second.z[1]};
+    auto proc_sample = [gain,b1,b2,a1,a2,&z1,&z2](const float in) noexcept -> float
+    {
+        const float y{in*gain - a1*z1 - a2*z2};
+        const float out{y + b1*z1 + b2*z2};
+        z2 += z1;
+        z1 += y;
+        return out;
+    };
+    std::transform(src.cbegin(), src.cend(), dst, proc_sample);
+    second.z[0] = z1;
+    second.z[1] = z2;
+}
+
+void NfcFilter::process3(const al::span<const float> src, float *RESTRICT dst)
+{
+    const float gain{third.gain};
+    const float b1{third.b1};
+    const float b2{third.b2};
+    const float b3{third.b3};
+    const float a1{third.a1};
+    const float a2{third.a2};
+    const float a3{third.a3};
+    float z1{third.z[0]};
+    float z2{third.z[1]};
+    float z3{third.z[2]};
+    auto proc_sample = [gain,b1,b2,b3,a1,a2,a3,&z1,&z2,&z3](const float in) noexcept -> float
+    {
+        float y{in*gain - a1*z1 - a2*z2};
+        float out{y + b1*z1 + b2*z2};
+        z2 += z1;
+        z1 += y;
+
+        y = out - a3*z3;
+        out = y + b3*z3;
+        z3 += y;
+        return out;
+    };
+    std::transform(src.cbegin(), src.cend(), dst, proc_sample);
+    third.z[0] = z1;
+    third.z[1] = z2;
+    third.z[2] = z3;
+}
+
+void NfcFilter::process4(const al::span<const float> src, float *RESTRICT dst)
+{
+    const float gain{fourth.gain};
+    const float b1{fourth.b1};
+    const float b2{fourth.b2};
+    const float b3{fourth.b3};
+    const float b4{fourth.b4};
+    const float a1{fourth.a1};
+    const float a2{fourth.a2};
+    const float a3{fourth.a3};
+    const float a4{fourth.a4};
+    float z1{fourth.z[0]};
+    float z2{fourth.z[1]};
+    float z3{fourth.z[2]};
+    float z4{fourth.z[3]};
+    auto proc_sample = [gain,b1,b2,b3,b4,a1,a2,a3,a4,&z1,&z2,&z3,&z4](const float in) noexcept -> float
+    {
+        float y{in*gain - a1*z1 - a2*z2};
+        float out{y + b1*z1 + b2*z2};
+        z2 += z1;
+        z1 += y;
+
+        y = out - a3*z3 - a4*z4;
+        out = y + b3*z3 + b4*z4;
+        z4 += z3;
+        z3 += y;
+        return out;
+    };
+    std::transform(src.cbegin(), src.cend(), dst, proc_sample);
+    fourth.z[0] = z1;
+    fourth.z[1] = z2;
+    fourth.z[2] = z3;
+    fourth.z[3] = z4;
+}
diff --git a/core/filters/nfc.h b/core/filters/nfc.h
new file mode 100644 (file)
index 0000000..33f67a5
--- /dev/null
@@ -0,0 +1,63 @@
+#ifndef CORE_FILTERS_NFC_H
+#define CORE_FILTERS_NFC_H
+
+#include <cstddef>
+
+#include "alspan.h"
+
+
+struct NfcFilter1 {
+    float base_gain, gain;
+    float b1, a1;
+    float z[1];
+};
+struct NfcFilter2 {
+    float base_gain, gain;
+    float b1, b2, a1, a2;
+    float z[2];
+};
+struct NfcFilter3 {
+    float base_gain, gain;
+    float b1, b2, b3, a1, a2, a3;
+    float z[3];
+};
+struct NfcFilter4 {
+    float base_gain, gain;
+    float b1, b2, b3, b4, a1, a2, a3, a4;
+    float z[4];
+};
+
+class NfcFilter {
+    NfcFilter1 first;
+    NfcFilter2 second;
+    NfcFilter3 third;
+    NfcFilter4 fourth;
+
+public:
+    /* NOTE:
+     * w0 = speed_of_sound / (source_distance * sample_rate);
+     * w1 = speed_of_sound / (control_distance * sample_rate);
+     *
+     * Generally speaking, the control distance should be approximately the
+     * average speaker distance, or based on the reference delay if outputing
+     * NFC-HOA. It must not be negative, 0, or infinite. The source distance
+     * should not be too small relative to the control distance.
+     */
+
+    void init(const float w1) noexcept;
+    void adjust(const float w0) noexcept;
+
+    /* Near-field control filter for first-order ambisonic channels (1-3). */
+    void process1(const al::span<const float> src, float *RESTRICT dst);
+
+    /* Near-field control filter for second-order ambisonic channels (4-8). */
+    void process2(const al::span<const float> src, float *RESTRICT dst);
+
+    /* Near-field control filter for third-order ambisonic channels (9-15). */
+    void process3(const al::span<const float> src, float *RESTRICT dst);
+
+    /* Near-field control filter for fourth-order ambisonic channels (16-24). */
+    void process4(const al::span<const float> src, float *RESTRICT dst);
+};
+
+#endif /* CORE_FILTERS_NFC_H */
diff --git a/core/filters/splitter.cpp b/core/filters/splitter.cpp
new file mode 100644 (file)
index 0000000..983ba36
--- /dev/null
@@ -0,0 +1,179 @@
+
+#include "config.h"
+
+#include "splitter.h"
+
+#include <algorithm>
+#include <cmath>
+#include <limits>
+
+#include "alnumbers.h"
+#include "opthelpers.h"
+
+
+template<typename Real>
+void BandSplitterR<Real>::init(Real f0norm)
+{
+    const Real w{f0norm * (al::numbers::pi_v<Real>*2)};
+    const Real cw{std::cos(w)};
+    if(cw > std::numeric_limits<float>::epsilon())
+        mCoeff = (std::sin(w) - 1.0f) / cw;
+    else
+        mCoeff = cw * -0.5f;
+
+    mLpZ1 = 0.0f;
+    mLpZ2 = 0.0f;
+    mApZ1 = 0.0f;
+}
+
+template<typename Real>
+void BandSplitterR<Real>::process(const al::span<const Real> input, Real *hpout, Real *lpout)
+{
+    const Real ap_coeff{mCoeff};
+    const Real lp_coeff{mCoeff*0.5f + 0.5f};
+    Real lp_z1{mLpZ1};
+    Real lp_z2{mLpZ2};
+    Real ap_z1{mApZ1};
+    auto proc_sample = [ap_coeff,lp_coeff,&lp_z1,&lp_z2,&ap_z1,&lpout](const Real in) noexcept -> Real
+    {
+        /* Low-pass sample processing. */
+        Real d{(in - lp_z1) * lp_coeff};
+        Real lp_y{lp_z1 + d};
+        lp_z1 = lp_y + d;
+
+        d = (lp_y - lp_z2) * lp_coeff;
+        lp_y = lp_z2 + d;
+        lp_z2 = lp_y + d;
+
+        *(lpout++) = lp_y;
+
+        /* All-pass sample processing. */
+        Real ap_y{in*ap_coeff + ap_z1};
+        ap_z1 = in - ap_y*ap_coeff;
+
+        /* High-pass generated from removing low-passed output. */
+        return ap_y - lp_y;
+    };
+    std::transform(input.cbegin(), input.cend(), hpout, proc_sample);
+    mLpZ1 = lp_z1;
+    mLpZ2 = lp_z2;
+    mApZ1 = ap_z1;
+}
+
+template<typename Real>
+void BandSplitterR<Real>::processHfScale(const al::span<const Real> input, Real *RESTRICT output,
+    const Real hfscale)
+{
+    const Real ap_coeff{mCoeff};
+    const Real lp_coeff{mCoeff*0.5f + 0.5f};
+    Real lp_z1{mLpZ1};
+    Real lp_z2{mLpZ2};
+    Real ap_z1{mApZ1};
+    auto proc_sample = [hfscale,ap_coeff,lp_coeff,&lp_z1,&lp_z2,&ap_z1](const Real in) noexcept -> Real
+    {
+        /* Low-pass sample processing. */
+        Real d{(in - lp_z1) * lp_coeff};
+        Real lp_y{lp_z1 + d};
+        lp_z1 = lp_y + d;
+
+        d = (lp_y - lp_z2) * lp_coeff;
+        lp_y = lp_z2 + d;
+        lp_z2 = lp_y + d;
+
+        /* All-pass sample processing. */
+        Real ap_y{in*ap_coeff + ap_z1};
+        ap_z1 = in - ap_y*ap_coeff;
+
+        /* High-pass generated by removing the low-passed signal, which is then
+         * scaled and added back to the low-passed signal.
+         */
+        return (ap_y-lp_y)*hfscale + lp_y;
+    };
+    std::transform(input.begin(), input.end(), output, proc_sample);
+    mLpZ1 = lp_z1;
+    mLpZ2 = lp_z2;
+    mApZ1 = ap_z1;
+}
+
+template<typename Real>
+void BandSplitterR<Real>::processHfScale(const al::span<Real> samples, const Real hfscale)
+{
+    const Real ap_coeff{mCoeff};
+    const Real lp_coeff{mCoeff*0.5f + 0.5f};
+    Real lp_z1{mLpZ1};
+    Real lp_z2{mLpZ2};
+    Real ap_z1{mApZ1};
+    auto proc_sample = [hfscale,ap_coeff,lp_coeff,&lp_z1,&lp_z2,&ap_z1](const Real in) noexcept -> Real
+    {
+        /* Low-pass sample processing. */
+        Real d{(in - lp_z1) * lp_coeff};
+        Real lp_y{lp_z1 + d};
+        lp_z1 = lp_y + d;
+
+        d = (lp_y - lp_z2) * lp_coeff;
+        lp_y = lp_z2 + d;
+        lp_z2 = lp_y + d;
+
+        /* All-pass sample processing. */
+        Real ap_y{in*ap_coeff + ap_z1};
+        ap_z1 = in - ap_y*ap_coeff;
+
+        /* High-pass generated by removing the low-passed signal, which is then
+         * scaled and added back to the low-passed signal.
+         */
+        return (ap_y-lp_y)*hfscale + lp_y;
+    };
+    std::transform(samples.begin(), samples.end(), samples.begin(), proc_sample);
+    mLpZ1 = lp_z1;
+    mLpZ2 = lp_z2;
+    mApZ1 = ap_z1;
+}
+
+template<typename Real>
+void BandSplitterR<Real>::processScale(const al::span<Real> samples, const Real hfscale, const Real lfscale)
+{
+    const Real ap_coeff{mCoeff};
+    const Real lp_coeff{mCoeff*0.5f + 0.5f};
+    Real lp_z1{mLpZ1};
+    Real lp_z2{mLpZ2};
+    Real ap_z1{mApZ1};
+    auto proc_sample = [hfscale,lfscale,ap_coeff,lp_coeff,&lp_z1,&lp_z2,&ap_z1](const Real in) noexcept -> Real
+    {
+        Real d{(in - lp_z1) * lp_coeff};
+        Real lp_y{lp_z1 + d};
+        lp_z1 = lp_y + d;
+
+        d = (lp_y - lp_z2) * lp_coeff;
+        lp_y = lp_z2 + d;
+        lp_z2 = lp_y + d;
+
+        Real ap_y{in*ap_coeff + ap_z1};
+        ap_z1 = in - ap_y*ap_coeff;
+
+        /* Apply separate factors to the high and low frequencies. */
+        return (ap_y-lp_y)*hfscale + lp_y*lfscale;
+    };
+    std::transform(samples.begin(), samples.end(), samples.begin(), proc_sample);
+    mLpZ1 = lp_z1;
+    mLpZ2 = lp_z2;
+    mApZ1 = ap_z1;
+}
+
+template<typename Real>
+void BandSplitterR<Real>::processAllPass(const al::span<Real> samples)
+{
+    const Real coeff{mCoeff};
+    Real z1{mApZ1};
+    auto proc_sample = [coeff,&z1](const Real in) noexcept -> Real
+    {
+        const Real out{in*coeff + z1};
+        z1 = in - out*coeff;
+        return out;
+    };
+    std::transform(samples.cbegin(), samples.cend(), samples.begin(), proc_sample);
+    mApZ1 = z1;
+}
+
+
+template class BandSplitterR<float>;
+template class BandSplitterR<double>;
diff --git a/core/filters/splitter.h b/core/filters/splitter.h
new file mode 100644 (file)
index 0000000..e853eb3
--- /dev/null
@@ -0,0 +1,40 @@
+#ifndef CORE_FILTERS_SPLITTER_H
+#define CORE_FILTERS_SPLITTER_H
+
+#include <cstddef>
+
+#include "alspan.h"
+
+
+/* Band splitter. Splits a signal into two phase-matching frequency bands. */
+template<typename Real>
+class BandSplitterR {
+    Real mCoeff{0.0f};
+    Real mLpZ1{0.0f};
+    Real mLpZ2{0.0f};
+    Real mApZ1{0.0f};
+
+public:
+    BandSplitterR() = default;
+    BandSplitterR(const BandSplitterR&) = default;
+    BandSplitterR(Real f0norm) { init(f0norm); }
+    BandSplitterR& operator=(const BandSplitterR&) = default;
+
+    void init(Real f0norm);
+    void clear() noexcept { mLpZ1 = mLpZ2 = mApZ1 = 0.0f; }
+    void process(const al::span<const Real> input, Real *hpout, Real *lpout);
+
+    void processHfScale(const al::span<const Real> input, Real *output, const Real hfscale);
+
+    void processHfScale(const al::span<Real> samples, const Real hfscale);
+    void processScale(const al::span<Real> samples, const Real hfscale, const Real lfscale);
+
+    /**
+     * The all-pass portion of the band splitter. Applies the same phase shift
+     * without splitting or scaling the signal.
+     */
+    void processAllPass(const al::span<Real> samples);
+};
+using BandSplitter = BandSplitterR<float>;
+
+#endif /* CORE_FILTERS_SPLITTER_H */
diff --git a/core/fmt_traits.cpp b/core/fmt_traits.cpp
new file mode 100644 (file)
index 0000000..054d876
--- /dev/null
@@ -0,0 +1,79 @@
+
+#include "config.h"
+
+#include "fmt_traits.h"
+
+
+namespace al {
+
+const int16_t muLawDecompressionTable[256] = {
+    -32124,-31100,-30076,-29052,-28028,-27004,-25980,-24956,
+    -23932,-22908,-21884,-20860,-19836,-18812,-17788,-16764,
+    -15996,-15484,-14972,-14460,-13948,-13436,-12924,-12412,
+    -11900,-11388,-10876,-10364, -9852, -9340, -8828, -8316,
+     -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,
+     -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,
+     -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,
+     -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,
+     -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,
+     -1372, -1308, -1244, -1180, -1116, -1052,  -988,  -924,
+      -876,  -844,  -812,  -780,  -748,  -716,  -684,  -652,
+      -620,  -588,  -556,  -524,  -492,  -460,  -428,  -396,
+      -372,  -356,  -340,  -324,  -308,  -292,  -276,  -260,
+      -244,  -228,  -212,  -196,  -180,  -164,  -148,  -132,
+      -120,  -112,  -104,   -96,   -88,   -80,   -72,   -64,
+       -56,   -48,   -40,   -32,   -24,   -16,    -8,     0,
+     32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,
+     23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,
+     15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,
+     11900, 11388, 10876, 10364,  9852,  9340,  8828,  8316,
+      7932,  7676,  7420,  7164,  6908,  6652,  6396,  6140,
+      5884,  5628,  5372,  5116,  4860,  4604,  4348,  4092,
+      3900,  3772,  3644,  3516,  3388,  3260,  3132,  3004,
+      2876,  2748,  2620,  2492,  2364,  2236,  2108,  1980,
+      1884,  1820,  1756,  1692,  1628,  1564,  1500,  1436,
+      1372,  1308,  1244,  1180,  1116,  1052,   988,   924,
+       876,   844,   812,   780,   748,   716,   684,   652,
+       620,   588,   556,   524,   492,   460,   428,   396,
+       372,   356,   340,   324,   308,   292,   276,   260,
+       244,   228,   212,   196,   180,   164,   148,   132,
+       120,   112,   104,    96,    88,    80,    72,    64,
+        56,    48,    40,    32,    24,    16,     8,     0
+};
+
+const int16_t aLawDecompressionTable[256] = {
+     -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,
+     -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,
+     -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,
+     -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,
+    -22016,-20992,-24064,-23040,-17920,-16896,-19968,-18944,
+    -30208,-29184,-32256,-31232,-26112,-25088,-28160,-27136,
+    -11008,-10496,-12032,-11520, -8960, -8448, -9984, -9472,
+    -15104,-14592,-16128,-15616,-13056,-12544,-14080,-13568,
+      -344,  -328,  -376,  -360,  -280,  -264,  -312,  -296,
+      -472,  -456,  -504,  -488,  -408,  -392,  -440,  -424,
+       -88,   -72,  -120,  -104,   -24,    -8,   -56,   -40,
+      -216,  -200,  -248,  -232,  -152,  -136,  -184,  -168,
+     -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,
+     -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,
+      -688,  -656,  -752,  -720,  -560,  -528,  -624,  -592,
+      -944,  -912, -1008,  -976,  -816,  -784,  -880,  -848,
+      5504,  5248,  6016,  5760,  4480,  4224,  4992,  4736,
+      7552,  7296,  8064,  7808,  6528,  6272,  7040,  6784,
+      2752,  2624,  3008,  2880,  2240,  2112,  2496,  2368,
+      3776,  3648,  4032,  3904,  3264,  3136,  3520,  3392,
+     22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,
+     30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,
+     11008, 10496, 12032, 11520,  8960,  8448,  9984,  9472,
+     15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,
+       344,   328,   376,   360,   280,   264,   312,   296,
+       472,   456,   504,   488,   408,   392,   440,   424,
+        88,    72,   120,   104,    24,     8,    56,    40,
+       216,   200,   248,   232,   152,   136,   184,   168,
+      1376,  1312,  1504,  1440,  1120,  1056,  1248,  1184,
+      1888,  1824,  2016,  1952,  1632,  1568,  1760,  1696,
+       688,   656,   752,   720,   560,   528,   624,   592,
+       944,   912,  1008,   976,   816,   784,   880,   848
+};
+
+} // namespace al
diff --git a/core/fmt_traits.h b/core/fmt_traits.h
new file mode 100644 (file)
index 0000000..f797f83
--- /dev/null
@@ -0,0 +1,81 @@
+#ifndef CORE_FMT_TRAITS_H
+#define CORE_FMT_TRAITS_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "albyte.h"
+#include "buffer_storage.h"
+
+
+namespace al {
+
+extern const int16_t muLawDecompressionTable[256];
+extern const int16_t aLawDecompressionTable[256];
+
+
+template<FmtType T>
+struct FmtTypeTraits { };
+
+template<>
+struct FmtTypeTraits<FmtUByte> {
+    using Type = uint8_t;
+
+    template<typename OutT>
+    static constexpr inline OutT to(const Type val) noexcept
+    { return val*OutT{1.0/128.0} - OutT{1.0}; }
+};
+template<>
+struct FmtTypeTraits<FmtShort> {
+    using Type = int16_t;
+
+    template<typename OutT>
+    static constexpr inline OutT to(const Type val) noexcept { return val*OutT{1.0/32768.0}; }
+};
+template<>
+struct FmtTypeTraits<FmtFloat> {
+    using Type = float;
+
+    template<typename OutT>
+    static constexpr inline OutT to(const Type val) noexcept { return val; }
+};
+template<>
+struct FmtTypeTraits<FmtDouble> {
+    using Type = double;
+
+    template<typename OutT>
+    static constexpr inline OutT to(const Type val) noexcept { return static_cast<OutT>(val); }
+};
+template<>
+struct FmtTypeTraits<FmtMulaw> {
+    using Type = uint8_t;
+
+    template<typename OutT>
+    static constexpr inline OutT to(const Type val) noexcept
+    { return muLawDecompressionTable[val] * OutT{1.0/32768.0}; }
+};
+template<>
+struct FmtTypeTraits<FmtAlaw> {
+    using Type = uint8_t;
+
+    template<typename OutT>
+    static constexpr inline OutT to(const Type val) noexcept
+    { return aLawDecompressionTable[val] * OutT{1.0/32768.0}; }
+};
+
+
+template<FmtType SrcType, typename DstT>
+inline void LoadSampleArray(DstT *RESTRICT dst, const al::byte *src, const size_t srcstep,
+    const size_t samples) noexcept
+{
+    using TypeTraits = FmtTypeTraits<SrcType>;
+    using SampleType = typename TypeTraits::Type;
+
+    const SampleType *RESTRICT ssrc{reinterpret_cast<const SampleType*>(src)};
+    for(size_t i{0u};i < samples;i++)
+        dst[i] = TypeTraits::template to<DstT>(ssrc[i*srcstep]);
+}
+
+} // namespace al
+
+#endif /* CORE_FMT_TRAITS_H */
diff --git a/core/fpu_ctrl.cpp b/core/fpu_ctrl.cpp
new file mode 100644 (file)
index 0000000..0cf0d6e
--- /dev/null
@@ -0,0 +1,61 @@
+
+#include "config.h"
+
+#include "fpu_ctrl.h"
+
+#ifdef HAVE_INTRIN_H
+#include <intrin.h>
+#endif
+#ifdef HAVE_SSE_INTRINSICS
+#include <emmintrin.h>
+#ifndef _MM_DENORMALS_ZERO_MASK
+/* Some headers seem to be missing these? */
+#define _MM_DENORMALS_ZERO_MASK 0x0040u
+#define _MM_DENORMALS_ZERO_ON 0x0040u
+#endif
+#endif
+
+#include "cpu_caps.h"
+
+
+void FPUCtl::enter() noexcept
+{
+    if(this->in_mode) return;
+
+#if defined(HAVE_SSE_INTRINSICS)
+    this->sse_state = _mm_getcsr();
+    unsigned int sseState{this->sse_state};
+    sseState &= ~(_MM_FLUSH_ZERO_MASK | _MM_DENORMALS_ZERO_MASK);
+    sseState |= _MM_FLUSH_ZERO_ON | _MM_DENORMALS_ZERO_ON;
+    _mm_setcsr(sseState);
+
+#elif defined(__GNUC__) && defined(HAVE_SSE)
+
+    if((CPUCapFlags&CPU_CAP_SSE))
+    {
+        __asm__ __volatile__("stmxcsr %0" : "=m" (*&this->sse_state));
+        unsigned int sseState{this->sse_state};
+        sseState |= 0x8000; /* set flush-to-zero */
+        if((CPUCapFlags&CPU_CAP_SSE2))
+            sseState |= 0x0040; /* set denormals-are-zero */
+        __asm__ __volatile__("ldmxcsr %0" : : "m" (*&sseState));
+    }
+#endif
+
+    this->in_mode = true;
+}
+
+void FPUCtl::leave() noexcept
+{
+    if(!this->in_mode) return;
+
+#if defined(HAVE_SSE_INTRINSICS)
+    _mm_setcsr(this->sse_state);
+
+#elif defined(__GNUC__) && defined(HAVE_SSE)
+
+    if((CPUCapFlags&CPU_CAP_SSE))
+        __asm__ __volatile__("ldmxcsr %0" : : "m" (*&this->sse_state));
+#endif
+    this->in_mode = false;
+}
diff --git a/core/fpu_ctrl.h b/core/fpu_ctrl.h
new file mode 100644 (file)
index 0000000..9554313
--- /dev/null
@@ -0,0 +1,21 @@
+#ifndef CORE_FPU_CTRL_H
+#define CORE_FPU_CTRL_H
+
+class FPUCtl {
+#if defined(HAVE_SSE_INTRINSICS) || (defined(__GNUC__) && defined(HAVE_SSE))
+    unsigned int sse_state{};
+#endif
+    bool in_mode{};
+
+public:
+    FPUCtl() noexcept { enter(); in_mode = true; }
+    ~FPUCtl() { if(in_mode) leave(); }
+
+    FPUCtl(const FPUCtl&) = delete;
+    FPUCtl& operator=(const FPUCtl&) = delete;
+
+    void enter() noexcept;
+    void leave() noexcept;
+};
+
+#endif /* CORE_FPU_CTRL_H */
diff --git a/core/front_stablizer.h b/core/front_stablizer.h
new file mode 100644 (file)
index 0000000..6825111
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef CORE_FRONT_STABLIZER_H
+#define CORE_FRONT_STABLIZER_H
+
+#include <array>
+#include <memory>
+
+#include "almalloc.h"
+#include "bufferline.h"
+#include "filters/splitter.h"
+
+
+struct FrontStablizer {
+    FrontStablizer(size_t numchans) : ChannelFilters{numchans} { }
+
+    alignas(16) std::array<float,BufferLineSize> MidDirect{};
+    alignas(16) std::array<float,BufferLineSize> Side{};
+    alignas(16) std::array<float,BufferLineSize> Temp{};
+
+    BandSplitter MidFilter;
+    alignas(16) FloatBufferLine MidLF{};
+    alignas(16) FloatBufferLine MidHF{};
+
+    al::FlexArray<BandSplitter,16> ChannelFilters;
+
+    static std::unique_ptr<FrontStablizer> Create(size_t numchans)
+    { return std::unique_ptr<FrontStablizer>{new(FamCount(numchans)) FrontStablizer{numchans}}; }
+
+    DEF_FAM_NEWDEL(FrontStablizer, ChannelFilters)
+};
+
+#endif /* CORE_FRONT_STABLIZER_H */
diff --git a/core/helpers.cpp b/core/helpers.cpp
new file mode 100644 (file)
index 0000000..99cf009
--- /dev/null
@@ -0,0 +1,569 @@
+
+#include "config.h"
+
+#include "helpers.h"
+
+#include <algorithm>
+#include <cerrno>
+#include <cstdarg>
+#include <cstdlib>
+#include <cstdio>
+#include <cstring>
+#include <mutex>
+#include <limits>
+#include <string>
+#include <tuple>
+
+#include "almalloc.h"
+#include "alfstream.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "logging.h"
+#include "strutils.h"
+#include "vector.h"
+
+
+/* Mixing thread piority level */
+int RTPrioLevel{1};
+
+/* Allow reducing the process's RTTime limit for RTKit. */
+bool AllowRTTimeLimit{true};
+
+
+#ifdef _WIN32
+
+#include <shlobj.h>
+
+const PathNamePair &GetProcBinary()
+{
+    static al::optional<PathNamePair> procbin;
+    if(procbin) return *procbin;
+
+    auto fullpath = al::vector<WCHAR>(256);
+    DWORD len{GetModuleFileNameW(nullptr, fullpath.data(), static_cast<DWORD>(fullpath.size()))};
+    while(len == fullpath.size())
+    {
+        fullpath.resize(fullpath.size() << 1);
+        len = GetModuleFileNameW(nullptr, fullpath.data(), static_cast<DWORD>(fullpath.size()));
+    }
+    if(len == 0)
+    {
+        ERR("Failed to get process name: error %lu\n", GetLastError());
+        procbin.emplace();
+        return *procbin;
+    }
+
+    fullpath.resize(len);
+    if(fullpath.back() != 0)
+        fullpath.push_back(0);
+
+    std::replace(fullpath.begin(), fullpath.end(), '/', '\\');
+    auto sep = std::find(fullpath.rbegin()+1, fullpath.rend(), '\\');
+    if(sep != fullpath.rend())
+    {
+        *sep = 0;
+        procbin.emplace(wstr_to_utf8(fullpath.data()), wstr_to_utf8(al::to_address(sep.base())));
+    }
+    else
+        procbin.emplace(std::string{}, wstr_to_utf8(fullpath.data()));
+
+    TRACE("Got binary: %s, %s\n", procbin->path.c_str(), procbin->fname.c_str());
+    return *procbin;
+}
+
+namespace {
+
+void DirectorySearch(const char *path, const char *ext, al::vector<std::string> *const results)
+{
+    std::string pathstr{path};
+    pathstr += "\\*";
+    pathstr += ext;
+    TRACE("Searching %s\n", pathstr.c_str());
+
+    std::wstring wpath{utf8_to_wstr(pathstr.c_str())};
+    WIN32_FIND_DATAW fdata;
+    HANDLE hdl{FindFirstFileW(wpath.c_str(), &fdata)};
+    if(hdl == INVALID_HANDLE_VALUE) return;
+
+    const auto base = results->size();
+
+    do {
+        results->emplace_back();
+        std::string &str = results->back();
+        str = path;
+        str += '\\';
+        str += wstr_to_utf8(fdata.cFileName);
+    } while(FindNextFileW(hdl, &fdata));
+    FindClose(hdl);
+
+    const al::span<std::string> newlist{results->data()+base, results->size()-base};
+    std::sort(newlist.begin(), newlist.end());
+    for(const auto &name : newlist)
+        TRACE(" got %s\n", name.c_str());
+}
+
+} // namespace
+
+al::vector<std::string> SearchDataFiles(const char *ext, const char *subdir)
+{
+    auto is_slash = [](int c) noexcept -> int { return (c == '\\' || c == '/'); };
+
+    static std::mutex search_lock;
+    std::lock_guard<std::mutex> _{search_lock};
+
+    /* If the path is absolute, use it directly. */
+    al::vector<std::string> results;
+    if(isalpha(subdir[0]) && subdir[1] == ':' && is_slash(subdir[2]))
+    {
+        std::string path{subdir};
+        std::replace(path.begin(), path.end(), '/', '\\');
+        DirectorySearch(path.c_str(), ext, &results);
+        return results;
+    }
+    if(subdir[0] == '\\' && subdir[1] == '\\' && subdir[2] == '?' && subdir[3] == '\\')
+    {
+        DirectorySearch(subdir, ext, &results);
+        return results;
+    }
+
+    std::string path;
+
+    /* Search the app-local directory. */
+    if(auto localpath = al::getenv(L"ALSOFT_LOCAL_PATH"))
+    {
+        path = wstr_to_utf8(localpath->c_str());
+        if(is_slash(path.back()))
+            path.pop_back();
+    }
+    else if(WCHAR *cwdbuf{_wgetcwd(nullptr, 0)})
+    {
+        path = wstr_to_utf8(cwdbuf);
+        if(is_slash(path.back()))
+            path.pop_back();
+        free(cwdbuf);
+    }
+    else
+        path = ".";
+    std::replace(path.begin(), path.end(), '/', '\\');
+    DirectorySearch(path.c_str(), ext, &results);
+
+    /* Search the local and global data dirs. */
+    static const int ids[2]{ CSIDL_APPDATA, CSIDL_COMMON_APPDATA };
+    for(int id : ids)
+    {
+        WCHAR buffer[MAX_PATH];
+        if(SHGetSpecialFolderPathW(nullptr, buffer, id, FALSE) == FALSE)
+            continue;
+
+        path = wstr_to_utf8(buffer);
+        if(!is_slash(path.back()))
+            path += '\\';
+        path += subdir;
+        std::replace(path.begin(), path.end(), '/', '\\');
+
+        DirectorySearch(path.c_str(), ext, &results);
+    }
+
+    return results;
+}
+
+void SetRTPriority(void)
+{
+    if(RTPrioLevel > 0)
+    {
+        if(!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL))
+            ERR("Failed to set priority level for thread\n");
+    }
+}
+
+#else
+
+#include <sys/types.h>
+#include <unistd.h>
+#include <dirent.h>
+#ifdef __FreeBSD__
+#include <sys/sysctl.h>
+#endif
+#ifdef __HAIKU__
+#include <FindDirectory.h>
+#endif
+#ifdef HAVE_PROC_PIDPATH
+#include <libproc.h>
+#endif
+#if defined(HAVE_PTHREAD_SETSCHEDPARAM) && !defined(__OpenBSD__)
+#include <pthread.h>
+#include <sched.h>
+#endif
+#ifdef HAVE_RTKIT
+#include <sys/time.h>
+#include <sys/resource.h>
+
+#include "dbus_wrap.h"
+#include "rtkit.h"
+#ifndef RLIMIT_RTTIME
+#define RLIMIT_RTTIME 15
+#endif
+#endif
+
+const PathNamePair &GetProcBinary()
+{
+    static al::optional<PathNamePair> procbin;
+    if(procbin) return *procbin;
+
+    al::vector<char> pathname;
+#ifdef __FreeBSD__
+    size_t pathlen;
+    int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };
+    if(sysctl(mib, 4, nullptr, &pathlen, nullptr, 0) == -1)
+        WARN("Failed to sysctl kern.proc.pathname: %s\n", strerror(errno));
+    else
+    {
+        pathname.resize(pathlen + 1);
+        sysctl(mib, 4, pathname.data(), &pathlen, nullptr, 0);
+        pathname.resize(pathlen);
+    }
+#endif
+#ifdef HAVE_PROC_PIDPATH
+    if(pathname.empty())
+    {
+        char procpath[PROC_PIDPATHINFO_MAXSIZE]{};
+        const pid_t pid{getpid()};
+        if(proc_pidpath(pid, procpath, sizeof(procpath)) < 1)
+            ERR("proc_pidpath(%d, ...) failed: %s\n", pid, strerror(errno));
+        else
+            pathname.insert(pathname.end(), procpath, procpath+strlen(procpath));
+    }
+#endif
+#ifdef __HAIKU__
+    if(pathname.empty())
+    {
+        char procpath[PATH_MAX];
+        if(find_path(B_APP_IMAGE_SYMBOL, B_FIND_PATH_IMAGE_PATH, NULL, procpath, sizeof(procpath)) == B_OK)
+            pathname.insert(pathname.end(), procpath, procpath+strlen(procpath));
+    }
+#endif
+#ifndef __SWITCH__
+    if(pathname.empty())
+    {
+        static const char SelfLinkNames[][32]{
+            "/proc/self/exe",
+            "/proc/self/file",
+            "/proc/curproc/exe",
+            "/proc/curproc/file"
+        };
+
+        pathname.resize(256);
+
+        const char *selfname{};
+        ssize_t len{};
+        for(const char *name : SelfLinkNames)
+        {
+            selfname = name;
+            len = readlink(selfname, pathname.data(), pathname.size());
+            if(len >= 0 || errno != ENOENT) break;
+        }
+
+        while(len > 0 && static_cast<size_t>(len) == pathname.size())
+        {
+            pathname.resize(pathname.size() << 1);
+            len = readlink(selfname, pathname.data(), pathname.size());
+        }
+        if(len <= 0)
+        {
+            WARN("Failed to readlink %s: %s\n", selfname, strerror(errno));
+            len = 0;
+        }
+
+        pathname.resize(static_cast<size_t>(len));
+    }
+#endif
+    while(!pathname.empty() && pathname.back() == 0)
+        pathname.pop_back();
+
+    auto sep = std::find(pathname.crbegin(), pathname.crend(), '/');
+    if(sep != pathname.crend())
+        procbin.emplace(std::string(pathname.cbegin(), sep.base()-1),
+            std::string(sep.base(), pathname.cend()));
+    else
+        procbin.emplace(std::string{}, std::string(pathname.cbegin(), pathname.cend()));
+
+    TRACE("Got binary: \"%s\", \"%s\"\n", procbin->path.c_str(), procbin->fname.c_str());
+    return *procbin;
+}
+
+namespace {
+
+void DirectorySearch(const char *path, const char *ext, al::vector<std::string> *const results)
+{
+    TRACE("Searching %s for *%s\n", path, ext);
+    DIR *dir{opendir(path)};
+    if(!dir) return;
+
+    const auto base = results->size();
+    const size_t extlen{strlen(ext)};
+
+    while(struct dirent *dirent{readdir(dir)})
+    {
+        if(strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0)
+            continue;
+
+        const size_t len{strlen(dirent->d_name)};
+        if(len <= extlen) continue;
+        if(al::strcasecmp(dirent->d_name+len-extlen, ext) != 0)
+            continue;
+
+        results->emplace_back();
+        std::string &str = results->back();
+        str = path;
+        if(str.back() != '/')
+            str.push_back('/');
+        str += dirent->d_name;
+    }
+    closedir(dir);
+
+    const al::span<std::string> newlist{results->data()+base, results->size()-base};
+    std::sort(newlist.begin(), newlist.end());
+    for(const auto &name : newlist)
+        TRACE(" got %s\n", name.c_str());
+}
+
+} // namespace
+
+al::vector<std::string> SearchDataFiles(const char *ext, const char *subdir)
+{
+    static std::mutex search_lock;
+    std::lock_guard<std::mutex> _{search_lock};
+
+    al::vector<std::string> results;
+    if(subdir[0] == '/')
+    {
+        DirectorySearch(subdir, ext, &results);
+        return results;
+    }
+
+    /* Search the app-local directory. */
+    if(auto localpath = al::getenv("ALSOFT_LOCAL_PATH"))
+        DirectorySearch(localpath->c_str(), ext, &results);
+    else
+    {
+        al::vector<char> cwdbuf(256);
+        while(!getcwd(cwdbuf.data(), cwdbuf.size()))
+        {
+            if(errno != ERANGE)
+            {
+                cwdbuf.clear();
+                break;
+            }
+            cwdbuf.resize(cwdbuf.size() << 1);
+        }
+        if(cwdbuf.empty())
+            DirectorySearch(".", ext, &results);
+        else
+        {
+            DirectorySearch(cwdbuf.data(), ext, &results);
+            cwdbuf.clear();
+        }
+    }
+
+    // Search local data dir
+    if(auto datapath = al::getenv("XDG_DATA_HOME"))
+    {
+        std::string &path = *datapath;
+        if(path.back() != '/')
+            path += '/';
+        path += subdir;
+        DirectorySearch(path.c_str(), ext, &results);
+    }
+    else if(auto homepath = al::getenv("HOME"))
+    {
+        std::string &path = *homepath;
+        if(path.back() == '/')
+            path.pop_back();
+        path += "/.local/share/";
+        path += subdir;
+        DirectorySearch(path.c_str(), ext, &results);
+    }
+
+    // Search global data dirs
+    std::string datadirs{al::getenv("XDG_DATA_DIRS").value_or("/usr/local/share/:/usr/share/")};
+
+    size_t curpos{0u};
+    while(curpos < datadirs.size())
+    {
+        size_t nextpos{datadirs.find(':', curpos)};
+
+        std::string path{(nextpos != std::string::npos) ?
+            datadirs.substr(curpos, nextpos++ - curpos) : datadirs.substr(curpos)};
+        curpos = nextpos;
+
+        if(path.empty()) continue;
+        if(path.back() != '/')
+            path += '/';
+        path += subdir;
+
+        DirectorySearch(path.c_str(), ext, &results);
+    }
+
+#ifdef ALSOFT_INSTALL_DATADIR
+    // Search the installation data directory
+    {
+        std::string path{ALSOFT_INSTALL_DATADIR};
+        if(!path.empty())
+        {
+            if(path.back() != '/')
+                path += '/';
+            path += subdir;
+            DirectorySearch(path.c_str(), ext, &results);
+        }
+    }
+#endif
+
+    return results;
+}
+
+namespace {
+
+bool SetRTPriorityPthread(int prio)
+{
+    int err{ENOTSUP};
+#if defined(HAVE_PTHREAD_SETSCHEDPARAM) && !defined(__OpenBSD__)
+    /* Get the min and max priority for SCHED_RR. Limit the max priority to
+     * half, for now, to ensure the thread can't take the highest priority and
+     * go rogue.
+     */
+    int rtmin{sched_get_priority_min(SCHED_RR)};
+    int rtmax{sched_get_priority_max(SCHED_RR)};
+    rtmax = (rtmax-rtmin)/2 + rtmin;
+
+    struct sched_param param{};
+    param.sched_priority = clampi(prio, rtmin, rtmax);
+#ifdef SCHED_RESET_ON_FORK
+    err = pthread_setschedparam(pthread_self(), SCHED_RR|SCHED_RESET_ON_FORK, &param);
+    if(err == EINVAL)
+#endif
+        err = pthread_setschedparam(pthread_self(), SCHED_RR, &param);
+    if(err == 0) return true;
+
+#else
+
+    std::ignore = prio;
+#endif
+    WARN("pthread_setschedparam failed: %s (%d)\n", std::strerror(err), err);
+    return false;
+}
+
+bool SetRTPriorityRTKit(int prio)
+{
+#ifdef HAVE_RTKIT
+    if(!HasDBus())
+    {
+        WARN("D-Bus not available\n");
+        return false;
+    }
+    dbus::Error error;
+    dbus::ConnectionPtr conn{dbus_bus_get(DBUS_BUS_SYSTEM, &error.get())};
+    if(!conn)
+    {
+        WARN("D-Bus connection failed with %s: %s\n", error->name, error->message);
+        return false;
+    }
+
+    /* Don't stupidly exit if the connection dies while doing this. */
+    dbus_connection_set_exit_on_disconnect(conn.get(), false);
+
+    int nicemin{};
+    int err{rtkit_get_min_nice_level(conn.get(), &nicemin)};
+    if(err == -ENOENT)
+    {
+        err = std::abs(err);
+        ERR("Could not query RTKit: %s (%d)\n", std::strerror(err), err);
+        return false;
+    }
+    int rtmax{rtkit_get_max_realtime_priority(conn.get())};
+    TRACE("Maximum real-time priority: %d, minimum niceness: %d\n", rtmax, nicemin);
+
+    auto limit_rttime = [](DBusConnection *c) -> int
+    {
+        using ulonglong = unsigned long long;
+        long long maxrttime{rtkit_get_rttime_usec_max(c)};
+        if(maxrttime <= 0) return static_cast<int>(std::abs(maxrttime));
+        const ulonglong umaxtime{static_cast<ulonglong>(maxrttime)};
+
+        struct rlimit rlim{};
+        if(getrlimit(RLIMIT_RTTIME, &rlim) != 0)
+            return errno;
+
+        TRACE("RTTime max: %llu (hard: %llu, soft: %llu)\n", umaxtime,
+            static_cast<ulonglong>(rlim.rlim_max), static_cast<ulonglong>(rlim.rlim_cur));
+        if(rlim.rlim_max > umaxtime)
+        {
+            rlim.rlim_max = static_cast<rlim_t>(std::min<ulonglong>(umaxtime,
+                std::numeric_limits<rlim_t>::max()));
+            rlim.rlim_cur = std::min(rlim.rlim_cur, rlim.rlim_max);
+            if(setrlimit(RLIMIT_RTTIME, &rlim) != 0)
+                return errno;
+        }
+        return 0;
+    };
+    if(rtmax > 0)
+    {
+        if(AllowRTTimeLimit)
+        {
+            err = limit_rttime(conn.get());
+            if(err != 0)
+                WARN("Failed to set RLIMIT_RTTIME for RTKit: %s (%d)\n",
+                    std::strerror(err), err);
+        }
+
+        /* Limit the maximum real-time priority to half. */
+        rtmax = (rtmax+1)/2;
+        prio = clampi(prio, 1, rtmax);
+
+        TRACE("Making real-time with priority %d (max: %d)\n", prio, rtmax);
+        err = rtkit_make_realtime(conn.get(), 0, prio);
+        if(err == 0) return true;
+
+        err = std::abs(err);
+        WARN("Failed to set real-time priority: %s (%d)\n", std::strerror(err), err);
+    }
+    /* Don't try to set the niceness for non-Linux systems. Standard POSIX has
+     * niceness as a per-process attribute, while the intent here is for the
+     * audio processing thread only to get a priority boost. Currently only
+     * Linux is known to have per-thread niceness.
+     */
+#ifdef __linux__
+    if(nicemin < 0)
+    {
+        TRACE("Making high priority with niceness %d\n", nicemin);
+        err = rtkit_make_high_priority(conn.get(), 0, nicemin);
+        if(err == 0) return true;
+
+        err = std::abs(err);
+        WARN("Failed to set high priority: %s (%d)\n", std::strerror(err), err);
+    }
+#endif /* __linux__ */
+
+#else
+
+    std::ignore = prio;
+    WARN("D-Bus not supported\n");
+#endif
+    return false;
+}
+
+} // namespace
+
+void SetRTPriority()
+{
+    if(RTPrioLevel <= 0)
+        return;
+
+    if(SetRTPriorityPthread(RTPrioLevel))
+        return;
+    if(SetRTPriorityRTKit(RTPrioLevel))
+        return;
+}
+
+#endif
diff --git a/core/helpers.h b/core/helpers.h
new file mode 100644 (file)
index 0000000..f0bfcf1
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef CORE_HELPERS_H
+#define CORE_HELPERS_H
+
+#include <string>
+
+#include "vector.h"
+
+
+struct PathNamePair { std::string path, fname; };
+const PathNamePair &GetProcBinary(void);
+
+extern int RTPrioLevel;
+extern bool AllowRTTimeLimit;
+void SetRTPriority(void);
+
+al::vector<std::string> SearchDataFiles(const char *match, const char *subdir);
+
+#endif /* CORE_HELPERS_H */
diff --git a/core/hrtf.cpp b/core/hrtf.cpp
new file mode 100644 (file)
index 0000000..d5c7573
--- /dev/null
@@ -0,0 +1,1473 @@
+
+#include "config.h"
+
+#include "hrtf.h"
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <cctype>
+#include <cmath>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <fstream>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <type_traits>
+#include <utility>
+
+#include "albit.h"
+#include "albyte.h"
+#include "alfstream.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "ambidefs.h"
+#include "filters/splitter.h"
+#include "helpers.h"
+#include "logging.h"
+#include "mixer/hrtfdefs.h"
+#include "opthelpers.h"
+#include "polyphase_resampler.h"
+#include "vector.h"
+
+
+namespace {
+
+struct HrtfEntry {
+    std::string mDispName;
+    std::string mFilename;
+
+    /* GCC warns when it tries to inline this. */
+    ~HrtfEntry();
+};
+HrtfEntry::~HrtfEntry() = default;
+
+struct LoadedHrtf {
+    std::string mFilename;
+    std::unique_ptr<HrtfStore> mEntry;
+
+    template<typename T, typename U>
+    LoadedHrtf(T&& name, U&& entry)
+        : mFilename{std::forward<T>(name)}, mEntry{std::forward<U>(entry)}
+    { }
+    LoadedHrtf(LoadedHrtf&&) = default;
+    /* GCC warns when it tries to inline this. */
+    ~LoadedHrtf();
+
+    LoadedHrtf& operator=(LoadedHrtf&&) = default;
+};
+LoadedHrtf::~LoadedHrtf() = default;
+
+
+/* Data set limits must be the same as or more flexible than those defined in
+ * the makemhr utility.
+ */
+constexpr uint MinFdCount{1};
+constexpr uint MaxFdCount{16};
+
+constexpr uint MinFdDistance{50};
+constexpr uint MaxFdDistance{2500};
+
+constexpr uint MinEvCount{5};
+constexpr uint MaxEvCount{181};
+
+constexpr uint MinAzCount{1};
+constexpr uint MaxAzCount{255};
+
+constexpr uint MaxHrirDelay{HrtfHistoryLength - 1};
+
+constexpr uint HrirDelayFracBits{2};
+constexpr uint HrirDelayFracOne{1 << HrirDelayFracBits};
+constexpr uint HrirDelayFracHalf{HrirDelayFracOne >> 1};
+
+static_assert(MaxHrirDelay*HrirDelayFracOne < 256, "MAX_HRIR_DELAY or DELAY_FRAC too large");
+
+constexpr char magicMarker00[8]{'M','i','n','P','H','R','0','0'};
+constexpr char magicMarker01[8]{'M','i','n','P','H','R','0','1'};
+constexpr char magicMarker02[8]{'M','i','n','P','H','R','0','2'};
+constexpr char magicMarker03[8]{'M','i','n','P','H','R','0','3'};
+
+/* First value for pass-through coefficients (remaining are 0), used for omni-
+ * directional sounds. */
+constexpr auto PassthruCoeff = static_cast<float>(1.0/al::numbers::sqrt2);
+
+std::mutex LoadedHrtfLock;
+al::vector<LoadedHrtf> LoadedHrtfs;
+
+std::mutex EnumeratedHrtfLock;
+al::vector<HrtfEntry> EnumeratedHrtfs;
+
+
+class databuf final : public std::streambuf {
+    int_type underflow() override
+    { return traits_type::eof(); }
+
+    pos_type seekoff(off_type offset, std::ios_base::seekdir whence, std::ios_base::openmode mode) override
+    {
+        if((mode&std::ios_base::out) || !(mode&std::ios_base::in))
+            return traits_type::eof();
+
+        char_type *cur;
+        switch(whence)
+        {
+            case std::ios_base::beg:
+                if(offset < 0 || offset > egptr()-eback())
+                    return traits_type::eof();
+                cur = eback() + offset;
+                break;
+
+            case std::ios_base::cur:
+                if((offset >= 0 && offset > egptr()-gptr()) ||
+                   (offset < 0 && -offset > gptr()-eback()))
+                    return traits_type::eof();
+                cur = gptr() + offset;
+                break;
+
+            case std::ios_base::end:
+                if(offset > 0 || -offset > egptr()-eback())
+                    return traits_type::eof();
+                cur = egptr() + offset;
+                break;
+
+            default:
+                return traits_type::eof();
+        }
+
+        setg(eback(), cur, egptr());
+        return cur - eback();
+    }
+
+    pos_type seekpos(pos_type pos, std::ios_base::openmode mode) override
+    {
+        // Simplified version of seekoff
+        if((mode&std::ios_base::out) || !(mode&std::ios_base::in))
+            return traits_type::eof();
+
+        if(pos < 0 || pos > egptr()-eback())
+            return traits_type::eof();
+
+        setg(eback(), eback() + static_cast<size_t>(pos), egptr());
+        return pos;
+    }
+
+public:
+    databuf(const char_type *start_, const char_type *end_) noexcept
+    {
+        setg(const_cast<char_type*>(start_), const_cast<char_type*>(start_),
+             const_cast<char_type*>(end_));
+    }
+};
+
+class idstream final : public std::istream {
+    databuf mStreamBuf;
+
+public:
+    idstream(const char *start_, const char *end_)
+      : std::istream{nullptr}, mStreamBuf{start_, end_}
+    { init(&mStreamBuf); }
+};
+
+
+struct IdxBlend { uint idx; float blend; };
+/* Calculate the elevation index given the polar elevation in radians. This
+ * will return an index between 0 and (evcount - 1).
+ */
+IdxBlend CalcEvIndex(uint evcount, float ev)
+{
+    ev = (al::numbers::pi_v<float>*0.5f + ev) * static_cast<float>(evcount-1) *
+        al::numbers::inv_pi_v<float>;
+    uint idx{float2uint(ev)};
+
+    return IdxBlend{minu(idx, evcount-1), ev-static_cast<float>(idx)};
+}
+
+/* Calculate the azimuth index given the polar azimuth in radians. This will
+ * return an index between 0 and (azcount - 1).
+ */
+IdxBlend CalcAzIndex(uint azcount, float az)
+{
+    az = (al::numbers::pi_v<float>*2.0f + az) * static_cast<float>(azcount) *
+        (al::numbers::inv_pi_v<float>*0.5f);
+    uint idx{float2uint(az)};
+
+    return IdxBlend{idx%azcount, az-static_cast<float>(idx)};
+}
+
+} // namespace
+
+
+/* Calculates static HRIR coefficients and delays for the given polar elevation
+ * and azimuth in radians. The coefficients are normalized.
+ */
+void HrtfStore::getCoeffs(float elevation, float azimuth, float distance, float spread,
+    HrirArray &coeffs, const al::span<uint,2> delays)
+{
+    const float dirfact{1.0f - (al::numbers::inv_pi_v<float>/2.0f * spread)};
+
+    size_t ebase{0};
+    auto match_field = [&ebase,distance](const Field &field) noexcept -> bool
+    {
+        if(distance >= field.distance)
+            return true;
+        ebase += field.evCount;
+        return false;
+    };
+    auto field = std::find_if(mFields.begin(), mFields.end()-1, match_field);
+
+    /* Calculate the elevation indices. */
+    const auto elev0 = CalcEvIndex(field->evCount, elevation);
+    const size_t elev1_idx{minu(elev0.idx+1, field->evCount-1)};
+    const size_t ir0offset{mElev[ebase + elev0.idx].irOffset};
+    const size_t ir1offset{mElev[ebase + elev1_idx].irOffset};
+
+    /* Calculate azimuth indices. */
+    const auto az0 = CalcAzIndex(mElev[ebase + elev0.idx].azCount, azimuth);
+    const auto az1 = CalcAzIndex(mElev[ebase + elev1_idx].azCount, azimuth);
+
+    /* Calculate the HRIR indices to blend. */
+    const size_t idx[4]{
+        ir0offset + az0.idx,
+        ir0offset + ((az0.idx+1) % mElev[ebase + elev0.idx].azCount),
+        ir1offset + az1.idx,
+        ir1offset + ((az1.idx+1) % mElev[ebase + elev1_idx].azCount)
+    };
+
+    /* Calculate bilinear blending weights, attenuated according to the
+     * directional panning factor.
+     */
+    const float blend[4]{
+        (1.0f-elev0.blend) * (1.0f-az0.blend) * dirfact,
+        (1.0f-elev0.blend) * (     az0.blend) * dirfact,
+        (     elev0.blend) * (1.0f-az1.blend) * dirfact,
+        (     elev0.blend) * (     az1.blend) * dirfact
+    };
+
+    /* Calculate the blended HRIR delays. */
+    float d{mDelays[idx[0]][0]*blend[0] + mDelays[idx[1]][0]*blend[1] + mDelays[idx[2]][0]*blend[2]
+        + mDelays[idx[3]][0]*blend[3]};
+    delays[0] = fastf2u(d * float{1.0f/HrirDelayFracOne});
+    d = mDelays[idx[0]][1]*blend[0] + mDelays[idx[1]][1]*blend[1] + mDelays[idx[2]][1]*blend[2]
+        + mDelays[idx[3]][1]*blend[3];
+    delays[1] = fastf2u(d * float{1.0f/HrirDelayFracOne});
+
+    /* Calculate the blended HRIR coefficients. */
+    float *coeffout{al::assume_aligned<16>(coeffs[0].data())};
+    coeffout[0] = PassthruCoeff * (1.0f-dirfact);
+    coeffout[1] = PassthruCoeff * (1.0f-dirfact);
+    std::fill_n(coeffout+2, size_t{HrirLength-1}*2, 0.0f);
+    for(size_t c{0};c < 4;c++)
+    {
+        const float *srccoeffs{al::assume_aligned<16>(mCoeffs[idx[c]][0].data())};
+        const float mult{blend[c]};
+        auto blend_coeffs = [mult](const float src, const float coeff) noexcept -> float
+        { return src*mult + coeff; };
+        std::transform(srccoeffs, srccoeffs + HrirLength*2, coeffout, coeffout, blend_coeffs);
+    }
+}
+
+
+std::unique_ptr<DirectHrtfState> DirectHrtfState::Create(size_t num_chans)
+{ return std::unique_ptr<DirectHrtfState>{new(FamCount(num_chans)) DirectHrtfState{num_chans}}; }
+
+void DirectHrtfState::build(const HrtfStore *Hrtf, const uint irSize, const bool perHrirMin,
+    const al::span<const AngularPoint> AmbiPoints, const float (*AmbiMatrix)[MaxAmbiChannels],
+    const float XOverFreq, const al::span<const float,MaxAmbiOrder+1> AmbiOrderHFGain)
+{
+    using double2 = std::array<double,2>;
+    struct ImpulseResponse {
+        const ConstHrirSpan hrir;
+        uint ldelay, rdelay;
+    };
+
+    const double xover_norm{double{XOverFreq} / Hrtf->mSampleRate};
+    mChannels[0].mSplitter.init(static_cast<float>(xover_norm));
+    for(size_t i{0};i < mChannels.size();++i)
+    {
+        const size_t order{AmbiIndex::OrderFromChannel()[i]};
+        mChannels[i].mSplitter = mChannels[0].mSplitter;
+        mChannels[i].mHfScale = AmbiOrderHFGain[order];
+    }
+
+    uint min_delay{HrtfHistoryLength*HrirDelayFracOne}, max_delay{0};
+    al::vector<ImpulseResponse> impres; impres.reserve(AmbiPoints.size());
+    auto calc_res = [Hrtf,&max_delay,&min_delay](const AngularPoint &pt) -> ImpulseResponse
+    {
+        auto &field = Hrtf->mFields[0];
+        const auto elev0 = CalcEvIndex(field.evCount, pt.Elev.value);
+        const size_t elev1_idx{minu(elev0.idx+1, field.evCount-1)};
+        const size_t ir0offset{Hrtf->mElev[elev0.idx].irOffset};
+        const size_t ir1offset{Hrtf->mElev[elev1_idx].irOffset};
+
+        const auto az0 = CalcAzIndex(Hrtf->mElev[elev0.idx].azCount, pt.Azim.value);
+        const auto az1 = CalcAzIndex(Hrtf->mElev[elev1_idx].azCount, pt.Azim.value);
+
+        const size_t idx[4]{
+            ir0offset + az0.idx,
+            ir0offset + ((az0.idx+1) % Hrtf->mElev[elev0.idx].azCount),
+            ir1offset + az1.idx,
+            ir1offset + ((az1.idx+1) % Hrtf->mElev[elev1_idx].azCount)
+        };
+
+        /* The largest blend factor serves as the closest HRIR. */
+        const size_t irOffset{idx[(elev0.blend >= 0.5f)*2 + (az1.blend >= 0.5f)]};
+        ImpulseResponse res{Hrtf->mCoeffs[irOffset],
+            Hrtf->mDelays[irOffset][0], Hrtf->mDelays[irOffset][1]};
+
+        min_delay = minu(min_delay, minu(res.ldelay, res.rdelay));
+        max_delay = maxu(max_delay, maxu(res.ldelay, res.rdelay));
+
+        return res;
+    };
+    std::transform(AmbiPoints.begin(), AmbiPoints.end(), std::back_inserter(impres), calc_res);
+    auto hrir_delay_round = [](const uint d) noexcept -> uint
+    { return (d+HrirDelayFracHalf) >> HrirDelayFracBits; };
+
+    TRACE("Min delay: %.2f, max delay: %.2f, FIR length: %u\n",
+        min_delay/double{HrirDelayFracOne}, max_delay/double{HrirDelayFracOne}, irSize);
+
+    auto tmpres = al::vector<std::array<double2,HrirLength>>(mChannels.size());
+    max_delay = 0;
+    for(size_t c{0u};c < AmbiPoints.size();++c)
+    {
+        const ConstHrirSpan hrir{impres[c].hrir};
+        const uint base_delay{perHrirMin ? minu(impres[c].ldelay, impres[c].rdelay) : min_delay};
+        const uint ldelay{hrir_delay_round(impres[c].ldelay - base_delay)};
+        const uint rdelay{hrir_delay_round(impres[c].rdelay - base_delay)};
+        max_delay = maxu(max_delay, maxu(impres[c].ldelay, impres[c].rdelay) - base_delay);
+
+        for(size_t i{0u};i < mChannels.size();++i)
+        {
+            const double mult{AmbiMatrix[c][i]};
+            const size_t numirs{HrirLength - maxz(ldelay, rdelay)};
+            size_t lidx{ldelay}, ridx{rdelay};
+            for(size_t j{0};j < numirs;++j)
+            {
+                tmpres[i][lidx++][0] += hrir[j][0] * mult;
+                tmpres[i][ridx++][1] += hrir[j][1] * mult;
+            }
+        }
+    }
+    impres.clear();
+
+    for(size_t i{0u};i < mChannels.size();++i)
+    {
+        auto copy_arr = [](const double2 &in) noexcept -> float2
+        { return float2{{static_cast<float>(in[0]), static_cast<float>(in[1])}}; };
+        std::transform(tmpres[i].cbegin(), tmpres[i].cend(), mChannels[i].mCoeffs.begin(),
+            copy_arr);
+    }
+    tmpres.clear();
+
+    const uint max_length{minu(hrir_delay_round(max_delay) + irSize, HrirLength)};
+    TRACE("New max delay: %.2f, FIR length: %u\n", max_delay/double{HrirDelayFracOne},
+        max_length);
+    mIrSize = max_length;
+}
+
+
+namespace {
+
+std::unique_ptr<HrtfStore> CreateHrtfStore(uint rate, uint8_t irSize,
+    const al::span<const HrtfStore::Field> fields,
+    const al::span<const HrtfStore::Elevation> elevs, const HrirArray *coeffs,
+    const ubyte2 *delays, const char *filename)
+{
+    const size_t irCount{size_t{elevs.back().azCount} + elevs.back().irOffset};
+    size_t total{sizeof(HrtfStore)};
+    total  = RoundUp(total, alignof(HrtfStore::Field)); /* Align for field infos */
+    total += sizeof(std::declval<HrtfStore&>().mFields[0])*fields.size();
+    total  = RoundUp(total, alignof(HrtfStore::Elevation)); /* Align for elevation infos */
+    total += sizeof(std::declval<HrtfStore&>().mElev[0])*elevs.size();
+    total  = RoundUp(total, 16); /* Align for coefficients using SIMD */
+    total += sizeof(std::declval<HrtfStore&>().mCoeffs[0])*irCount;
+    total += sizeof(std::declval<HrtfStore&>().mDelays[0])*irCount;
+
+    std::unique_ptr<HrtfStore> Hrtf{};
+    if(void *ptr{al_calloc(16, total)})
+    {
+        Hrtf.reset(al::construct_at(static_cast<HrtfStore*>(ptr)));
+        InitRef(Hrtf->mRef, 1u);
+        Hrtf->mSampleRate = rate;
+        Hrtf->mIrSize = irSize;
+
+        /* Set up pointers to storage following the main HRTF struct. */
+        char *base = reinterpret_cast<char*>(Hrtf.get());
+        size_t offset{sizeof(HrtfStore)};
+
+        offset = RoundUp(offset, alignof(HrtfStore::Field)); /* Align for field infos */
+        auto field_ = reinterpret_cast<HrtfStore::Field*>(base + offset);
+        offset += sizeof(field_[0])*fields.size();
+
+        offset = RoundUp(offset, alignof(HrtfStore::Elevation)); /* Align for elevation infos */
+        auto elev_ = reinterpret_cast<HrtfStore::Elevation*>(base + offset);
+        offset += sizeof(elev_[0])*elevs.size();
+
+        offset = RoundUp(offset, 16); /* Align for coefficients using SIMD */
+        auto coeffs_ = reinterpret_cast<HrirArray*>(base + offset);
+        offset += sizeof(coeffs_[0])*irCount;
+
+        auto delays_ = reinterpret_cast<ubyte2*>(base + offset);
+        offset += sizeof(delays_[0])*irCount;
+
+        if(offset != total)
+            throw std::runtime_error{"HrtfStore allocation size mismatch"};
+
+        /* Copy input data to storage. */
+        std::uninitialized_copy(fields.cbegin(), fields.cend(), field_);
+        std::uninitialized_copy(elevs.cbegin(), elevs.cend(), elev_);
+        std::uninitialized_copy_n(coeffs, irCount, coeffs_);
+        std::uninitialized_copy_n(delays, irCount, delays_);
+
+        /* Finally, assign the storage pointers. */
+        Hrtf->mFields = al::as_span(field_, fields.size());
+        Hrtf->mElev = elev_;
+        Hrtf->mCoeffs = coeffs_;
+        Hrtf->mDelays = delays_;
+    }
+    else
+        ERR("Out of memory allocating storage for %s.\n", filename);
+
+    return Hrtf;
+}
+
+void MirrorLeftHrirs(const al::span<const HrtfStore::Elevation> elevs, HrirArray *coeffs,
+    ubyte2 *delays)
+{
+    for(const auto &elev : elevs)
+    {
+        const ushort evoffset{elev.irOffset};
+        const ushort azcount{elev.azCount};
+        for(size_t j{0};j < azcount;j++)
+        {
+            const size_t lidx{evoffset + j};
+            const size_t ridx{evoffset + ((azcount-j) % azcount)};
+
+            const size_t irSize{coeffs[ridx].size()};
+            for(size_t k{0};k < irSize;k++)
+                coeffs[ridx][k][1] = coeffs[lidx][k][0];
+            delays[ridx][1] = delays[lidx][0];
+        }
+    }
+}
+
+
+template<size_t num_bits, typename T>
+constexpr std::enable_if_t<std::is_signed<T>::value && num_bits < sizeof(T)*8,
+T> fixsign(T value) noexcept
+{
+    constexpr auto signbit = static_cast<T>(1u << (num_bits-1));
+    return static_cast<T>((value^signbit) - signbit);
+}
+
+template<size_t num_bits, typename T>
+constexpr std::enable_if_t<!std::is_signed<T>::value || num_bits == sizeof(T)*8,
+T> fixsign(T value) noexcept
+{ return value; }
+
+template<typename T, size_t num_bits=sizeof(T)*8>
+inline std::enable_if_t<al::endian::native == al::endian::little,
+T> readle(std::istream &data)
+{
+    static_assert((num_bits&7) == 0, "num_bits must be a multiple of 8");
+    static_assert(num_bits <= sizeof(T)*8, "num_bits is too large for the type");
+
+    T ret{};
+    if(!data.read(reinterpret_cast<char*>(&ret), num_bits/8))
+        return static_cast<T>(EOF);
+
+    return fixsign<num_bits>(ret);
+}
+
+template<typename T, size_t num_bits=sizeof(T)*8>
+inline std::enable_if_t<al::endian::native == al::endian::big,
+T> readle(std::istream &data)
+{
+    static_assert((num_bits&7) == 0, "num_bits must be a multiple of 8");
+    static_assert(num_bits <= sizeof(T)*8, "num_bits is too large for the type");
+
+    T ret{};
+    al::byte b[sizeof(T)]{};
+    if(!data.read(reinterpret_cast<char*>(b), num_bits/8))
+        return static_cast<T>(EOF);
+    std::reverse_copy(std::begin(b), std::end(b), reinterpret_cast<al::byte*>(&ret));
+
+    return fixsign<num_bits>(ret);
+}
+
+template<>
+inline uint8_t readle<uint8_t,8>(std::istream &data)
+{ return static_cast<uint8_t>(data.get()); }
+
+
+std::unique_ptr<HrtfStore> LoadHrtf00(std::istream &data, const char *filename)
+{
+    uint rate{readle<uint32_t>(data)};
+    ushort irCount{readle<uint16_t>(data)};
+    ushort irSize{readle<uint16_t>(data)};
+    ubyte evCount{readle<uint8_t>(data)};
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+
+    if(irSize < MinIrLength || irSize > HrirLength)
+    {
+        ERR("Unsupported HRIR size, irSize=%d (%d to %d)\n", irSize, MinIrLength, HrirLength);
+        return nullptr;
+    }
+    if(evCount < MinEvCount || evCount > MaxEvCount)
+    {
+        ERR("Unsupported elevation count: evCount=%d (%d to %d)\n",
+            evCount, MinEvCount, MaxEvCount);
+        return nullptr;
+    }
+
+    auto elevs = al::vector<HrtfStore::Elevation>(evCount);
+    for(auto &elev : elevs)
+        elev.irOffset = readle<uint16_t>(data);
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+    for(size_t i{1};i < evCount;i++)
+    {
+        if(elevs[i].irOffset <= elevs[i-1].irOffset)
+        {
+            ERR("Invalid evOffset: evOffset[%zu]=%d (last=%d)\n", i, elevs[i].irOffset,
+                elevs[i-1].irOffset);
+            return nullptr;
+        }
+    }
+    if(irCount <= elevs.back().irOffset)
+    {
+        ERR("Invalid evOffset: evOffset[%zu]=%d (irCount=%d)\n",
+            elevs.size()-1, elevs.back().irOffset, irCount);
+        return nullptr;
+    }
+
+    for(size_t i{1};i < evCount;i++)
+    {
+        elevs[i-1].azCount = static_cast<ushort>(elevs[i].irOffset - elevs[i-1].irOffset);
+        if(elevs[i-1].azCount < MinAzCount || elevs[i-1].azCount > MaxAzCount)
+        {
+            ERR("Unsupported azimuth count: azCount[%zd]=%d (%d to %d)\n",
+                i-1, elevs[i-1].azCount, MinAzCount, MaxAzCount);
+            return nullptr;
+        }
+    }
+    elevs.back().azCount = static_cast<ushort>(irCount - elevs.back().irOffset);
+    if(elevs.back().azCount < MinAzCount || elevs.back().azCount > MaxAzCount)
+    {
+        ERR("Unsupported azimuth count: azCount[%zu]=%d (%d to %d)\n",
+            elevs.size()-1, elevs.back().azCount, MinAzCount, MaxAzCount);
+        return nullptr;
+    }
+
+    auto coeffs = al::vector<HrirArray>(irCount, HrirArray{});
+    auto delays = al::vector<ubyte2>(irCount);
+    for(auto &hrir : coeffs)
+    {
+        for(auto &val : al::span<float2>{hrir.data(), irSize})
+            val[0] = readle<int16_t>(data) / 32768.0f;
+    }
+    for(auto &val : delays)
+        val[0] = readle<uint8_t>(data);
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+    for(size_t i{0};i < irCount;i++)
+    {
+        if(delays[i][0] > MaxHrirDelay)
+        {
+            ERR("Invalid delays[%zd]: %d (%d)\n", i, delays[i][0], MaxHrirDelay);
+            return nullptr;
+        }
+        delays[i][0] <<= HrirDelayFracBits;
+    }
+
+    /* Mirror the left ear responses to the right ear. */
+    MirrorLeftHrirs({elevs.data(), elevs.size()}, coeffs.data(), delays.data());
+
+    const HrtfStore::Field field[1]{{0.0f, evCount}};
+    return CreateHrtfStore(rate, static_cast<uint8_t>(irSize), field, {elevs.data(), elevs.size()},
+        coeffs.data(), delays.data(), filename);
+}
+
+std::unique_ptr<HrtfStore> LoadHrtf01(std::istream &data, const char *filename)
+{
+    uint rate{readle<uint32_t>(data)};
+    uint8_t irSize{readle<uint8_t>(data)};
+    ubyte evCount{readle<uint8_t>(data)};
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+
+    if(irSize < MinIrLength || irSize > HrirLength)
+    {
+        ERR("Unsupported HRIR size, irSize=%d (%d to %d)\n", irSize, MinIrLength, HrirLength);
+        return nullptr;
+    }
+    if(evCount < MinEvCount || evCount > MaxEvCount)
+    {
+        ERR("Unsupported elevation count: evCount=%d (%d to %d)\n",
+            evCount, MinEvCount, MaxEvCount);
+        return nullptr;
+    }
+
+    auto elevs = al::vector<HrtfStore::Elevation>(evCount);
+    for(auto &elev : elevs)
+        elev.azCount = readle<uint8_t>(data);
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+    for(size_t i{0};i < evCount;++i)
+    {
+        if(elevs[i].azCount < MinAzCount || elevs[i].azCount > MaxAzCount)
+        {
+            ERR("Unsupported azimuth count: azCount[%zd]=%d (%d to %d)\n", i, elevs[i].azCount,
+                MinAzCount, MaxAzCount);
+            return nullptr;
+        }
+    }
+
+    elevs[0].irOffset = 0;
+    for(size_t i{1};i < evCount;i++)
+        elevs[i].irOffset = static_cast<ushort>(elevs[i-1].irOffset + elevs[i-1].azCount);
+    const ushort irCount{static_cast<ushort>(elevs.back().irOffset + elevs.back().azCount)};
+
+    auto coeffs = al::vector<HrirArray>(irCount, HrirArray{});
+    auto delays = al::vector<ubyte2>(irCount);
+    for(auto &hrir : coeffs)
+    {
+        for(auto &val : al::span<float2>{hrir.data(), irSize})
+            val[0] = readle<int16_t>(data) / 32768.0f;
+    }
+    for(auto &val : delays)
+        val[0] = readle<uint8_t>(data);
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+    for(size_t i{0};i < irCount;i++)
+    {
+        if(delays[i][0] > MaxHrirDelay)
+        {
+            ERR("Invalid delays[%zd]: %d (%d)\n", i, delays[i][0], MaxHrirDelay);
+            return nullptr;
+        }
+        delays[i][0] <<= HrirDelayFracBits;
+    }
+
+    /* Mirror the left ear responses to the right ear. */
+    MirrorLeftHrirs({elevs.data(), elevs.size()}, coeffs.data(), delays.data());
+
+    const HrtfStore::Field field[1]{{0.0f, evCount}};
+    return CreateHrtfStore(rate, irSize, field, {elevs.data(), elevs.size()}, coeffs.data(),
+        delays.data(), filename);
+}
+
+std::unique_ptr<HrtfStore> LoadHrtf02(std::istream &data, const char *filename)
+{
+    constexpr ubyte SampleType_S16{0};
+    constexpr ubyte SampleType_S24{1};
+    constexpr ubyte ChanType_LeftOnly{0};
+    constexpr ubyte ChanType_LeftRight{1};
+
+    uint rate{readle<uint32_t>(data)};
+    ubyte sampleType{readle<uint8_t>(data)};
+    ubyte channelType{readle<uint8_t>(data)};
+    uint8_t irSize{readle<uint8_t>(data)};
+    ubyte fdCount{readle<uint8_t>(data)};
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+
+    if(sampleType > SampleType_S24)
+    {
+        ERR("Unsupported sample type: %d\n", sampleType);
+        return nullptr;
+    }
+    if(channelType > ChanType_LeftRight)
+    {
+        ERR("Unsupported channel type: %d\n", channelType);
+        return nullptr;
+    }
+
+    if(irSize < MinIrLength || irSize > HrirLength)
+    {
+        ERR("Unsupported HRIR size, irSize=%d (%d to %d)\n", irSize, MinIrLength, HrirLength);
+        return nullptr;
+    }
+    if(fdCount < 1 || fdCount > MaxFdCount)
+    {
+        ERR("Unsupported number of field-depths: fdCount=%d (%d to %d)\n", fdCount, MinFdCount,
+            MaxFdCount);
+        return nullptr;
+    }
+
+    auto fields = al::vector<HrtfStore::Field>(fdCount);
+    auto elevs = al::vector<HrtfStore::Elevation>{};
+    for(size_t f{0};f < fdCount;f++)
+    {
+        const ushort distance{readle<uint16_t>(data)};
+        const ubyte evCount{readle<uint8_t>(data)};
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+
+        if(distance < MinFdDistance || distance > MaxFdDistance)
+        {
+            ERR("Unsupported field distance[%zu]=%d (%d to %d millimeters)\n", f, distance,
+                MinFdDistance, MaxFdDistance);
+            return nullptr;
+        }
+        if(evCount < MinEvCount || evCount > MaxEvCount)
+        {
+            ERR("Unsupported elevation count: evCount[%zu]=%d (%d to %d)\n", f, evCount,
+                MinEvCount, MaxEvCount);
+            return nullptr;
+        }
+
+        fields[f].distance = distance / 1000.0f;
+        fields[f].evCount = evCount;
+        if(f > 0 && fields[f].distance <= fields[f-1].distance)
+        {
+            ERR("Field distance[%zu] is not after previous (%f > %f)\n", f, fields[f].distance,
+                fields[f-1].distance);
+            return nullptr;
+        }
+
+        const size_t ebase{elevs.size()};
+        elevs.resize(ebase + evCount);
+        for(auto &elev : al::span<HrtfStore::Elevation>(elevs.data()+ebase, evCount))
+            elev.azCount = readle<uint8_t>(data);
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+
+        for(size_t e{0};e < evCount;e++)
+        {
+            if(elevs[ebase+e].azCount < MinAzCount || elevs[ebase+e].azCount > MaxAzCount)
+            {
+                ERR("Unsupported azimuth count: azCount[%zu][%zu]=%d (%d to %d)\n", f, e,
+                    elevs[ebase+e].azCount, MinAzCount, MaxAzCount);
+                return nullptr;
+            }
+        }
+    }
+
+    elevs[0].irOffset = 0;
+    std::partial_sum(elevs.cbegin(), elevs.cend(), elevs.begin(),
+        [](const HrtfStore::Elevation &last, const HrtfStore::Elevation &cur)
+            -> HrtfStore::Elevation
+        {
+            return HrtfStore::Elevation{cur.azCount,
+                static_cast<ushort>(last.azCount + last.irOffset)};
+        });
+    const auto irTotal = static_cast<ushort>(elevs.back().azCount + elevs.back().irOffset);
+
+    auto coeffs = al::vector<HrirArray>(irTotal, HrirArray{});
+    auto delays = al::vector<ubyte2>(irTotal);
+    if(channelType == ChanType_LeftOnly)
+    {
+        if(sampleType == SampleType_S16)
+        {
+            for(auto &hrir : coeffs)
+            {
+                for(auto &val : al::span<float2>{hrir.data(), irSize})
+                    val[0] = readle<int16_t>(data) / 32768.0f;
+            }
+        }
+        else if(sampleType == SampleType_S24)
+        {
+            for(auto &hrir : coeffs)
+            {
+                for(auto &val : al::span<float2>{hrir.data(), irSize})
+                    val[0] = static_cast<float>(readle<int,24>(data)) / 8388608.0f;
+            }
+        }
+        for(auto &val : delays)
+            val[0] = readle<uint8_t>(data);
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+        for(size_t i{0};i < irTotal;++i)
+        {
+            if(delays[i][0] > MaxHrirDelay)
+            {
+                ERR("Invalid delays[%zu][0]: %d (%d)\n", i, delays[i][0], MaxHrirDelay);
+                return nullptr;
+            }
+            delays[i][0] <<= HrirDelayFracBits;
+        }
+
+        /* Mirror the left ear responses to the right ear. */
+        MirrorLeftHrirs({elevs.data(), elevs.size()}, coeffs.data(), delays.data());
+    }
+    else if(channelType == ChanType_LeftRight)
+    {
+        if(sampleType == SampleType_S16)
+        {
+            for(auto &hrir : coeffs)
+            {
+                for(auto &val : al::span<float2>{hrir.data(), irSize})
+                {
+                    val[0] = readle<int16_t>(data) / 32768.0f;
+                    val[1] = readle<int16_t>(data) / 32768.0f;
+                }
+            }
+        }
+        else if(sampleType == SampleType_S24)
+        {
+            for(auto &hrir : coeffs)
+            {
+                for(auto &val : al::span<float2>{hrir.data(), irSize})
+                {
+                    val[0] = static_cast<float>(readle<int,24>(data)) / 8388608.0f;
+                    val[1] = static_cast<float>(readle<int,24>(data)) / 8388608.0f;
+                }
+            }
+        }
+        for(auto &val : delays)
+        {
+            val[0] = readle<uint8_t>(data);
+            val[1] = readle<uint8_t>(data);
+        }
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+
+        for(size_t i{0};i < irTotal;++i)
+        {
+            if(delays[i][0] > MaxHrirDelay)
+            {
+                ERR("Invalid delays[%zu][0]: %d (%d)\n", i, delays[i][0], MaxHrirDelay);
+                return nullptr;
+            }
+            if(delays[i][1] > MaxHrirDelay)
+            {
+                ERR("Invalid delays[%zu][1]: %d (%d)\n", i, delays[i][1], MaxHrirDelay);
+                return nullptr;
+            }
+            delays[i][0] <<= HrirDelayFracBits;
+            delays[i][1] <<= HrirDelayFracBits;
+        }
+    }
+
+    if(fdCount > 1)
+    {
+        auto fields_ = al::vector<HrtfStore::Field>(fields.size());
+        auto elevs_ = al::vector<HrtfStore::Elevation>(elevs.size());
+        auto coeffs_ = al::vector<HrirArray>(coeffs.size());
+        auto delays_ = al::vector<ubyte2>(delays.size());
+
+        /* Simple reverse for the per-field elements. */
+        std::reverse_copy(fields.cbegin(), fields.cend(), fields_.begin());
+
+        /* Each field has a group of elevations, which each have an azimuth
+         * count. Reverse the order of the groups, keeping the relative order
+         * of per-group azimuth counts.
+         */
+        auto elevs__end = elevs_.end();
+        auto copy_azs = [&elevs,&elevs__end](const ptrdiff_t ebase, const HrtfStore::Field &field)
+            -> ptrdiff_t
+        {
+            auto elevs_src = elevs.begin()+ebase;
+            elevs__end = std::copy_backward(elevs_src, elevs_src+field.evCount, elevs__end);
+            return ebase + field.evCount;
+        };
+        (void)std::accumulate(fields.cbegin(), fields.cend(), ptrdiff_t{0}, copy_azs);
+        assert(elevs_.begin() == elevs__end);
+
+        /* Reestablish the IR offset for each elevation index, given the new
+         * ordering of elevations.
+         */
+        elevs_[0].irOffset = 0;
+        std::partial_sum(elevs_.cbegin(), elevs_.cend(), elevs_.begin(),
+            [](const HrtfStore::Elevation &last, const HrtfStore::Elevation &cur)
+                -> HrtfStore::Elevation
+            {
+                return HrtfStore::Elevation{cur.azCount,
+                    static_cast<ushort>(last.azCount + last.irOffset)};
+            });
+
+        /* Reverse the order of each field's group of IRs. */
+        auto coeffs_end = coeffs_.end();
+        auto delays_end = delays_.end();
+        auto copy_irs = [&elevs,&coeffs,&delays,&coeffs_end,&delays_end](
+            const ptrdiff_t ebase, const HrtfStore::Field &field) -> ptrdiff_t
+        {
+            auto accum_az = [](int count, const HrtfStore::Elevation &elev) noexcept -> int
+            { return count + elev.azCount; };
+            const auto elevs_mid = elevs.cbegin() + ebase;
+            const auto elevs_end = elevs_mid + field.evCount;
+            const int abase{std::accumulate(elevs.cbegin(), elevs_mid, 0, accum_az)};
+            const int num_azs{std::accumulate(elevs_mid, elevs_end, 0, accum_az)};
+
+            coeffs_end = std::copy_backward(coeffs.cbegin() + abase,
+                coeffs.cbegin() + (abase+num_azs), coeffs_end);
+            delays_end = std::copy_backward(delays.cbegin() + abase,
+                delays.cbegin() + (abase+num_azs), delays_end);
+
+            return ebase + field.evCount;
+        };
+        (void)std::accumulate(fields.cbegin(), fields.cend(), ptrdiff_t{0}, copy_irs);
+        assert(coeffs_.begin() == coeffs_end);
+        assert(delays_.begin() == delays_end);
+
+        fields = std::move(fields_);
+        elevs = std::move(elevs_);
+        coeffs = std::move(coeffs_);
+        delays = std::move(delays_);
+    }
+
+    return CreateHrtfStore(rate, irSize, {fields.data(), fields.size()},
+        {elevs.data(), elevs.size()}, coeffs.data(), delays.data(), filename);
+}
+
+std::unique_ptr<HrtfStore> LoadHrtf03(std::istream &data, const char *filename)
+{
+    constexpr ubyte ChanType_LeftOnly{0};
+    constexpr ubyte ChanType_LeftRight{1};
+
+    uint rate{readle<uint32_t>(data)};
+    ubyte channelType{readle<uint8_t>(data)};
+    uint8_t irSize{readle<uint8_t>(data)};
+    ubyte fdCount{readle<uint8_t>(data)};
+    if(!data || data.eof())
+    {
+        ERR("Failed reading %s\n", filename);
+        return nullptr;
+    }
+
+    if(channelType > ChanType_LeftRight)
+    {
+        ERR("Unsupported channel type: %d\n", channelType);
+        return nullptr;
+    }
+
+    if(irSize < MinIrLength || irSize > HrirLength)
+    {
+        ERR("Unsupported HRIR size, irSize=%d (%d to %d)\n", irSize, MinIrLength, HrirLength);
+        return nullptr;
+    }
+    if(fdCount < 1 || fdCount > MaxFdCount)
+    {
+        ERR("Unsupported number of field-depths: fdCount=%d (%d to %d)\n", fdCount, MinFdCount,
+            MaxFdCount);
+        return nullptr;
+    }
+
+    auto fields = al::vector<HrtfStore::Field>(fdCount);
+    auto elevs = al::vector<HrtfStore::Elevation>{};
+    for(size_t f{0};f < fdCount;f++)
+    {
+        const ushort distance{readle<uint16_t>(data)};
+        const ubyte evCount{readle<uint8_t>(data)};
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+
+        if(distance < MinFdDistance || distance > MaxFdDistance)
+        {
+            ERR("Unsupported field distance[%zu]=%d (%d to %d millimeters)\n", f, distance,
+                MinFdDistance, MaxFdDistance);
+            return nullptr;
+        }
+        if(evCount < MinEvCount || evCount > MaxEvCount)
+        {
+            ERR("Unsupported elevation count: evCount[%zu]=%d (%d to %d)\n", f, evCount,
+                MinEvCount, MaxEvCount);
+            return nullptr;
+        }
+
+        fields[f].distance = distance / 1000.0f;
+        fields[f].evCount = evCount;
+        if(f > 0 && fields[f].distance > fields[f-1].distance)
+        {
+            ERR("Field distance[%zu] is not before previous (%f <= %f)\n", f, fields[f].distance,
+                fields[f-1].distance);
+            return nullptr;
+        }
+
+        const size_t ebase{elevs.size()};
+        elevs.resize(ebase + evCount);
+        for(auto &elev : al::span<HrtfStore::Elevation>(elevs.data()+ebase, evCount))
+            elev.azCount = readle<uint8_t>(data);
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+
+        for(size_t e{0};e < evCount;e++)
+        {
+            if(elevs[ebase+e].azCount < MinAzCount || elevs[ebase+e].azCount > MaxAzCount)
+            {
+                ERR("Unsupported azimuth count: azCount[%zu][%zu]=%d (%d to %d)\n", f, e,
+                    elevs[ebase+e].azCount, MinAzCount, MaxAzCount);
+                return nullptr;
+            }
+        }
+    }
+
+    elevs[0].irOffset = 0;
+    std::partial_sum(elevs.cbegin(), elevs.cend(), elevs.begin(),
+        [](const HrtfStore::Elevation &last, const HrtfStore::Elevation &cur)
+            -> HrtfStore::Elevation
+        {
+            return HrtfStore::Elevation{cur.azCount,
+                static_cast<ushort>(last.azCount + last.irOffset)};
+        });
+    const auto irTotal = static_cast<ushort>(elevs.back().azCount + elevs.back().irOffset);
+
+    auto coeffs = al::vector<HrirArray>(irTotal, HrirArray{});
+    auto delays = al::vector<ubyte2>(irTotal);
+    if(channelType == ChanType_LeftOnly)
+    {
+        for(auto &hrir : coeffs)
+        {
+            for(auto &val : al::span<float2>{hrir.data(), irSize})
+                val[0] = static_cast<float>(readle<int,24>(data)) / 8388608.0f;
+        }
+        for(auto &val : delays)
+            val[0] = readle<uint8_t>(data);
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+        for(size_t i{0};i < irTotal;++i)
+        {
+            if(delays[i][0] > MaxHrirDelay<<HrirDelayFracBits)
+            {
+                ERR("Invalid delays[%zu][0]: %f (%d)\n", i,
+                    delays[i][0] / float{HrirDelayFracOne}, MaxHrirDelay);
+                return nullptr;
+            }
+        }
+
+        /* Mirror the left ear responses to the right ear. */
+        MirrorLeftHrirs({elevs.data(), elevs.size()}, coeffs.data(), delays.data());
+    }
+    else if(channelType == ChanType_LeftRight)
+    {
+        for(auto &hrir : coeffs)
+        {
+            for(auto &val : al::span<float2>{hrir.data(), irSize})
+            {
+                val[0] = static_cast<float>(readle<int,24>(data)) / 8388608.0f;
+                val[1] = static_cast<float>(readle<int,24>(data)) / 8388608.0f;
+            }
+        }
+        for(auto &val : delays)
+        {
+            val[0] = readle<uint8_t>(data);
+            val[1] = readle<uint8_t>(data);
+        }
+        if(!data || data.eof())
+        {
+            ERR("Failed reading %s\n", filename);
+            return nullptr;
+        }
+
+        for(size_t i{0};i < irTotal;++i)
+        {
+            if(delays[i][0] > MaxHrirDelay<<HrirDelayFracBits)
+            {
+                ERR("Invalid delays[%zu][0]: %f (%d)\n", i,
+                    delays[i][0] / float{HrirDelayFracOne}, MaxHrirDelay);
+                return nullptr;
+            }
+            if(delays[i][1] > MaxHrirDelay<<HrirDelayFracBits)
+            {
+                ERR("Invalid delays[%zu][1]: %f (%d)\n", i,
+                    delays[i][1] / float{HrirDelayFracOne}, MaxHrirDelay);
+                return nullptr;
+            }
+        }
+    }
+
+    return CreateHrtfStore(rate, irSize, {fields.data(), fields.size()},
+        {elevs.data(), elevs.size()}, coeffs.data(), delays.data(), filename);
+}
+
+
+bool checkName(const std::string &name)
+{
+    auto match_name = [&name](const HrtfEntry &entry) -> bool { return name == entry.mDispName; };
+    auto &enum_names = EnumeratedHrtfs;
+    return std::find_if(enum_names.cbegin(), enum_names.cend(), match_name) != enum_names.cend();
+}
+
+void AddFileEntry(const std::string &filename)
+{
+    /* Check if this file has already been enumerated. */
+    auto enum_iter = std::find_if(EnumeratedHrtfs.cbegin(), EnumeratedHrtfs.cend(),
+        [&filename](const HrtfEntry &entry) -> bool
+        { return entry.mFilename == filename; });
+    if(enum_iter != EnumeratedHrtfs.cend())
+    {
+        TRACE("Skipping duplicate file entry %s\n", filename.c_str());
+        return;
+    }
+
+    /* TODO: Get a human-readable name from the HRTF data (possibly coming in a
+     * format update). */
+    size_t namepos{filename.find_last_of('/')+1};
+    if(!namepos) namepos = filename.find_last_of('\\')+1;
+
+    size_t extpos{filename.find_last_of('.')};
+    if(extpos <= namepos) extpos = std::string::npos;
+
+    const std::string basename{(extpos == std::string::npos) ?
+        filename.substr(namepos) : filename.substr(namepos, extpos-namepos)};
+    std::string newname{basename};
+    int count{1};
+    while(checkName(newname))
+    {
+        newname = basename;
+        newname += " #";
+        newname += std::to_string(++count);
+    }
+    EnumeratedHrtfs.emplace_back(HrtfEntry{newname, filename});
+    const HrtfEntry &entry = EnumeratedHrtfs.back();
+
+    TRACE("Adding file entry \"%s\"\n", entry.mFilename.c_str());
+}
+
+/* Unfortunate that we have to duplicate AddFileEntry to take a memory buffer
+ * for input instead of opening the given filename.
+ */
+void AddBuiltInEntry(const std::string &dispname, uint residx)
+{
+    const std::string filename{'!'+std::to_string(residx)+'_'+dispname};
+
+    auto enum_iter = std::find_if(EnumeratedHrtfs.cbegin(), EnumeratedHrtfs.cend(),
+        [&filename](const HrtfEntry &entry) -> bool
+        { return entry.mFilename == filename; });
+    if(enum_iter != EnumeratedHrtfs.cend())
+    {
+        TRACE("Skipping duplicate file entry %s\n", filename.c_str());
+        return;
+    }
+
+    /* TODO: Get a human-readable name from the HRTF data (possibly coming in a
+     * format update). */
+
+    std::string newname{dispname};
+    int count{1};
+    while(checkName(newname))
+    {
+        newname = dispname;
+        newname += " #";
+        newname += std::to_string(++count);
+    }
+    EnumeratedHrtfs.emplace_back(HrtfEntry{newname, filename});
+    const HrtfEntry &entry = EnumeratedHrtfs.back();
+
+    TRACE("Adding built-in entry \"%s\"\n", entry.mFilename.c_str());
+}
+
+
+#define IDR_DEFAULT_HRTF_MHR 1
+
+#ifndef ALSOFT_EMBED_HRTF_DATA
+
+al::span<const char> GetResource(int /*name*/)
+{ return {}; }
+
+#else
+
+constexpr unsigned char hrtf_default[]{
+#include "default_hrtf.txt"
+};
+
+al::span<const char> GetResource(int name)
+{
+    if(name == IDR_DEFAULT_HRTF_MHR)
+        return {reinterpret_cast<const char*>(hrtf_default), sizeof(hrtf_default)};
+    return {};
+}
+#endif
+
+} // namespace
+
+
+al::vector<std::string> EnumerateHrtf(al::optional<std::string> pathopt)
+{
+    std::lock_guard<std::mutex> _{EnumeratedHrtfLock};
+    EnumeratedHrtfs.clear();
+
+    bool usedefaults{true};
+    if(pathopt)
+    {
+        const char *pathlist{pathopt->c_str()};
+        while(pathlist && *pathlist)
+        {
+            const char *next, *end;
+
+            while(isspace(*pathlist) || *pathlist == ',')
+                pathlist++;
+            if(*pathlist == '\0')
+                continue;
+
+            next = strchr(pathlist, ',');
+            if(next)
+                end = next++;
+            else
+            {
+                end = pathlist + strlen(pathlist);
+                usedefaults = false;
+            }
+
+            while(end != pathlist && isspace(*(end-1)))
+                --end;
+            if(end != pathlist)
+            {
+                const std::string pname{pathlist, end};
+                for(const auto &fname : SearchDataFiles(".mhr", pname.c_str()))
+                    AddFileEntry(fname);
+            }
+
+            pathlist = next;
+        }
+    }
+
+    if(usedefaults)
+    {
+        for(const auto &fname : SearchDataFiles(".mhr", "openal/hrtf"))
+            AddFileEntry(fname);
+
+        if(!GetResource(IDR_DEFAULT_HRTF_MHR).empty())
+            AddBuiltInEntry("Built-In HRTF", IDR_DEFAULT_HRTF_MHR);
+    }
+
+    al::vector<std::string> list;
+    list.reserve(EnumeratedHrtfs.size());
+    for(auto &entry : EnumeratedHrtfs)
+        list.emplace_back(entry.mDispName);
+
+    return list;
+}
+
+HrtfStorePtr GetLoadedHrtf(const std::string &name, const uint devrate)
+{
+    std::lock_guard<std::mutex> _{EnumeratedHrtfLock};
+    auto entry_iter = std::find_if(EnumeratedHrtfs.cbegin(), EnumeratedHrtfs.cend(),
+        [&name](const HrtfEntry &entry) -> bool { return entry.mDispName == name; });
+    if(entry_iter == EnumeratedHrtfs.cend())
+        return nullptr;
+    const std::string &fname = entry_iter->mFilename;
+
+    std::lock_guard<std::mutex> __{LoadedHrtfLock};
+    auto hrtf_lt_fname = [](LoadedHrtf &hrtf, const std::string &filename) -> bool
+    { return hrtf.mFilename < filename; };
+    auto handle = std::lower_bound(LoadedHrtfs.begin(), LoadedHrtfs.end(), fname, hrtf_lt_fname);
+    while(handle != LoadedHrtfs.end() && handle->mFilename == fname)
+    {
+        HrtfStore *hrtf{handle->mEntry.get()};
+        if(hrtf && hrtf->mSampleRate == devrate)
+        {
+            hrtf->add_ref();
+            return HrtfStorePtr{hrtf};
+        }
+        ++handle;
+    }
+
+    std::unique_ptr<std::istream> stream;
+    int residx{};
+    char ch{};
+    if(sscanf(fname.c_str(), "!%d%c", &residx, &ch) == 2 && ch == '_')
+    {
+        TRACE("Loading %s...\n", fname.c_str());
+        al::span<const char> res{GetResource(residx)};
+        if(res.empty())
+        {
+            ERR("Could not get resource %u, %s\n", residx, name.c_str());
+            return nullptr;
+        }
+        stream = std::make_unique<idstream>(res.begin(), res.end());
+    }
+    else
+    {
+        TRACE("Loading %s...\n", fname.c_str());
+        auto fstr = std::make_unique<al::ifstream>(fname.c_str(), std::ios::binary);
+        if(!fstr->is_open())
+        {
+            ERR("Could not open %s\n", fname.c_str());
+            return nullptr;
+        }
+        stream = std::move(fstr);
+    }
+
+    std::unique_ptr<HrtfStore> hrtf;
+    char magic[sizeof(magicMarker03)];
+    stream->read(magic, sizeof(magic));
+    if(stream->gcount() < static_cast<std::streamsize>(sizeof(magicMarker03)))
+        ERR("%s data is too short (%zu bytes)\n", name.c_str(), stream->gcount());
+    else if(memcmp(magic, magicMarker03, sizeof(magicMarker03)) == 0)
+    {
+        TRACE("Detected data set format v3\n");
+        hrtf = LoadHrtf03(*stream, name.c_str());
+    }
+    else if(memcmp(magic, magicMarker02, sizeof(magicMarker02)) == 0)
+    {
+        TRACE("Detected data set format v2\n");
+        hrtf = LoadHrtf02(*stream, name.c_str());
+    }
+    else if(memcmp(magic, magicMarker01, sizeof(magicMarker01)) == 0)
+    {
+        TRACE("Detected data set format v1\n");
+        hrtf = LoadHrtf01(*stream, name.c_str());
+    }
+    else if(memcmp(magic, magicMarker00, sizeof(magicMarker00)) == 0)
+    {
+        TRACE("Detected data set format v0\n");
+        hrtf = LoadHrtf00(*stream, name.c_str());
+    }
+    else
+        ERR("Invalid header in %s: \"%.8s\"\n", name.c_str(), magic);
+    stream.reset();
+
+    if(!hrtf)
+    {
+        ERR("Failed to load %s\n", name.c_str());
+        return nullptr;
+    }
+
+    if(hrtf->mSampleRate != devrate)
+    {
+        TRACE("Resampling HRTF %s (%uhz -> %uhz)\n", name.c_str(), hrtf->mSampleRate, devrate);
+
+        /* Calculate the last elevation's index and get the total IR count. */
+        const size_t lastEv{std::accumulate(hrtf->mFields.begin(), hrtf->mFields.end(), size_t{0},
+            [](const size_t curval, const HrtfStore::Field &field) noexcept -> size_t
+            { return curval + field.evCount; }
+        ) - 1};
+        const size_t irCount{size_t{hrtf->mElev[lastEv].irOffset} + hrtf->mElev[lastEv].azCount};
+
+        /* Resample all the IRs. */
+        std::array<std::array<double,HrirLength>,2> inout;
+        PPhaseResampler rs;
+        rs.init(hrtf->mSampleRate, devrate);
+        for(size_t i{0};i < irCount;++i)
+        {
+            HrirArray &coeffs = const_cast<HrirArray&>(hrtf->mCoeffs[i]);
+            for(size_t j{0};j < 2;++j)
+            {
+                std::transform(coeffs.cbegin(), coeffs.cend(), inout[0].begin(),
+                    [j](const float2 &in) noexcept -> double { return in[j]; });
+                rs.process(HrirLength, inout[0].data(), HrirLength, inout[1].data());
+                for(size_t k{0};k < HrirLength;++k)
+                    coeffs[k][j] = static_cast<float>(inout[1][k]);
+            }
+        }
+        rs = {};
+
+        /* Scale the delays for the new sample rate. */
+        float max_delay{0.0f};
+        auto new_delays = al::vector<float2>(irCount);
+        const float rate_scale{static_cast<float>(devrate)/static_cast<float>(hrtf->mSampleRate)};
+        for(size_t i{0};i < irCount;++i)
+        {
+            for(size_t j{0};j < 2;++j)
+            {
+                const float new_delay{std::round(hrtf->mDelays[i][j] * rate_scale) /
+                    float{HrirDelayFracOne}};
+                max_delay = maxf(max_delay, new_delay);
+                new_delays[i][j] = new_delay;
+            }
+        }
+
+        /* If the new delays exceed the max, scale it down to fit (essentially
+         * shrinking the head radius; not ideal but better than a per-delay
+         * clamp).
+         */
+        float delay_scale{HrirDelayFracOne};
+        if(max_delay > MaxHrirDelay)
+        {
+            WARN("Resampled delay exceeds max (%.2f > %d)\n", max_delay, MaxHrirDelay);
+            delay_scale *= float{MaxHrirDelay} / max_delay;
+        }
+
+        for(size_t i{0};i < irCount;++i)
+        {
+            ubyte2 &delays = const_cast<ubyte2&>(hrtf->mDelays[i]);
+            for(size_t j{0};j < 2;++j)
+                delays[j] = static_cast<ubyte>(float2int(new_delays[i][j]*delay_scale + 0.5f));
+        }
+
+        /* Scale the IR size for the new sample rate and update the stored
+         * sample rate.
+         */
+        const float newIrSize{std::round(static_cast<float>(hrtf->mIrSize) * rate_scale)};
+        hrtf->mIrSize = static_cast<uint8_t>(minf(HrirLength, newIrSize));
+        hrtf->mSampleRate = devrate;
+    }
+
+    TRACE("Loaded HRTF %s for sample rate %uhz, %u-sample filter\n", name.c_str(),
+        hrtf->mSampleRate, hrtf->mIrSize);
+    handle = LoadedHrtfs.emplace(handle, fname, std::move(hrtf));
+
+    return HrtfStorePtr{handle->mEntry.get()};
+}
+
+
+void HrtfStore::add_ref()
+{
+    auto ref = IncrementRef(mRef);
+    TRACE("HrtfStore %p increasing refcount to %u\n", decltype(std::declval<void*>()){this}, ref);
+}
+
+void HrtfStore::dec_ref()
+{
+    auto ref = DecrementRef(mRef);
+    TRACE("HrtfStore %p decreasing refcount to %u\n", decltype(std::declval<void*>()){this}, ref);
+    if(ref == 0)
+    {
+        std::lock_guard<std::mutex> _{LoadedHrtfLock};
+
+        /* Go through and remove all unused HRTFs. */
+        auto remove_unused = [](LoadedHrtf &hrtf) -> bool
+        {
+            HrtfStore *entry{hrtf.mEntry.get()};
+            if(entry && ReadRef(entry->mRef) == 0)
+            {
+                TRACE("Unloading unused HRTF %s\n", hrtf.mFilename.data());
+                hrtf.mEntry = nullptr;
+                return true;
+            }
+            return false;
+        };
+        auto iter = std::remove_if(LoadedHrtfs.begin(), LoadedHrtfs.end(), remove_unused);
+        LoadedHrtfs.erase(iter, LoadedHrtfs.end());
+    }
+}
diff --git a/core/hrtf.h b/core/hrtf.h
new file mode 100644 (file)
index 0000000..eb18682
--- /dev/null
@@ -0,0 +1,89 @@
+#ifndef CORE_HRTF_H
+#define CORE_HRTF_H
+
+#include <array>
+#include <cstddef>
+#include <memory>
+#include <string>
+
+#include "almalloc.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "atomic.h"
+#include "ambidefs.h"
+#include "bufferline.h"
+#include "mixer/hrtfdefs.h"
+#include "intrusive_ptr.h"
+#include "vector.h"
+
+
+struct HrtfStore {
+    RefCount mRef;
+
+    uint mSampleRate : 24;
+    uint mIrSize : 8;
+
+    struct Field {
+        float distance;
+        ubyte evCount;
+    };
+    /* NOTE: Fields are stored *backwards*. field[0] is the farthest field, and
+     * field[fdCount-1] is the nearest.
+     */
+    al::span<const Field> mFields;
+
+    struct Elevation {
+        ushort azCount;
+        ushort irOffset;
+    };
+    Elevation *mElev;
+    const HrirArray *mCoeffs;
+    const ubyte2 *mDelays;
+
+    void getCoeffs(float elevation, float azimuth, float distance, float spread, HrirArray &coeffs,
+        const al::span<uint,2> delays);
+
+    void add_ref();
+    void dec_ref();
+
+    DEF_PLACE_NEWDEL()
+};
+using HrtfStorePtr = al::intrusive_ptr<HrtfStore>;
+
+
+struct EvRadians { float value; };
+struct AzRadians { float value; };
+struct AngularPoint {
+    EvRadians Elev;
+    AzRadians Azim;
+};
+
+
+struct DirectHrtfState {
+    std::array<float,BufferLineSize> mTemp;
+
+    /* HRTF filter state for dry buffer content */
+    uint mIrSize{0};
+    al::FlexArray<HrtfChannelState> mChannels;
+
+    DirectHrtfState(size_t numchans) : mChannels{numchans} { }
+    /**
+     * Produces HRTF filter coefficients for decoding B-Format, given a set of
+     * virtual speaker positions, a matching decoding matrix, and per-order
+     * high-frequency gains for the decoder. The calculated impulse responses
+     * are ordered and scaled according to the matrix input.
+     */
+    void build(const HrtfStore *Hrtf, const uint irSize, const bool perHrirMin,
+        const al::span<const AngularPoint> AmbiPoints, const float (*AmbiMatrix)[MaxAmbiChannels],
+        const float XOverFreq, const al::span<const float,MaxAmbiOrder+1> AmbiOrderHFGain);
+
+    static std::unique_ptr<DirectHrtfState> Create(size_t num_chans);
+
+    DEF_FAM_NEWDEL(DirectHrtfState, mChannels)
+};
+
+
+al::vector<std::string> EnumerateHrtf(al::optional<std::string> pathopt);
+HrtfStorePtr GetLoadedHrtf(const std::string &name, const uint devrate);
+
+#endif /* CORE_HRTF_H */
diff --git a/core/logging.cpp b/core/logging.cpp
new file mode 100644 (file)
index 0000000..34a95e5
--- /dev/null
@@ -0,0 +1,89 @@
+
+#include "config.h"
+
+#include "logging.h"
+
+#include <cstdarg>
+#include <cstdio>
+#include <string>
+
+#include "alspan.h"
+#include "strutils.h"
+#include "vector.h"
+
+
+#if defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#elif defined(__ANDROID__)
+#include <android/log.h>
+#endif
+
+void al_print(LogLevel level, FILE *logfile, const char *fmt, ...)
+{
+    /* Kind of ugly since string literals are const char arrays with a size
+     * that includes the null terminator, which we want to exclude from the
+     * span.
+     */
+    auto prefix = al::as_span("[ALSOFT] (--) ").first<14>();
+    switch(level)
+    {
+    case LogLevel::Disable: break;
+    case LogLevel::Error: prefix = al::as_span("[ALSOFT] (EE) ").first<14>(); break;
+    case LogLevel::Warning: prefix = al::as_span("[ALSOFT] (WW) ").first<14>(); break;
+    case LogLevel::Trace: prefix = al::as_span("[ALSOFT] (II) ").first<14>(); break;
+    }
+
+    al::vector<char> dynmsg;
+    std::array<char,256> stcmsg{};
+
+    char *str{stcmsg.data()};
+    auto prefend1 = std::copy_n(prefix.begin(), prefix.size(), stcmsg.begin());
+    al::span<char> msg{prefend1, stcmsg.end()};
+
+    std::va_list args, args2;
+    va_start(args, fmt);
+    va_copy(args2, args);
+    const int msglen{std::vsnprintf(msg.data(), msg.size(), fmt, args)};
+    if(msglen >= 0 && static_cast<size_t>(msglen) >= msg.size()) UNLIKELY
+    {
+        dynmsg.resize(static_cast<size_t>(msglen)+prefix.size() + 1u);
+
+        str = dynmsg.data();
+        auto prefend2 = std::copy_n(prefix.begin(), prefix.size(), dynmsg.begin());
+        msg = {prefend2, dynmsg.end()};
+
+        std::vsnprintf(msg.data(), msg.size(), fmt, args2);
+    }
+    va_end(args2);
+    va_end(args);
+
+    if(gLogLevel >= level)
+    {
+        fputs(str, logfile);
+        fflush(logfile);
+    }
+#if defined(_WIN32) && !defined(NDEBUG)
+    /* OutputDebugStringW has no 'level' property to distinguish between
+     * informational, warning, or error debug messages. So only print them for
+     * non-Release builds.
+     */
+    std::wstring wstr{utf8_to_wstr(str)};
+    OutputDebugStringW(wstr.c_str());
+#elif defined(__ANDROID__)
+    auto android_severity = [](LogLevel l) noexcept
+    {
+        switch(l)
+        {
+        case LogLevel::Trace: return ANDROID_LOG_DEBUG;
+        case LogLevel::Warning: return ANDROID_LOG_WARN;
+        case LogLevel::Error: return ANDROID_LOG_ERROR;
+        /* Should not happen. */
+        case LogLevel::Disable:
+            break;
+        }
+        return ANDROID_LOG_ERROR;
+    };
+    __android_log_print(android_severity(level), "openal", "%s", str);
+#endif
+}
diff --git a/core/logging.h b/core/logging.h
new file mode 100644 (file)
index 0000000..f4b6ab5
--- /dev/null
@@ -0,0 +1,51 @@
+#ifndef CORE_LOGGING_H
+#define CORE_LOGGING_H
+
+#include <stdio.h>
+
+#include "opthelpers.h"
+
+
+enum class LogLevel {
+    Disable,
+    Error,
+    Warning,
+    Trace
+};
+extern LogLevel gLogLevel;
+
+extern FILE *gLogFile;
+
+#ifdef __USE_MINGW_ANSI_STDIO
+[[gnu::format(gnu_printf,3,4)]]
+#else
+[[gnu::format(printf,3,4)]]
+#endif
+void al_print(LogLevel level, FILE *logfile, const char *fmt, ...);
+
+#if (!defined(_WIN32) || defined(NDEBUG)) && !defined(__ANDROID__)
+#define TRACE(...) do {                                                       \
+    if(gLogLevel >= LogLevel::Trace) UNLIKELY                                 \
+        al_print(LogLevel::Trace, gLogFile, __VA_ARGS__);                     \
+} while(0)
+
+#define WARN(...) do {                                                        \
+    if(gLogLevel >= LogLevel::Warning) UNLIKELY                               \
+        al_print(LogLevel::Warning, gLogFile, __VA_ARGS__);                   \
+} while(0)
+
+#define ERR(...) do {                                                         \
+    if(gLogLevel >= LogLevel::Error) UNLIKELY                                 \
+        al_print(LogLevel::Error, gLogFile, __VA_ARGS__);                     \
+} while(0)
+
+#else
+
+#define TRACE(...) al_print(LogLevel::Trace, gLogFile, __VA_ARGS__)
+
+#define WARN(...) al_print(LogLevel::Warning, gLogFile, __VA_ARGS__)
+
+#define ERR(...) al_print(LogLevel::Error, gLogFile, __VA_ARGS__)
+#endif
+
+#endif /* CORE_LOGGING_H */
diff --git a/core/mastering.cpp b/core/mastering.cpp
new file mode 100644 (file)
index 0000000..97a4008
--- /dev/null
@@ -0,0 +1,439 @@
+
+#include "config.h"
+
+#include "mastering.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstddef>
+#include <functional>
+#include <iterator>
+#include <limits>
+#include <new>
+
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "opthelpers.h"
+
+
+/* These structures assume BufferLineSize is a power of 2. */
+static_assert((BufferLineSize & (BufferLineSize-1)) == 0, "BufferLineSize is not a power of 2");
+
+struct SlidingHold {
+    alignas(16) float mValues[BufferLineSize];
+    uint mExpiries[BufferLineSize];
+    uint mLowerIndex;
+    uint mUpperIndex;
+    uint mLength;
+};
+
+
+namespace {
+
+using namespace std::placeholders;
+
+/* This sliding hold follows the input level with an instant attack and a
+ * fixed duration hold before an instant release to the next highest level.
+ * It is a sliding window maximum (descending maxima) implementation based on
+ * Richard Harter's ascending minima algorithm available at:
+ *
+ *   http://www.richardhartersworld.com/cri/2001/slidingmin.html
+ */
+float UpdateSlidingHold(SlidingHold *Hold, const uint i, const float in)
+{
+    static constexpr uint mask{BufferLineSize - 1};
+    const uint length{Hold->mLength};
+    float (&values)[BufferLineSize] = Hold->mValues;
+    uint (&expiries)[BufferLineSize] = Hold->mExpiries;
+    uint lowerIndex{Hold->mLowerIndex};
+    uint upperIndex{Hold->mUpperIndex};
+
+    if(i >= expiries[upperIndex])
+        upperIndex = (upperIndex + 1) & mask;
+
+    if(in >= values[upperIndex])
+    {
+        values[upperIndex] = in;
+        expiries[upperIndex] = i + length;
+        lowerIndex = upperIndex;
+    }
+    else
+    {
+        do {
+            do {
+                if(!(in >= values[lowerIndex]))
+                    goto found_place;
+            } while(lowerIndex--);
+            lowerIndex = mask;
+        } while(true);
+    found_place:
+
+        lowerIndex = (lowerIndex + 1) & mask;
+        values[lowerIndex] = in;
+        expiries[lowerIndex] = i + length;
+    }
+
+    Hold->mLowerIndex = lowerIndex;
+    Hold->mUpperIndex = upperIndex;
+
+    return values[upperIndex];
+}
+
+void ShiftSlidingHold(SlidingHold *Hold, const uint n)
+{
+    auto exp_begin = std::begin(Hold->mExpiries) + Hold->mUpperIndex;
+    auto exp_last = std::begin(Hold->mExpiries) + Hold->mLowerIndex;
+    if(exp_last-exp_begin < 0)
+    {
+        std::transform(exp_begin, std::end(Hold->mExpiries), exp_begin,
+            [n](uint e){ return e - n; });
+        exp_begin = std::begin(Hold->mExpiries);
+    }
+    std::transform(exp_begin, exp_last+1, exp_begin, [n](uint e){ return e - n; });
+}
+
+
+/* Multichannel compression is linked via the absolute maximum of all
+ * channels.
+ */
+void LinkChannels(Compressor *Comp, const uint SamplesToDo, const FloatBufferLine *OutBuffer)
+{
+    const size_t numChans{Comp->mNumChans};
+
+    ASSUME(SamplesToDo > 0);
+    ASSUME(numChans > 0);
+
+    auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+    std::fill(side_begin, side_begin+SamplesToDo, 0.0f);
+
+    auto fill_max = [SamplesToDo,side_begin](const FloatBufferLine &input) -> void
+    {
+        const float *RESTRICT buffer{al::assume_aligned<16>(input.data())};
+        auto max_abs = std::bind(maxf, _1, std::bind(static_cast<float(&)(float)>(std::fabs), _2));
+        std::transform(side_begin, side_begin+SamplesToDo, buffer, side_begin, max_abs);
+    };
+    std::for_each(OutBuffer, OutBuffer+numChans, fill_max);
+}
+
+/* This calculates the squared crest factor of the control signal for the
+ * basic automation of the attack/release times.  As suggested by the paper,
+ * it uses an instantaneous squared peak detector and a squared RMS detector
+ * both with 200ms release times.
+ */
+void CrestDetector(Compressor *Comp, const uint SamplesToDo)
+{
+    const float a_crest{Comp->mCrestCoeff};
+    float y2_peak{Comp->mLastPeakSq};
+    float y2_rms{Comp->mLastRmsSq};
+
+    ASSUME(SamplesToDo > 0);
+
+    auto calc_crest = [&y2_rms,&y2_peak,a_crest](const float x_abs) noexcept -> float
+    {
+        const float x2{clampf(x_abs * x_abs, 0.000001f, 1000000.0f)};
+
+        y2_peak = maxf(x2, lerpf(x2, y2_peak, a_crest));
+        y2_rms = lerpf(x2, y2_rms, a_crest);
+        return y2_peak / y2_rms;
+    };
+    auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+    std::transform(side_begin, side_begin+SamplesToDo, std::begin(Comp->mCrestFactor), calc_crest);
+
+    Comp->mLastPeakSq = y2_peak;
+    Comp->mLastRmsSq = y2_rms;
+}
+
+/* The side-chain starts with a simple peak detector (based on the absolute
+ * value of the incoming signal) and performs most of its operations in the
+ * log domain.
+ */
+void PeakDetector(Compressor *Comp, const uint SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    /* Clamp the minimum amplitude to near-zero and convert to logarithm. */
+    auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+    std::transform(side_begin, side_begin+SamplesToDo, side_begin,
+        [](float s) { return std::log(maxf(0.000001f, s)); });
+}
+
+/* An optional hold can be used to extend the peak detector so it can more
+ * solidly detect fast transients.  This is best used when operating as a
+ * limiter.
+ */
+void PeakHoldDetector(Compressor *Comp, const uint SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    SlidingHold *hold{Comp->mHold};
+    uint i{0};
+    auto detect_peak = [&i,hold](const float x_abs) -> float
+    {
+        const float x_G{std::log(maxf(0.000001f, x_abs))};
+        return UpdateSlidingHold(hold, i++, x_G);
+    };
+    auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+    std::transform(side_begin, side_begin+SamplesToDo, side_begin, detect_peak);
+
+    ShiftSlidingHold(hold, SamplesToDo);
+}
+
+/* This is the heart of the feed-forward compressor.  It operates in the log
+ * domain (to better match human hearing) and can apply some basic automation
+ * to knee width, attack/release times, make-up/post gain, and clipping
+ * reduction.
+ */
+void GainCompressor(Compressor *Comp, const uint SamplesToDo)
+{
+    const bool autoKnee{Comp->mAuto.Knee};
+    const bool autoAttack{Comp->mAuto.Attack};
+    const bool autoRelease{Comp->mAuto.Release};
+    const bool autoPostGain{Comp->mAuto.PostGain};
+    const bool autoDeclip{Comp->mAuto.Declip};
+    const uint lookAhead{Comp->mLookAhead};
+    const float threshold{Comp->mThreshold};
+    const float slope{Comp->mSlope};
+    const float attack{Comp->mAttack};
+    const float release{Comp->mRelease};
+    const float c_est{Comp->mGainEstimate};
+    const float a_adp{Comp->mAdaptCoeff};
+    const float *crestFactor{Comp->mCrestFactor};
+    float postGain{Comp->mPostGain};
+    float knee{Comp->mKnee};
+    float t_att{attack};
+    float t_rel{release - attack};
+    float a_att{std::exp(-1.0f / t_att)};
+    float a_rel{std::exp(-1.0f / t_rel)};
+    float y_1{Comp->mLastRelease};
+    float y_L{Comp->mLastAttack};
+    float c_dev{Comp->mLastGainDev};
+
+    ASSUME(SamplesToDo > 0);
+
+    for(float &sideChain : al::span<float>{Comp->mSideChain, SamplesToDo})
+    {
+        if(autoKnee)
+            knee = maxf(0.0f, 2.5f * (c_dev + c_est));
+        const float knee_h{0.5f * knee};
+
+        /* This is the gain computer.  It applies a static compression curve
+         * to the control signal.
+         */
+        const float x_over{std::addressof(sideChain)[lookAhead] - threshold};
+        const float y_G{
+            (x_over <= -knee_h) ? 0.0f :
+            (std::fabs(x_over) < knee_h) ? (x_over + knee_h) * (x_over + knee_h) / (2.0f * knee) :
+            x_over};
+
+        const float y2_crest{*(crestFactor++)};
+        if(autoAttack)
+        {
+            t_att = 2.0f*attack/y2_crest;
+            a_att = std::exp(-1.0f / t_att);
+        }
+        if(autoRelease)
+        {
+            t_rel = 2.0f*release/y2_crest - t_att;
+            a_rel = std::exp(-1.0f / t_rel);
+        }
+
+        /* Gain smoothing (ballistics) is done via a smooth decoupled peak
+         * detector.  The attack time is subtracted from the release time
+         * above to compensate for the chained operating mode.
+         */
+        const float x_L{-slope * y_G};
+        y_1 = maxf(x_L, lerpf(x_L, y_1, a_rel));
+        y_L = lerpf(y_1, y_L, a_att);
+
+        /* Knee width and make-up gain automation make use of a smoothed
+         * measurement of deviation between the control signal and estimate.
+         * The estimate is also used to bias the measurement to hot-start its
+         * average.
+         */
+        c_dev = lerpf(-(y_L+c_est), c_dev, a_adp);
+
+        if(autoPostGain)
+        {
+            /* Clipping reduction is only viable when make-up gain is being
+             * automated. It modifies the deviation to further attenuate the
+             * control signal when clipping is detected. The adaptation time
+             * is sufficiently long enough to suppress further clipping at the
+             * same output level.
+             */
+            if(autoDeclip)
+                c_dev = maxf(c_dev, sideChain - y_L - threshold - c_est);
+
+            postGain = -(c_dev + c_est);
+        }
+
+        sideChain = std::exp(postGain - y_L);
+    }
+
+    Comp->mLastRelease = y_1;
+    Comp->mLastAttack = y_L;
+    Comp->mLastGainDev = c_dev;
+}
+
+/* Combined with the hold time, a look-ahead delay can improve handling of
+ * fast transients by allowing the envelope time to converge prior to
+ * reaching the offending impulse.  This is best used when operating as a
+ * limiter.
+ */
+void SignalDelay(Compressor *Comp, const uint SamplesToDo, FloatBufferLine *OutBuffer)
+{
+    const size_t numChans{Comp->mNumChans};
+    const uint lookAhead{Comp->mLookAhead};
+
+    ASSUME(SamplesToDo > 0);
+    ASSUME(numChans > 0);
+    ASSUME(lookAhead > 0);
+
+    for(size_t c{0};c < numChans;c++)
+    {
+        float *inout{al::assume_aligned<16>(OutBuffer[c].data())};
+        float *delaybuf{al::assume_aligned<16>(Comp->mDelay[c].data())};
+
+        auto inout_end = inout + SamplesToDo;
+        if(SamplesToDo >= lookAhead) LIKELY
+        {
+            auto delay_end = std::rotate(inout, inout_end - lookAhead, inout_end);
+            std::swap_ranges(inout, delay_end, delaybuf);
+        }
+        else
+        {
+            auto delay_start = std::swap_ranges(inout, inout_end, delaybuf);
+            std::rotate(delaybuf, delay_start, delaybuf + lookAhead);
+        }
+    }
+}
+
+} // namespace
+
+
+std::unique_ptr<Compressor> Compressor::Create(const size_t NumChans, const float SampleRate,
+    const bool AutoKnee, const bool AutoAttack, const bool AutoRelease, const bool AutoPostGain,
+    const bool AutoDeclip, const float LookAheadTime, const float HoldTime, const float PreGainDb,
+    const float PostGainDb, const float ThresholdDb, const float Ratio, const float KneeDb,
+    const float AttackTime, const float ReleaseTime)
+{
+    const auto lookAhead = static_cast<uint>(
+        clampf(std::round(LookAheadTime*SampleRate), 0.0f, BufferLineSize-1));
+    const auto hold = static_cast<uint>(
+        clampf(std::round(HoldTime*SampleRate), 0.0f, BufferLineSize-1));
+
+    size_t size{sizeof(Compressor)};
+    if(lookAhead > 0)
+    {
+        size += sizeof(*Compressor::mDelay) * NumChans;
+        /* The sliding hold implementation doesn't handle a length of 1. A 1-
+         * sample hold is useless anyway, it would only ever give back what was
+         * just given to it.
+         */
+        if(hold > 1)
+            size += sizeof(*Compressor::mHold);
+    }
+
+    auto Comp = CompressorPtr{al::construct_at(static_cast<Compressor*>(al_calloc(16, size)))};
+    Comp->mNumChans = NumChans;
+    Comp->mAuto.Knee = AutoKnee;
+    Comp->mAuto.Attack = AutoAttack;
+    Comp->mAuto.Release = AutoRelease;
+    Comp->mAuto.PostGain = AutoPostGain;
+    Comp->mAuto.Declip = AutoPostGain && AutoDeclip;
+    Comp->mLookAhead = lookAhead;
+    Comp->mPreGain = std::pow(10.0f, PreGainDb / 20.0f);
+    Comp->mPostGain = PostGainDb * std::log(10.0f) / 20.0f;
+    Comp->mThreshold = ThresholdDb * std::log(10.0f) / 20.0f;
+    Comp->mSlope = 1.0f / maxf(1.0f, Ratio) - 1.0f;
+    Comp->mKnee = maxf(0.0f, KneeDb * std::log(10.0f) / 20.0f);
+    Comp->mAttack = maxf(1.0f, AttackTime * SampleRate);
+    Comp->mRelease = maxf(1.0f, ReleaseTime * SampleRate);
+
+    /* Knee width automation actually treats the compressor as a limiter. By
+     * varying the knee width, it can effectively be seen as applying
+     * compression over a wide range of ratios.
+     */
+    if(AutoKnee)
+        Comp->mSlope = -1.0f;
+
+    if(lookAhead > 0)
+    {
+        if(hold > 1)
+        {
+            Comp->mHold = al::construct_at(reinterpret_cast<SlidingHold*>(Comp.get() + 1));
+            Comp->mHold->mValues[0] = -std::numeric_limits<float>::infinity();
+            Comp->mHold->mExpiries[0] = hold;
+            Comp->mHold->mLength = hold;
+            Comp->mDelay = reinterpret_cast<FloatBufferLine*>(Comp->mHold + 1);
+        }
+        else
+            Comp->mDelay = reinterpret_cast<FloatBufferLine*>(Comp.get() + 1);
+        std::uninitialized_fill_n(Comp->mDelay, NumChans, FloatBufferLine{});
+    }
+
+    Comp->mCrestCoeff = std::exp(-1.0f / (0.200f * SampleRate)); // 200ms
+    Comp->mGainEstimate = Comp->mThreshold * -0.5f * Comp->mSlope;
+    Comp->mAdaptCoeff = std::exp(-1.0f / (2.0f * SampleRate)); // 2s
+
+    return Comp;
+}
+
+Compressor::~Compressor()
+{
+    if(mHold)
+        al::destroy_at(mHold);
+    mHold = nullptr;
+    if(mDelay)
+        al::destroy_n(mDelay, mNumChans);
+    mDelay = nullptr;
+}
+
+
+void Compressor::process(const uint SamplesToDo, FloatBufferLine *OutBuffer)
+{
+    const size_t numChans{mNumChans};
+
+    ASSUME(SamplesToDo > 0);
+    ASSUME(numChans > 0);
+
+    const float preGain{mPreGain};
+    if(preGain != 1.0f)
+    {
+        auto apply_gain = [SamplesToDo,preGain](FloatBufferLine &input) noexcept -> void
+        {
+            float *buffer{al::assume_aligned<16>(input.data())};
+            std::transform(buffer, buffer+SamplesToDo, buffer,
+                [preGain](float s) { return s * preGain; });
+        };
+        std::for_each(OutBuffer, OutBuffer+numChans, apply_gain);
+    }
+
+    LinkChannels(this, SamplesToDo, OutBuffer);
+
+    if(mAuto.Attack || mAuto.Release)
+        CrestDetector(this, SamplesToDo);
+
+    if(mHold)
+        PeakHoldDetector(this, SamplesToDo);
+    else
+        PeakDetector(this, SamplesToDo);
+
+    GainCompressor(this, SamplesToDo);
+
+    if(mDelay)
+        SignalDelay(this, SamplesToDo, OutBuffer);
+
+    const float (&sideChain)[BufferLineSize*2] = mSideChain;
+    auto apply_comp = [SamplesToDo,&sideChain](FloatBufferLine &input) noexcept -> void
+    {
+        float *buffer{al::assume_aligned<16>(input.data())};
+        const float *gains{al::assume_aligned<16>(&sideChain[0])};
+        std::transform(gains, gains+SamplesToDo, buffer, buffer,
+            [](float g, float s) { return g * s; });
+    };
+    std::for_each(OutBuffer, OutBuffer+numChans, apply_comp);
+
+    auto side_begin = std::begin(mSideChain) + SamplesToDo;
+    std::copy(side_begin, side_begin+mLookAhead, std::begin(mSideChain));
+}
diff --git a/core/mastering.h b/core/mastering.h
new file mode 100644 (file)
index 0000000..1a36937
--- /dev/null
@@ -0,0 +1,105 @@
+#ifndef CORE_MASTERING_H
+#define CORE_MASTERING_H
+
+#include <memory>
+
+#include "almalloc.h"
+#include "bufferline.h"
+
+struct SlidingHold;
+
+using uint = unsigned int;
+
+
+/* General topology and basic automation was based on the following paper:
+ *
+ *   D. Giannoulis, M. Massberg and J. D. Reiss,
+ *   "Parameter Automation in a Dynamic Range Compressor,"
+ *   Journal of the Audio Engineering Society, v61 (10), Oct. 2013
+ *
+ * Available (along with supplemental reading) at:
+ *
+ *   http://c4dm.eecs.qmul.ac.uk/audioengineering/compressors/
+ */
+struct Compressor {
+    size_t mNumChans{0u};
+
+    struct {
+        bool Knee : 1;
+        bool Attack : 1;
+        bool Release : 1;
+        bool PostGain : 1;
+        bool Declip : 1;
+    } mAuto{};
+
+    uint mLookAhead{0};
+
+    float mPreGain{0.0f};
+    float mPostGain{0.0f};
+
+    float mThreshold{0.0f};
+    float mSlope{0.0f};
+    float mKnee{0.0f};
+
+    float mAttack{0.0f};
+    float mRelease{0.0f};
+
+    alignas(16) float mSideChain[2*BufferLineSize]{};
+    alignas(16) float mCrestFactor[BufferLineSize]{};
+
+    SlidingHold *mHold{nullptr};
+    FloatBufferLine *mDelay{nullptr};
+
+    float mCrestCoeff{0.0f};
+    float mGainEstimate{0.0f};
+    float mAdaptCoeff{0.0f};
+
+    float mLastPeakSq{0.0f};
+    float mLastRmsSq{0.0f};
+    float mLastRelease{0.0f};
+    float mLastAttack{0.0f};
+    float mLastGainDev{0.0f};
+
+
+    ~Compressor();
+    void process(const uint SamplesToDo, FloatBufferLine *OutBuffer);
+    int getLookAhead() const noexcept { return static_cast<int>(mLookAhead); }
+
+    DEF_PLACE_NEWDEL()
+
+    /**
+     * The compressor is initialized with the following settings:
+     *
+     * \param NumChans      Number of channels to process.
+     * \param SampleRate    Sample rate to process.
+     * \param AutoKnee      Whether to automate the knee width parameter.
+     * \param AutoAttack    Whether to automate the attack time parameter.
+     * \param AutoRelease   Whether to automate the release time parameter.
+     * \param AutoPostGain  Whether to automate the make-up (post) gain
+     *        parameter.
+     * \param AutoDeclip    Whether to automate clipping reduction. Ignored
+     *        when not automating make-up gain.
+     * \param LookAheadTime Look-ahead time (in seconds).
+     * \param HoldTime      Peak hold-time (in seconds).
+     * \param PreGainDb     Gain applied before detection (in dB).
+     * \param PostGainDb    Make-up gain applied after compression (in dB).
+     * \param ThresholdDb   Triggering threshold (in dB).
+     * \param Ratio         Compression ratio (x:1). Set to INFINIFTY for true
+     *        limiting. Ignored when automating knee width.
+     * \param KneeDb        Knee width (in dB). Ignored when automating knee
+     *        width.
+     * \param AttackTime    Attack time (in seconds). Acts as a maximum when
+     *        automating attack time.
+     * \param ReleaseTime   Release time (in seconds). Acts as a maximum when
+     *        automating release time.
+     */
+    static std::unique_ptr<Compressor> Create(const size_t NumChans, const float SampleRate,
+        const bool AutoKnee, const bool AutoAttack, const bool AutoRelease,
+        const bool AutoPostGain, const bool AutoDeclip, const float LookAheadTime,
+        const float HoldTime, const float PreGainDb, const float PostGainDb,
+        const float ThresholdDb, const float Ratio, const float KneeDb, const float AttackTime,
+        const float ReleaseTime);
+};
+using CompressorPtr = std::unique_ptr<Compressor>;
+
+#endif /* CORE_MASTERING_H */
diff --git a/core/mixer.cpp b/core/mixer.cpp
new file mode 100644 (file)
index 0000000..066c57b
--- /dev/null
@@ -0,0 +1,95 @@
+
+#include "config.h"
+
+#include "mixer.h"
+
+#include <cmath>
+
+#include "alnumbers.h"
+#include "devformat.h"
+#include "device.h"
+#include "mixer/defs.h"
+
+struct CTag;
+
+
+MixerOutFunc MixSamplesOut{Mix_<CTag>};
+MixerOneFunc MixSamplesOne{Mix_<CTag>};
+
+
+std::array<float,MaxAmbiChannels> CalcAmbiCoeffs(const float y, const float z, const float x,
+    const float spread)
+{
+    std::array<float,MaxAmbiChannels> coeffs{CalcAmbiCoeffs(y, z, x)};
+
+    if(spread > 0.0f)
+    {
+        /* Implement the spread by using a spherical source that subtends the
+         * angle spread. See:
+         * http://www.ppsloan.org/publications/StupidSH36.pdf - Appendix A3
+         *
+         * When adjusted for N3D normalization instead of SN3D, these
+         * calculations are:
+         *
+         * ZH0 = -sqrt(pi) * (-1+ca);
+         * ZH1 =  0.5*sqrt(pi) * sa*sa;
+         * ZH2 = -0.5*sqrt(pi) * ca*(-1+ca)*(ca+1);
+         * ZH3 = -0.125*sqrt(pi) * (-1+ca)*(ca+1)*(5*ca*ca - 1);
+         * ZH4 = -0.125*sqrt(pi) * ca*(-1+ca)*(ca+1)*(7*ca*ca - 3);
+         * ZH5 = -0.0625*sqrt(pi) * (-1+ca)*(ca+1)*(21*ca*ca*ca*ca - 14*ca*ca + 1);
+         *
+         * The gain of the source is compensated for size, so that the
+         * loudness doesn't depend on the spread. Thus:
+         *
+         * ZH0 = 1.0f;
+         * ZH1 = 0.5f * (ca+1.0f);
+         * ZH2 = 0.5f * (ca+1.0f)*ca;
+         * ZH3 = 0.125f * (ca+1.0f)*(5.0f*ca*ca - 1.0f);
+         * ZH4 = 0.125f * (ca+1.0f)*(7.0f*ca*ca - 3.0f)*ca;
+         * ZH5 = 0.0625f * (ca+1.0f)*(21.0f*ca*ca*ca*ca - 14.0f*ca*ca + 1.0f);
+         */
+        const float ca{std::cos(spread * 0.5f)};
+        /* Increase the source volume by up to +3dB for a full spread. */
+        const float scale{std::sqrt(1.0f + al::numbers::inv_pi_v<float>/2.0f*spread)};
+
+        const float ZH0_norm{scale};
+        const float ZH1_norm{scale * 0.5f * (ca+1.f)};
+        const float ZH2_norm{scale * 0.5f * (ca+1.f)*ca};
+        const float ZH3_norm{scale * 0.125f * (ca+1.f)*(5.f*ca*ca-1.f)};
+
+        /* Zeroth-order */
+        coeffs[0]  *= ZH0_norm;
+        /* First-order */
+        coeffs[1]  *= ZH1_norm;
+        coeffs[2]  *= ZH1_norm;
+        coeffs[3]  *= ZH1_norm;
+        /* Second-order */
+        coeffs[4]  *= ZH2_norm;
+        coeffs[5]  *= ZH2_norm;
+        coeffs[6]  *= ZH2_norm;
+        coeffs[7]  *= ZH2_norm;
+        coeffs[8]  *= ZH2_norm;
+        /* Third-order */
+        coeffs[9]  *= ZH3_norm;
+        coeffs[10] *= ZH3_norm;
+        coeffs[11] *= ZH3_norm;
+        coeffs[12] *= ZH3_norm;
+        coeffs[13] *= ZH3_norm;
+        coeffs[14] *= ZH3_norm;
+        coeffs[15] *= ZH3_norm;
+    }
+
+    return coeffs;
+}
+
+void ComputePanGains(const MixParams *mix, const float*RESTRICT coeffs, const float ingain,
+    const al::span<float,MaxAmbiChannels> gains)
+{
+    auto ambimap = mix->AmbiMap.cbegin();
+
+    auto iter = std::transform(ambimap, ambimap+mix->Buffer.size(), gains.begin(),
+        [coeffs,ingain](const BFChannelConfig &chanmap) noexcept -> float
+        { return chanmap.Scale * coeffs[chanmap.Index] * ingain; }
+    );
+    std::fill(iter, gains.end(), 0.0f);
+}
diff --git a/core/mixer.h b/core/mixer.h
new file mode 100644 (file)
index 0000000..aa7597b
--- /dev/null
@@ -0,0 +1,109 @@
+#ifndef CORE_MIXER_H
+#define CORE_MIXER_H
+
+#include <array>
+#include <cmath>
+#include <stddef.h>
+#include <type_traits>
+
+#include "alspan.h"
+#include "ambidefs.h"
+#include "bufferline.h"
+#include "devformat.h"
+
+struct MixParams;
+
+/* Mixer functions that handle one input and multiple output channels. */
+using MixerOutFunc = void(*)(const al::span<const float> InSamples,
+    const al::span<FloatBufferLine> OutBuffer, float *CurrentGains, const float *TargetGains,
+    const size_t Counter, const size_t OutPos);
+
+extern MixerOutFunc MixSamplesOut;
+inline void MixSamples(const al::span<const float> InSamples,
+    const al::span<FloatBufferLine> OutBuffer, float *CurrentGains, const float *TargetGains,
+    const size_t Counter, const size_t OutPos)
+{ MixSamplesOut(InSamples, OutBuffer, CurrentGains, TargetGains, Counter, OutPos); }
+
+/* Mixer functions that handle one input and one output channel. */
+using MixerOneFunc = void(*)(const al::span<const float> InSamples, float *OutBuffer,
+    float &CurrentGain, const float TargetGain, const size_t Counter);
+
+extern MixerOneFunc MixSamplesOne;
+inline void MixSamples(const al::span<const float> InSamples, float *OutBuffer, float &CurrentGain,
+    const float TargetGain, const size_t Counter)
+{ MixSamplesOne(InSamples, OutBuffer, CurrentGain, TargetGain, Counter); }
+
+
+/**
+ * Calculates ambisonic encoder coefficients using the X, Y, and Z direction
+ * components, which must represent a normalized (unit length) vector, and the
+ * spread is the angular width of the sound (0...tau).
+ *
+ * NOTE: The components use ambisonic coordinates. As a result:
+ *
+ * Ambisonic Y = OpenAL -X
+ * Ambisonic Z = OpenAL Y
+ * Ambisonic X = OpenAL -Z
+ *
+ * The components are ordered such that OpenAL's X, Y, and Z are the first,
+ * second, and third parameters respectively -- simply negate X and Z.
+ */
+std::array<float,MaxAmbiChannels> CalcAmbiCoeffs(const float y, const float z, const float x,
+    const float spread);
+
+/**
+ * CalcDirectionCoeffs
+ *
+ * Calculates ambisonic coefficients based on an OpenAL direction vector. The
+ * vector must be normalized (unit length), and the spread is the angular width
+ * of the sound (0...tau).
+ */
+inline std::array<float,MaxAmbiChannels> CalcDirectionCoeffs(const float (&dir)[3],
+    const float spread)
+{
+    /* Convert from OpenAL coords to Ambisonics. */
+    return CalcAmbiCoeffs(-dir[0], dir[1], -dir[2], spread);
+}
+
+/**
+ * CalcDirectionCoeffs
+ *
+ * Calculates ambisonic coefficients based on an OpenAL direction vector. The
+ * vector must be normalized (unit length).
+ */
+constexpr std::array<float,MaxAmbiChannels> CalcDirectionCoeffs(const float (&dir)[3])
+{
+    /* Convert from OpenAL coords to Ambisonics. */
+    return CalcAmbiCoeffs(-dir[0], dir[1], -dir[2]);
+}
+
+/**
+ * CalcAngleCoeffs
+ *
+ * Calculates ambisonic coefficients based on azimuth and elevation. The
+ * azimuth and elevation parameters are in radians, going right and up
+ * respectively.
+ */
+inline std::array<float,MaxAmbiChannels> CalcAngleCoeffs(const float azimuth,
+    const float elevation, const float spread)
+{
+    const float x{-std::sin(azimuth) * std::cos(elevation)};
+    const float y{ std::sin(elevation)};
+    const float z{ std::cos(azimuth) * std::cos(elevation)};
+
+    return CalcAmbiCoeffs(x, y, z, spread);
+}
+
+
+/**
+ * ComputePanGains
+ *
+ * Computes panning gains using the given channel decoder coefficients and the
+ * pre-calculated direction or angle coefficients. For B-Format sources, the
+ * coeffs are a 'slice' of a transform matrix for the input channel, used to
+ * scale and orient the sound samples.
+ */
+void ComputePanGains(const MixParams *mix, const float*RESTRICT coeffs, const float ingain,
+    const al::span<float,MaxAmbiChannels> gains);
+
+#endif /* CORE_MIXER_H */
diff --git a/core/mixer/defs.h b/core/mixer/defs.h
new file mode 100644 (file)
index 0000000..48daca9
--- /dev/null
@@ -0,0 +1,109 @@
+#ifndef CORE_MIXER_DEFS_H
+#define CORE_MIXER_DEFS_H
+
+#include <array>
+#include <stdlib.h>
+
+#include "alspan.h"
+#include "core/bufferline.h"
+#include "core/resampler_limits.h"
+
+struct CubicCoefficients;
+struct HrtfChannelState;
+struct HrtfFilter;
+struct MixHrtfFilter;
+
+using uint = unsigned int;
+using float2 = std::array<float,2>;
+
+
+constexpr int MixerFracBits{16};
+constexpr int MixerFracOne{1 << MixerFracBits};
+constexpr int MixerFracMask{MixerFracOne - 1};
+constexpr int MixerFracHalf{MixerFracOne >> 1};
+
+constexpr float GainSilenceThreshold{0.00001f}; /* -100dB */
+
+
+enum class Resampler : uint8_t {
+    Point,
+    Linear,
+    Cubic,
+    FastBSinc12,
+    BSinc12,
+    FastBSinc24,
+    BSinc24,
+
+    Max = BSinc24
+};
+
+/* Interpolator state. Kind of a misnomer since the interpolator itself is
+ * stateless. This just keeps it from having to recompute scale-related
+ * mappings for every sample.
+ */
+struct BsincState {
+    float sf; /* Scale interpolation factor. */
+    uint m; /* Coefficient count. */
+    uint l; /* Left coefficient offset. */
+    /* Filter coefficients, followed by the phase, scale, and scale-phase
+     * delta coefficients. Starting at phase index 0, each subsequent phase
+     * index follows contiguously.
+     */
+    const float *filter;
+};
+
+struct CubicState {
+    /* Filter coefficients, and coefficient deltas. Starting at phase index 0,
+     * each subsequent phase index follows contiguously.
+     */
+    const CubicCoefficients *filter;
+};
+
+union InterpState {
+    CubicState cubic;
+    BsincState bsinc;
+};
+
+using ResamplerFunc = void(*)(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst);
+
+ResamplerFunc PrepareResampler(Resampler resampler, uint increment, InterpState *state);
+
+
+template<typename TypeTag, typename InstTag>
+void Resample_(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst);
+
+template<typename InstTag>
+void Mix_(const al::span<const float> InSamples, const al::span<FloatBufferLine> OutBuffer,
+    float *CurrentGains, const float *TargetGains, const size_t Counter, const size_t OutPos);
+template<typename InstTag>
+void Mix_(const al::span<const float> InSamples, float *OutBuffer, float &CurrentGain,
+    const float TargetGain, const size_t Counter);
+
+template<typename InstTag>
+void MixHrtf_(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const MixHrtfFilter *hrtfparams, const size_t BufferSize);
+template<typename InstTag>
+void MixHrtfBlend_(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const HrtfFilter *oldparams, const MixHrtfFilter *newparams, const size_t BufferSize);
+template<typename InstTag>
+void MixDirectHrtf_(const FloatBufferSpan LeftOut, const FloatBufferSpan RightOut,
+    const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples,
+    float *TempBuf, HrtfChannelState *ChanState, const size_t IrSize, const size_t BufferSize);
+
+/* Vectorized resampler helpers */
+template<size_t N>
+inline void InitPosArrays(uint frac, uint increment, uint (&frac_arr)[N], uint (&pos_arr)[N])
+{
+    pos_arr[0] = 0;
+    frac_arr[0] = frac;
+    for(size_t i{1};i < N;i++)
+    {
+        const uint frac_tmp{frac_arr[i-1] + increment};
+        pos_arr[i] = pos_arr[i-1] + (frac_tmp>>MixerFracBits);
+        frac_arr[i] = frac_tmp&MixerFracMask;
+    }
+}
+
+#endif /* CORE_MIXER_DEFS_H */
diff --git a/core/mixer/hrtfbase.h b/core/mixer/hrtfbase.h
new file mode 100644 (file)
index 0000000..36f88e4
--- /dev/null
@@ -0,0 +1,129 @@
+#ifndef CORE_MIXER_HRTFBASE_H
+#define CORE_MIXER_HRTFBASE_H
+
+#include <algorithm>
+#include <cmath>
+
+#include "almalloc.h"
+#include "hrtfdefs.h"
+#include "opthelpers.h"
+
+
+using uint = unsigned int;
+
+using ApplyCoeffsT = void(&)(float2 *RESTRICT Values, const size_t irSize,
+    const ConstHrirSpan Coeffs, const float left, const float right);
+
+template<ApplyCoeffsT ApplyCoeffs>
+inline void MixHrtfBase(const float *InSamples, float2 *RESTRICT AccumSamples, const size_t IrSize,
+    const MixHrtfFilter *hrtfparams, const size_t BufferSize)
+{
+    ASSUME(BufferSize > 0);
+
+    const ConstHrirSpan Coeffs{hrtfparams->Coeffs};
+    const float gainstep{hrtfparams->GainStep};
+    const float gain{hrtfparams->Gain};
+
+    size_t ldelay{HrtfHistoryLength - hrtfparams->Delay[0]};
+    size_t rdelay{HrtfHistoryLength - hrtfparams->Delay[1]};
+    float stepcount{0.0f};
+    for(size_t i{0u};i < BufferSize;++i)
+    {
+        const float g{gain + gainstep*stepcount};
+        const float left{InSamples[ldelay++] * g};
+        const float right{InSamples[rdelay++] * g};
+        ApplyCoeffs(AccumSamples+i, IrSize, Coeffs, left, right);
+
+        stepcount += 1.0f;
+    }
+}
+
+template<ApplyCoeffsT ApplyCoeffs>
+inline void MixHrtfBlendBase(const float *InSamples, float2 *RESTRICT AccumSamples,
+    const size_t IrSize, const HrtfFilter *oldparams, const MixHrtfFilter *newparams,
+    const size_t BufferSize)
+{
+    ASSUME(BufferSize > 0);
+
+    const ConstHrirSpan OldCoeffs{oldparams->Coeffs};
+    const float oldGainStep{oldparams->Gain / static_cast<float>(BufferSize)};
+    const ConstHrirSpan NewCoeffs{newparams->Coeffs};
+    const float newGainStep{newparams->GainStep};
+
+    if(oldparams->Gain > GainSilenceThreshold) LIKELY
+    {
+        size_t ldelay{HrtfHistoryLength - oldparams->Delay[0]};
+        size_t rdelay{HrtfHistoryLength - oldparams->Delay[1]};
+        auto stepcount = static_cast<float>(BufferSize);
+        for(size_t i{0u};i < BufferSize;++i)
+        {
+            const float g{oldGainStep*stepcount};
+            const float left{InSamples[ldelay++] * g};
+            const float right{InSamples[rdelay++] * g};
+            ApplyCoeffs(AccumSamples+i, IrSize, OldCoeffs, left, right);
+
+            stepcount -= 1.0f;
+        }
+    }
+
+    if(newGainStep*static_cast<float>(BufferSize) > GainSilenceThreshold) LIKELY
+    {
+        size_t ldelay{HrtfHistoryLength+1 - newparams->Delay[0]};
+        size_t rdelay{HrtfHistoryLength+1 - newparams->Delay[1]};
+        float stepcount{1.0f};
+        for(size_t i{1u};i < BufferSize;++i)
+        {
+            const float g{newGainStep*stepcount};
+            const float left{InSamples[ldelay++] * g};
+            const float right{InSamples[rdelay++] * g};
+            ApplyCoeffs(AccumSamples+i, IrSize, NewCoeffs, left, right);
+
+            stepcount += 1.0f;
+        }
+    }
+}
+
+template<ApplyCoeffsT ApplyCoeffs>
+inline void MixDirectHrtfBase(const FloatBufferSpan LeftOut, const FloatBufferSpan RightOut,
+    const al::span<const FloatBufferLine> InSamples, float2 *RESTRICT AccumSamples,
+    float *TempBuf, HrtfChannelState *ChanState, const size_t IrSize, const size_t BufferSize)
+{
+    ASSUME(BufferSize > 0);
+
+    for(const FloatBufferLine &input : InSamples)
+    {
+        /* For dual-band processing, the signal needs extra scaling applied to
+         * the high frequency response. The band-splitter applies this scaling
+         * with a consistent phase shift regardless of the scale amount.
+         */
+        ChanState->mSplitter.processHfScale({input.data(), BufferSize}, TempBuf,
+            ChanState->mHfScale);
+
+        /* Now apply the HRIR coefficients to this channel. */
+        const float *RESTRICT tempbuf{al::assume_aligned<16>(TempBuf)};
+        const ConstHrirSpan Coeffs{ChanState->mCoeffs};
+        for(size_t i{0u};i < BufferSize;++i)
+        {
+            const float insample{tempbuf[i]};
+            ApplyCoeffs(AccumSamples+i, IrSize, Coeffs, insample, insample);
+        }
+
+        ++ChanState;
+    }
+
+    /* Add the HRTF signal to the existing "direct" signal. */
+    float *RESTRICT left{al::assume_aligned<16>(LeftOut.data())};
+    float *RESTRICT right{al::assume_aligned<16>(RightOut.data())};
+    for(size_t i{0u};i < BufferSize;++i)
+        left[i]  += AccumSamples[i][0];
+    for(size_t i{0u};i < BufferSize;++i)
+        right[i] += AccumSamples[i][1];
+
+    /* Copy the new in-progress accumulation values to the front and clear the
+     * following samples for the next mix.
+     */
+    auto accum_iter = std::copy_n(AccumSamples+BufferSize, HrirLength, AccumSamples);
+    std::fill_n(accum_iter, BufferSize, float2{});
+}
+
+#endif /* CORE_MIXER_HRTFBASE_H */
diff --git a/core/mixer/hrtfdefs.h b/core/mixer/hrtfdefs.h
new file mode 100644 (file)
index 0000000..3c903ed
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef CORE_MIXER_HRTFDEFS_H
+#define CORE_MIXER_HRTFDEFS_H
+
+#include <array>
+
+#include "alspan.h"
+#include "core/ambidefs.h"
+#include "core/bufferline.h"
+#include "core/filters/splitter.h"
+
+
+using float2 = std::array<float,2>;
+using ubyte = unsigned char;
+using ubyte2 = std::array<ubyte,2>;
+using ushort = unsigned short;
+using uint = unsigned int;
+using uint2 = std::array<uint,2>;
+
+constexpr uint HrtfHistoryBits{6};
+constexpr uint HrtfHistoryLength{1 << HrtfHistoryBits};
+constexpr uint HrtfHistoryMask{HrtfHistoryLength - 1};
+
+constexpr uint HrirBits{7};
+constexpr uint HrirLength{1 << HrirBits};
+constexpr uint HrirMask{HrirLength - 1};
+
+constexpr uint MinIrLength{8};
+
+using HrirArray = std::array<float2,HrirLength>;
+using HrirSpan = al::span<float2,HrirLength>;
+using ConstHrirSpan = al::span<const float2,HrirLength>;
+
+struct MixHrtfFilter {
+    const ConstHrirSpan Coeffs;
+    uint2 Delay;
+    float Gain;
+    float GainStep;
+};
+
+struct HrtfFilter {
+    alignas(16) HrirArray Coeffs;
+    uint2 Delay;
+    float Gain;
+};
+
+
+struct HrtfChannelState {
+    BandSplitter mSplitter;
+    float mHfScale{};
+    alignas(16) HrirArray mCoeffs{};
+};
+
+#endif /* CORE_MIXER_HRTFDEFS_H */
diff --git a/core/mixer/mixer_c.cpp b/core/mixer/mixer_c.cpp
new file mode 100644 (file)
index 0000000..28a92ef
--- /dev/null
@@ -0,0 +1,218 @@
+#include "config.h"
+
+#include <cassert>
+#include <cmath>
+#include <limits>
+
+#include "alnumeric.h"
+#include "core/bsinc_defs.h"
+#include "core/cubic_defs.h"
+#include "defs.h"
+#include "hrtfbase.h"
+
+struct CTag;
+struct PointTag;
+struct LerpTag;
+struct CubicTag;
+struct BSincTag;
+struct FastBSincTag;
+
+
+namespace {
+
+constexpr uint BsincPhaseDiffBits{MixerFracBits - BSincPhaseBits};
+constexpr uint BsincPhaseDiffOne{1 << BsincPhaseDiffBits};
+constexpr uint BsincPhaseDiffMask{BsincPhaseDiffOne - 1u};
+
+constexpr uint CubicPhaseDiffBits{MixerFracBits - CubicPhaseBits};
+constexpr uint CubicPhaseDiffOne{1 << CubicPhaseDiffBits};
+constexpr uint CubicPhaseDiffMask{CubicPhaseDiffOne - 1u};
+
+inline float do_point(const InterpState&, const float *RESTRICT vals, const uint)
+{ return vals[0]; }
+inline float do_lerp(const InterpState&, const float *RESTRICT vals, const uint frac)
+{ return lerpf(vals[0], vals[1], static_cast<float>(frac)*(1.0f/MixerFracOne)); }
+inline float do_cubic(const InterpState &istate, const float *RESTRICT vals, const uint frac)
+{
+    /* Calculate the phase index and factor. */
+    const uint pi{frac >> CubicPhaseDiffBits};
+    const float pf{static_cast<float>(frac&CubicPhaseDiffMask) * (1.0f/CubicPhaseDiffOne)};
+
+    const float *RESTRICT fil{al::assume_aligned<16>(istate.cubic.filter[pi].mCoeffs)};
+    const float *RESTRICT phd{al::assume_aligned<16>(istate.cubic.filter[pi].mDeltas)};
+
+    /* Apply the phase interpolated filter. */
+    return (fil[0] + pf*phd[0])*vals[0] + (fil[1] + pf*phd[1])*vals[1]
+        + (fil[2] + pf*phd[2])*vals[2] + (fil[3] + pf*phd[3])*vals[3];
+}
+inline float do_bsinc(const InterpState &istate, const float *RESTRICT vals, const uint frac)
+{
+    const size_t m{istate.bsinc.m};
+    ASSUME(m > 0);
+
+    /* Calculate the phase index and factor. */
+    const uint pi{frac >> BsincPhaseDiffBits};
+    const float pf{static_cast<float>(frac&BsincPhaseDiffMask) * (1.0f/BsincPhaseDiffOne)};
+
+    const float *RESTRICT fil{istate.bsinc.filter + m*pi*2};
+    const float *RESTRICT phd{fil + m};
+    const float *RESTRICT scd{fil + BSincPhaseCount*2*m};
+    const float *RESTRICT spd{scd + m};
+
+    /* Apply the scale and phase interpolated filter. */
+    float r{0.0f};
+    for(size_t j_f{0};j_f < m;j_f++)
+        r += (fil[j_f] + istate.bsinc.sf*scd[j_f] + pf*(phd[j_f] + istate.bsinc.sf*spd[j_f])) * vals[j_f];
+    return r;
+}
+inline float do_fastbsinc(const InterpState &istate, const float *RESTRICT vals, const uint frac)
+{
+    const size_t m{istate.bsinc.m};
+    ASSUME(m > 0);
+
+    /* Calculate the phase index and factor. */
+    const uint pi{frac >> BsincPhaseDiffBits};
+    const float pf{static_cast<float>(frac&BsincPhaseDiffMask) * (1.0f/BsincPhaseDiffOne)};
+
+    const float *RESTRICT fil{istate.bsinc.filter + m*pi*2};
+    const float *RESTRICT phd{fil + m};
+
+    /* Apply the phase interpolated filter. */
+    float r{0.0f};
+    for(size_t j_f{0};j_f < m;j_f++)
+        r += (fil[j_f] + pf*phd[j_f]) * vals[j_f];
+    return r;
+}
+
+using SamplerT = float(&)(const InterpState&, const float*RESTRICT, const uint);
+template<SamplerT Sampler>
+void DoResample(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    const InterpState istate{*state};
+    ASSUME(frac < MixerFracOne);
+    for(float &out : dst)
+    {
+        out = Sampler(istate, src, frac);
+
+        frac += increment;
+        src  += frac>>MixerFracBits;
+        frac &= MixerFracMask;
+    }
+}
+
+inline void ApplyCoeffs(float2 *RESTRICT Values, const size_t IrSize, const ConstHrirSpan Coeffs,
+    const float left, const float right)
+{
+    ASSUME(IrSize >= MinIrLength);
+    for(size_t c{0};c < IrSize;++c)
+    {
+        Values[c][0] += Coeffs[c][0] * left;
+        Values[c][1] += Coeffs[c][1] * right;
+    }
+}
+
+force_inline void MixLine(const al::span<const float> InSamples, float *RESTRICT dst,
+    float &CurrentGain, const float TargetGain, const float delta, const size_t min_len,
+    size_t Counter)
+{
+    float gain{CurrentGain};
+    const float step{(TargetGain-gain) * delta};
+
+    size_t pos{0};
+    if(!(std::abs(step) > std::numeric_limits<float>::epsilon()))
+        gain = TargetGain;
+    else
+    {
+        float step_count{0.0f};
+        for(;pos != min_len;++pos)
+        {
+            dst[pos] += InSamples[pos] * (gain + step*step_count);
+            step_count += 1.0f;
+        }
+        if(pos == Counter)
+            gain = TargetGain;
+        else
+            gain += step*step_count;
+    }
+    CurrentGain = gain;
+
+    if(!(std::abs(gain) > GainSilenceThreshold))
+        return;
+    for(;pos != InSamples.size();++pos)
+        dst[pos] += InSamples[pos] * gain;
+}
+
+} // namespace
+
+template<>
+void Resample_<PointTag,CTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{ DoResample<do_point>(state, src, frac, increment, dst); }
+
+template<>
+void Resample_<LerpTag,CTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{ DoResample<do_lerp>(state, src, frac, increment, dst); }
+
+template<>
+void Resample_<CubicTag,CTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{ DoResample<do_cubic>(state, src-1, frac, increment, dst); }
+
+template<>
+void Resample_<BSincTag,CTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{ DoResample<do_bsinc>(state, src-state->bsinc.l, frac, increment, dst); }
+
+template<>
+void Resample_<FastBSincTag,CTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{ DoResample<do_fastbsinc>(state, src-state->bsinc.l, frac, increment, dst); }
+
+
+template<>
+void MixHrtf_<CTag>(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const MixHrtfFilter *hrtfparams, const size_t BufferSize)
+{ MixHrtfBase<ApplyCoeffs>(InSamples, AccumSamples, IrSize, hrtfparams, BufferSize); }
+
+template<>
+void MixHrtfBlend_<CTag>(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const HrtfFilter *oldparams, const MixHrtfFilter *newparams, const size_t BufferSize)
+{
+    MixHrtfBlendBase<ApplyCoeffs>(InSamples, AccumSamples, IrSize, oldparams, newparams,
+        BufferSize);
+}
+
+template<>
+void MixDirectHrtf_<CTag>(const FloatBufferSpan LeftOut, const FloatBufferSpan RightOut,
+    const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples,
+    float *TempBuf, HrtfChannelState *ChanState, const size_t IrSize, const size_t BufferSize)
+{
+    MixDirectHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, TempBuf, ChanState,
+        IrSize, BufferSize);
+}
+
+
+template<>
+void Mix_<CTag>(const al::span<const float> InSamples, const al::span<FloatBufferLine> OutBuffer,
+    float *CurrentGains, const float *TargetGains, const size_t Counter, const size_t OutPos)
+{
+    const float delta{(Counter > 0) ? 1.0f / static_cast<float>(Counter) : 0.0f};
+    const auto min_len = minz(Counter, InSamples.size());
+
+    for(FloatBufferLine &output : OutBuffer)
+        MixLine(InSamples, al::assume_aligned<16>(output.data()+OutPos), *CurrentGains++,
+            *TargetGains++, delta, min_len, Counter);
+}
+
+template<>
+void Mix_<CTag>(const al::span<const float> InSamples, float *OutBuffer, float &CurrentGain,
+    const float TargetGain, const size_t Counter)
+{
+    const float delta{(Counter > 0) ? 1.0f / static_cast<float>(Counter) : 0.0f};
+    const auto min_len = minz(Counter, InSamples.size());
+
+    MixLine(InSamples, al::assume_aligned<16>(OutBuffer), CurrentGain,
+        TargetGain, delta, min_len, Counter);
+}
diff --git a/core/mixer/mixer_neon.cpp b/core/mixer/mixer_neon.cpp
new file mode 100644 (file)
index 0000000..ef2936b
--- /dev/null
@@ -0,0 +1,362 @@
+#include "config.h"
+
+#include <arm_neon.h>
+
+#include <cmath>
+#include <limits>
+
+#include "alnumeric.h"
+#include "core/bsinc_defs.h"
+#include "core/cubic_defs.h"
+#include "defs.h"
+#include "hrtfbase.h"
+
+struct NEONTag;
+struct LerpTag;
+struct CubicTag;
+struct BSincTag;
+struct FastBSincTag;
+
+
+#if defined(__GNUC__) && !defined(__clang__) && !defined(__ARM_NEON)
+#pragma GCC target("fpu=neon")
+#endif
+
+namespace {
+
+constexpr uint BSincPhaseDiffBits{MixerFracBits - BSincPhaseBits};
+constexpr uint BSincPhaseDiffOne{1 << BSincPhaseDiffBits};
+constexpr uint BSincPhaseDiffMask{BSincPhaseDiffOne - 1u};
+
+constexpr uint CubicPhaseDiffBits{MixerFracBits - CubicPhaseBits};
+constexpr uint CubicPhaseDiffOne{1 << CubicPhaseDiffBits};
+constexpr uint CubicPhaseDiffMask{CubicPhaseDiffOne - 1u};
+
+inline float32x4_t set_f4(float l0, float l1, float l2, float l3)
+{
+    float32x4_t ret{vmovq_n_f32(l0)};
+    ret = vsetq_lane_f32(l1, ret, 1);
+    ret = vsetq_lane_f32(l2, ret, 2);
+    ret = vsetq_lane_f32(l3, ret, 3);
+    return ret;
+}
+
+inline void ApplyCoeffs(float2 *RESTRICT Values, const size_t IrSize, const ConstHrirSpan Coeffs,
+    const float left, const float right)
+{
+    float32x4_t leftright4;
+    {
+        float32x2_t leftright2{vmov_n_f32(left)};
+        leftright2 = vset_lane_f32(right, leftright2, 1);
+        leftright4 = vcombine_f32(leftright2, leftright2);
+    }
+
+    ASSUME(IrSize >= MinIrLength);
+    for(size_t c{0};c < IrSize;c += 2)
+    {
+        float32x4_t vals = vld1q_f32(&Values[c][0]);
+        float32x4_t coefs = vld1q_f32(&Coeffs[c][0]);
+
+        vals = vmlaq_f32(vals, coefs, leftright4);
+
+        vst1q_f32(&Values[c][0], vals);
+    }
+}
+
+force_inline void MixLine(const al::span<const float> InSamples, float *RESTRICT dst,
+    float &CurrentGain, const float TargetGain, const float delta, const size_t min_len,
+    const size_t aligned_len, size_t Counter)
+{
+    float gain{CurrentGain};
+    const float step{(TargetGain-gain) * delta};
+
+    size_t pos{0};
+    if(!(std::abs(step) > std::numeric_limits<float>::epsilon()))
+        gain = TargetGain;
+    else
+    {
+        float step_count{0.0f};
+        /* Mix with applying gain steps in aligned multiples of 4. */
+        if(size_t todo{min_len >> 2})
+        {
+            const float32x4_t four4{vdupq_n_f32(4.0f)};
+            const float32x4_t step4{vdupq_n_f32(step)};
+            const float32x4_t gain4{vdupq_n_f32(gain)};
+            float32x4_t step_count4{vdupq_n_f32(0.0f)};
+            step_count4 = vsetq_lane_f32(1.0f, step_count4, 1);
+            step_count4 = vsetq_lane_f32(2.0f, step_count4, 2);
+            step_count4 = vsetq_lane_f32(3.0f, step_count4, 3);
+
+            do {
+                const float32x4_t val4 = vld1q_f32(&InSamples[pos]);
+                float32x4_t dry4 = vld1q_f32(&dst[pos]);
+                dry4 = vmlaq_f32(dry4, val4, vmlaq_f32(gain4, step4, step_count4));
+                step_count4 = vaddq_f32(step_count4, four4);
+                vst1q_f32(&dst[pos], dry4);
+                pos += 4;
+            } while(--todo);
+            /* NOTE: step_count4 now represents the next four counts after the
+             * last four mixed samples, so the lowest element represents the
+             * next step count to apply.
+             */
+            step_count = vgetq_lane_f32(step_count4, 0);
+        }
+        /* Mix with applying left over gain steps that aren't aligned multiples of 4. */
+        for(size_t leftover{min_len&3};leftover;++pos,--leftover)
+        {
+            dst[pos] += InSamples[pos] * (gain + step*step_count);
+            step_count += 1.0f;
+        }
+        if(pos == Counter)
+            gain = TargetGain;
+        else
+            gain += step*step_count;
+
+        /* Mix until pos is aligned with 4 or the mix is done. */
+        for(size_t leftover{aligned_len&3};leftover;++pos,--leftover)
+            dst[pos] += InSamples[pos] * gain;
+    }
+    CurrentGain = gain;
+
+    if(!(std::abs(gain) > GainSilenceThreshold))
+        return;
+    if(size_t todo{(InSamples.size()-pos) >> 2})
+    {
+        const float32x4_t gain4 = vdupq_n_f32(gain);
+        do {
+            const float32x4_t val4 = vld1q_f32(&InSamples[pos]);
+            float32x4_t dry4 = vld1q_f32(&dst[pos]);
+            dry4 = vmlaq_f32(dry4, val4, gain4);
+            vst1q_f32(&dst[pos], dry4);
+            pos += 4;
+        } while(--todo);
+    }
+    for(size_t leftover{(InSamples.size()-pos)&3};leftover;++pos,--leftover)
+        dst[pos] += InSamples[pos] * gain;
+}
+
+} // namespace
+
+template<>
+void Resample_<LerpTag,NEONTag>(const InterpState*, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    ASSUME(frac < MixerFracOne);
+
+    const int32x4_t increment4 = vdupq_n_s32(static_cast<int>(increment*4));
+    const float32x4_t fracOne4 = vdupq_n_f32(1.0f/MixerFracOne);
+    const int32x4_t fracMask4 = vdupq_n_s32(MixerFracMask);
+    alignas(16) uint pos_[4], frac_[4];
+    int32x4_t pos4, frac4;
+
+    InitPosArrays(frac, increment, frac_, pos_);
+    frac4 = vld1q_s32(reinterpret_cast<int*>(frac_));
+    pos4 = vld1q_s32(reinterpret_cast<int*>(pos_));
+
+    auto dst_iter = dst.begin();
+    for(size_t todo{dst.size()>>2};todo;--todo)
+    {
+        const int pos0{vgetq_lane_s32(pos4, 0)};
+        const int pos1{vgetq_lane_s32(pos4, 1)};
+        const int pos2{vgetq_lane_s32(pos4, 2)};
+        const int pos3{vgetq_lane_s32(pos4, 3)};
+        const float32x4_t val1{set_f4(src[pos0], src[pos1], src[pos2], src[pos3])};
+        const float32x4_t val2{set_f4(src[pos0+1], src[pos1+1], src[pos2+1], src[pos3+1])};
+
+        /* val1 + (val2-val1)*mu */
+        const float32x4_t r0{vsubq_f32(val2, val1)};
+        const float32x4_t mu{vmulq_f32(vcvtq_f32_s32(frac4), fracOne4)};
+        const float32x4_t out{vmlaq_f32(val1, mu, r0)};
+
+        vst1q_f32(dst_iter, out);
+        dst_iter += 4;
+
+        frac4 = vaddq_s32(frac4, increment4);
+        pos4 = vaddq_s32(pos4, vshrq_n_s32(frac4, MixerFracBits));
+        frac4 = vandq_s32(frac4, fracMask4);
+    }
+
+    if(size_t todo{dst.size()&3})
+    {
+        src += static_cast<uint>(vgetq_lane_s32(pos4, 0));
+        frac = static_cast<uint>(vgetq_lane_s32(frac4, 0));
+
+        do {
+            *(dst_iter++) = lerpf(src[0], src[1], static_cast<float>(frac) * (1.0f/MixerFracOne));
+
+            frac += increment;
+            src  += frac>>MixerFracBits;
+            frac &= MixerFracMask;
+        } while(--todo);
+    }
+}
+
+template<>
+void Resample_<CubicTag,NEONTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    ASSUME(frac < MixerFracOne);
+
+    const CubicCoefficients *RESTRICT filter = al::assume_aligned<16>(state->cubic.filter);
+
+    src -= 1;
+    for(float &out_sample : dst)
+    {
+        const uint pi{frac >> CubicPhaseDiffBits};
+        const float pf{static_cast<float>(frac&CubicPhaseDiffMask) * (1.0f/CubicPhaseDiffOne)};
+        const float32x4_t pf4{vdupq_n_f32(pf)};
+
+        /* Apply the phase interpolated filter. */
+
+        /* f = fil + pf*phd */
+        const float32x4_t f4 = vmlaq_f32(vld1q_f32(filter[pi].mCoeffs), pf4,
+            vld1q_f32(filter[pi].mDeltas));
+        /* r = f*src */
+        float32x4_t r4{vmulq_f32(f4, vld1q_f32(src))};
+
+        r4 = vaddq_f32(r4, vrev64q_f32(r4));
+        out_sample = vget_lane_f32(vadd_f32(vget_low_f32(r4), vget_high_f32(r4)), 0);
+
+        frac += increment;
+        src  += frac>>MixerFracBits;
+        frac &= MixerFracMask;
+    }
+}
+
+template<>
+void Resample_<BSincTag,NEONTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    const float *const filter{state->bsinc.filter};
+    const float32x4_t sf4{vdupq_n_f32(state->bsinc.sf)};
+    const size_t m{state->bsinc.m};
+    ASSUME(m > 0);
+    ASSUME(frac < MixerFracOne);
+
+    src -= state->bsinc.l;
+    for(float &out_sample : dst)
+    {
+        // Calculate the phase index and factor.
+        const uint pi{frac >> BSincPhaseDiffBits};
+        const float pf{static_cast<float>(frac&BSincPhaseDiffMask) * (1.0f/BSincPhaseDiffOne)};
+
+        // Apply the scale and phase interpolated filter.
+        float32x4_t r4{vdupq_n_f32(0.0f)};
+        {
+            const float32x4_t pf4{vdupq_n_f32(pf)};
+            const float *RESTRICT fil{filter + m*pi*2};
+            const float *RESTRICT phd{fil + m};
+            const float *RESTRICT scd{fil + BSincPhaseCount*2*m};
+            const float *RESTRICT spd{scd + m};
+            size_t td{m >> 2};
+            size_t j{0u};
+
+            do {
+                /* f = ((fil + sf*scd) + pf*(phd + sf*spd)) */
+                const float32x4_t f4 = vmlaq_f32(
+                    vmlaq_f32(vld1q_f32(&fil[j]), sf4, vld1q_f32(&scd[j])),
+                    pf4, vmlaq_f32(vld1q_f32(&phd[j]), sf4, vld1q_f32(&spd[j])));
+                /* r += f*src */
+                r4 = vmlaq_f32(r4, f4, vld1q_f32(&src[j]));
+                j += 4;
+            } while(--td);
+        }
+        r4 = vaddq_f32(r4, vrev64q_f32(r4));
+        out_sample = vget_lane_f32(vadd_f32(vget_low_f32(r4), vget_high_f32(r4)), 0);
+
+        frac += increment;
+        src  += frac>>MixerFracBits;
+        frac &= MixerFracMask;
+    }
+}
+
+template<>
+void Resample_<FastBSincTag,NEONTag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    const float *const filter{state->bsinc.filter};
+    const size_t m{state->bsinc.m};
+    ASSUME(m > 0);
+    ASSUME(frac < MixerFracOne);
+
+    src -= state->bsinc.l;
+    for(float &out_sample : dst)
+    {
+        // Calculate the phase index and factor.
+        const uint pi{frac >> BSincPhaseDiffBits};
+        const float pf{static_cast<float>(frac&BSincPhaseDiffMask) * (1.0f/BSincPhaseDiffOne)};
+
+        // Apply the phase interpolated filter.
+        float32x4_t r4{vdupq_n_f32(0.0f)};
+        {
+            const float32x4_t pf4{vdupq_n_f32(pf)};
+            const float *RESTRICT fil{filter + m*pi*2};
+            const float *RESTRICT phd{fil + m};
+            size_t td{m >> 2};
+            size_t j{0u};
+
+            do {
+                /* f = fil + pf*phd */
+                const float32x4_t f4 = vmlaq_f32(vld1q_f32(&fil[j]), pf4, vld1q_f32(&phd[j]));
+                /* r += f*src */
+                r4 = vmlaq_f32(r4, f4, vld1q_f32(&src[j]));
+                j += 4;
+            } while(--td);
+        }
+        r4 = vaddq_f32(r4, vrev64q_f32(r4));
+        out_sample = vget_lane_f32(vadd_f32(vget_low_f32(r4), vget_high_f32(r4)), 0);
+
+        frac += increment;
+        src  += frac>>MixerFracBits;
+        frac &= MixerFracMask;
+    }
+}
+
+
+template<>
+void MixHrtf_<NEONTag>(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const MixHrtfFilter *hrtfparams, const size_t BufferSize)
+{ MixHrtfBase<ApplyCoeffs>(InSamples, AccumSamples, IrSize, hrtfparams, BufferSize); }
+
+template<>
+void MixHrtfBlend_<NEONTag>(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const HrtfFilter *oldparams, const MixHrtfFilter *newparams, const size_t BufferSize)
+{
+    MixHrtfBlendBase<ApplyCoeffs>(InSamples, AccumSamples, IrSize, oldparams, newparams,
+        BufferSize);
+}
+
+template<>
+void MixDirectHrtf_<NEONTag>(const FloatBufferSpan LeftOut, const FloatBufferSpan RightOut,
+    const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples,
+    float *TempBuf, HrtfChannelState *ChanState, const size_t IrSize, const size_t BufferSize)
+{
+    MixDirectHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, TempBuf, ChanState,
+        IrSize, BufferSize);
+}
+
+
+template<>
+void Mix_<NEONTag>(const al::span<const float> InSamples, const al::span<FloatBufferLine> OutBuffer,
+    float *CurrentGains, const float *TargetGains, const size_t Counter, const size_t OutPos)
+{
+    const float delta{(Counter > 0) ? 1.0f / static_cast<float>(Counter) : 0.0f};
+    const auto min_len = minz(Counter, InSamples.size());
+    const auto aligned_len = minz((min_len+3) & ~size_t{3}, InSamples.size()) - min_len;
+
+    for(FloatBufferLine &output : OutBuffer)
+        MixLine(InSamples, al::assume_aligned<16>(output.data()+OutPos), *CurrentGains++,
+            *TargetGains++, delta, min_len, aligned_len, Counter);
+}
+
+template<>
+void Mix_<NEONTag>(const al::span<const float> InSamples, float *OutBuffer, float &CurrentGain,
+    const float TargetGain, const size_t Counter)
+{
+    const float delta{(Counter > 0) ? 1.0f / static_cast<float>(Counter) : 0.0f};
+    const auto min_len = minz(Counter, InSamples.size());
+    const auto aligned_len = minz((min_len+3) & ~size_t{3}, InSamples.size()) - min_len;
+
+    MixLine(InSamples, al::assume_aligned<16>(OutBuffer), CurrentGain, TargetGain, delta, min_len,
+        aligned_len, Counter);
+}
diff --git a/core/mixer/mixer_sse.cpp b/core/mixer/mixer_sse.cpp
new file mode 100644 (file)
index 0000000..0aa5d5f
--- /dev/null
@@ -0,0 +1,327 @@
+#include "config.h"
+
+#include <xmmintrin.h>
+
+#include <cmath>
+#include <limits>
+
+#include "alnumeric.h"
+#include "core/bsinc_defs.h"
+#include "core/cubic_defs.h"
+#include "defs.h"
+#include "hrtfbase.h"
+
+struct SSETag;
+struct CubicTag;
+struct BSincTag;
+struct FastBSincTag;
+
+
+#if defined(__GNUC__) && !defined(__clang__) && !defined(__SSE__)
+#pragma GCC target("sse")
+#endif
+
+namespace {
+
+constexpr uint BSincPhaseDiffBits{MixerFracBits - BSincPhaseBits};
+constexpr uint BSincPhaseDiffOne{1 << BSincPhaseDiffBits};
+constexpr uint BSincPhaseDiffMask{BSincPhaseDiffOne - 1u};
+
+constexpr uint CubicPhaseDiffBits{MixerFracBits - CubicPhaseBits};
+constexpr uint CubicPhaseDiffOne{1 << CubicPhaseDiffBits};
+constexpr uint CubicPhaseDiffMask{CubicPhaseDiffOne - 1u};
+
+#define MLA4(x, y, z) _mm_add_ps(x, _mm_mul_ps(y, z))
+
+inline void ApplyCoeffs(float2 *RESTRICT Values, const size_t IrSize, const ConstHrirSpan Coeffs,
+    const float left, const float right)
+{
+    const __m128 lrlr{_mm_setr_ps(left, right, left, right)};
+
+    ASSUME(IrSize >= MinIrLength);
+    /* This isn't technically correct to test alignment, but it's true for
+     * systems that support SSE, which is the only one that needs to know the
+     * alignment of Values (which alternates between 8- and 16-byte aligned).
+     */
+    if(!(reinterpret_cast<uintptr_t>(Values)&15))
+    {
+        for(size_t i{0};i < IrSize;i += 2)
+        {
+            const __m128 coeffs{_mm_load_ps(Coeffs[i].data())};
+            __m128 vals{_mm_load_ps(Values[i].data())};
+            vals = MLA4(vals, lrlr, coeffs);
+            _mm_store_ps(Values[i].data(), vals);
+        }
+    }
+    else
+    {
+        __m128 imp0, imp1;
+        __m128 coeffs{_mm_load_ps(Coeffs[0].data())};
+        __m128 vals{_mm_loadl_pi(_mm_setzero_ps(), reinterpret_cast<__m64*>(Values[0].data()))};
+        imp0 = _mm_mul_ps(lrlr, coeffs);
+        vals = _mm_add_ps(imp0, vals);
+        _mm_storel_pi(reinterpret_cast<__m64*>(Values[0].data()), vals);
+        size_t td{((IrSize+1)>>1) - 1};
+        size_t i{1};
+        do {
+            coeffs = _mm_load_ps(Coeffs[i+1].data());
+            vals = _mm_load_ps(Values[i].data());
+            imp1 = _mm_mul_ps(lrlr, coeffs);
+            imp0 = _mm_shuffle_ps(imp0, imp1, _MM_SHUFFLE(1, 0, 3, 2));
+            vals = _mm_add_ps(imp0, vals);
+            _mm_store_ps(Values[i].data(), vals);
+            imp0 = imp1;
+            i += 2;
+        } while(--td);
+        vals = _mm_loadl_pi(vals, reinterpret_cast<__m64*>(Values[i].data()));
+        imp0 = _mm_movehl_ps(imp0, imp0);
+        vals = _mm_add_ps(imp0, vals);
+        _mm_storel_pi(reinterpret_cast<__m64*>(Values[i].data()), vals);
+    }
+}
+
+force_inline void MixLine(const al::span<const float> InSamples, float *RESTRICT dst,
+    float &CurrentGain, const float TargetGain, const float delta, const size_t min_len,
+    const size_t aligned_len, size_t Counter)
+{
+    float gain{CurrentGain};
+    const float step{(TargetGain-gain) * delta};
+
+    size_t pos{0};
+    if(!(std::abs(step) > std::numeric_limits<float>::epsilon()))
+        gain = TargetGain;
+    else
+    {
+        float step_count{0.0f};
+        /* Mix with applying gain steps in aligned multiples of 4. */
+        if(size_t todo{min_len >> 2})
+        {
+            const __m128 four4{_mm_set1_ps(4.0f)};
+            const __m128 step4{_mm_set1_ps(step)};
+            const __m128 gain4{_mm_set1_ps(gain)};
+            __m128 step_count4{_mm_setr_ps(0.0f, 1.0f, 2.0f, 3.0f)};
+            do {
+                const __m128 val4{_mm_load_ps(&InSamples[pos])};
+                __m128 dry4{_mm_load_ps(&dst[pos])};
+
+                /* dry += val * (gain + step*step_count) */
+                dry4 = MLA4(dry4, val4, MLA4(gain4, step4, step_count4));
+
+                _mm_store_ps(&dst[pos], dry4);
+                step_count4 = _mm_add_ps(step_count4, four4);
+                pos += 4;
+            } while(--todo);
+            /* NOTE: step_count4 now represents the next four counts after the
+             * last four mixed samples, so the lowest element represents the
+             * next step count to apply.
+             */
+            step_count = _mm_cvtss_f32(step_count4);
+        }
+        /* Mix with applying left over gain steps that aren't aligned multiples of 4. */
+        for(size_t leftover{min_len&3};leftover;++pos,--leftover)
+        {
+            dst[pos] += InSamples[pos] * (gain + step*step_count);
+            step_count += 1.0f;
+        }
+        if(pos == Counter)
+            gain = TargetGain;
+        else
+            gain += step*step_count;
+
+        /* Mix until pos is aligned with 4 or the mix is done. */
+        for(size_t leftover{aligned_len&3};leftover;++pos,--leftover)
+            dst[pos] += InSamples[pos] * gain;
+    }
+    CurrentGain = gain;
+
+    if(!(std::abs(gain) > GainSilenceThreshold))
+        return;
+    if(size_t todo{(InSamples.size()-pos) >> 2})
+    {
+        const __m128 gain4{_mm_set1_ps(gain)};
+        do {
+            const __m128 val4{_mm_load_ps(&InSamples[pos])};
+            __m128 dry4{_mm_load_ps(&dst[pos])};
+            dry4 = _mm_add_ps(dry4, _mm_mul_ps(val4, gain4));
+            _mm_store_ps(&dst[pos], dry4);
+            pos += 4;
+        } while(--todo);
+    }
+    for(size_t leftover{(InSamples.size()-pos)&3};leftover;++pos,--leftover)
+        dst[pos] += InSamples[pos] * gain;
+}
+
+} // namespace
+
+template<>
+void Resample_<CubicTag,SSETag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    ASSUME(frac < MixerFracOne);
+
+    const CubicCoefficients *RESTRICT filter = al::assume_aligned<16>(state->cubic.filter);
+
+    src -= 1;
+    for(float &out_sample : dst)
+    {
+        const uint pi{frac >> CubicPhaseDiffBits};
+        const float pf{static_cast<float>(frac&CubicPhaseDiffMask) * (1.0f/CubicPhaseDiffOne)};
+        const __m128 pf4{_mm_set1_ps(pf)};
+
+        /* Apply the phase interpolated filter. */
+
+        /* f = fil + pf*phd */
+        const __m128 f4 = MLA4(_mm_load_ps(filter[pi].mCoeffs), pf4,
+            _mm_load_ps(filter[pi].mDeltas));
+        /* r = f*src */
+        __m128 r4{_mm_mul_ps(f4, _mm_loadu_ps(src))};
+
+        r4 = _mm_add_ps(r4, _mm_shuffle_ps(r4, r4, _MM_SHUFFLE(0, 1, 2, 3)));
+        r4 = _mm_add_ps(r4, _mm_movehl_ps(r4, r4));
+        out_sample = _mm_cvtss_f32(r4);
+
+        frac += increment;
+        src  += frac>>MixerFracBits;
+        frac &= MixerFracMask;
+    }
+}
+
+template<>
+void Resample_<BSincTag,SSETag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    const float *const filter{state->bsinc.filter};
+    const __m128 sf4{_mm_set1_ps(state->bsinc.sf)};
+    const size_t m{state->bsinc.m};
+    ASSUME(m > 0);
+    ASSUME(frac < MixerFracOne);
+
+    src -= state->bsinc.l;
+    for(float &out_sample : dst)
+    {
+        // Calculate the phase index and factor.
+        const uint pi{frac >> BSincPhaseDiffBits};
+        const float pf{static_cast<float>(frac&BSincPhaseDiffMask) * (1.0f/BSincPhaseDiffOne)};
+
+        // Apply the scale and phase interpolated filter.
+        __m128 r4{_mm_setzero_ps()};
+        {
+            const __m128 pf4{_mm_set1_ps(pf)};
+            const float *RESTRICT fil{filter + m*pi*2};
+            const float *RESTRICT phd{fil + m};
+            const float *RESTRICT scd{fil + BSincPhaseCount*2*m};
+            const float *RESTRICT spd{scd + m};
+            size_t td{m >> 2};
+            size_t j{0u};
+
+            do {
+                /* f = ((fil + sf*scd) + pf*(phd + sf*spd)) */
+                const __m128 f4 = MLA4(
+                    MLA4(_mm_load_ps(&fil[j]), sf4, _mm_load_ps(&scd[j])),
+                    pf4, MLA4(_mm_load_ps(&phd[j]), sf4, _mm_load_ps(&spd[j])));
+                /* r += f*src */
+                r4 = MLA4(r4, f4, _mm_loadu_ps(&src[j]));
+                j += 4;
+            } while(--td);
+        }
+        r4 = _mm_add_ps(r4, _mm_shuffle_ps(r4, r4, _MM_SHUFFLE(0, 1, 2, 3)));
+        r4 = _mm_add_ps(r4, _mm_movehl_ps(r4, r4));
+        out_sample = _mm_cvtss_f32(r4);
+
+        frac += increment;
+        src  += frac>>MixerFracBits;
+        frac &= MixerFracMask;
+    }
+}
+
+template<>
+void Resample_<FastBSincTag,SSETag>(const InterpState *state, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    const float *const filter{state->bsinc.filter};
+    const size_t m{state->bsinc.m};
+    ASSUME(m > 0);
+    ASSUME(frac < MixerFracOne);
+
+    src -= state->bsinc.l;
+    for(float &out_sample : dst)
+    {
+        // Calculate the phase index and factor.
+        const uint pi{frac >> BSincPhaseDiffBits};
+        const float pf{static_cast<float>(frac&BSincPhaseDiffMask) * (1.0f/BSincPhaseDiffOne)};
+
+        // Apply the phase interpolated filter.
+        __m128 r4{_mm_setzero_ps()};
+        {
+            const __m128 pf4{_mm_set1_ps(pf)};
+            const float *RESTRICT fil{filter + m*pi*2};
+            const float *RESTRICT phd{fil + m};
+            size_t td{m >> 2};
+            size_t j{0u};
+
+            do {
+                /* f = fil + pf*phd */
+                const __m128 f4 = MLA4(_mm_load_ps(&fil[j]), pf4, _mm_load_ps(&phd[j]));
+                /* r += f*src */
+                r4 = MLA4(r4, f4, _mm_loadu_ps(&src[j]));
+                j += 4;
+            } while(--td);
+        }
+        r4 = _mm_add_ps(r4, _mm_shuffle_ps(r4, r4, _MM_SHUFFLE(0, 1, 2, 3)));
+        r4 = _mm_add_ps(r4, _mm_movehl_ps(r4, r4));
+        out_sample = _mm_cvtss_f32(r4);
+
+        frac += increment;
+        src  += frac>>MixerFracBits;
+        frac &= MixerFracMask;
+    }
+}
+
+
+template<>
+void MixHrtf_<SSETag>(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const MixHrtfFilter *hrtfparams, const size_t BufferSize)
+{ MixHrtfBase<ApplyCoeffs>(InSamples, AccumSamples, IrSize, hrtfparams, BufferSize); }
+
+template<>
+void MixHrtfBlend_<SSETag>(const float *InSamples, float2 *AccumSamples, const uint IrSize,
+    const HrtfFilter *oldparams, const MixHrtfFilter *newparams, const size_t BufferSize)
+{
+    MixHrtfBlendBase<ApplyCoeffs>(InSamples, AccumSamples, IrSize, oldparams, newparams,
+        BufferSize);
+}
+
+template<>
+void MixDirectHrtf_<SSETag>(const FloatBufferSpan LeftOut, const FloatBufferSpan RightOut,
+    const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples,
+    float *TempBuf, HrtfChannelState *ChanState, const size_t IrSize, const size_t BufferSize)
+{
+    MixDirectHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, TempBuf, ChanState,
+        IrSize, BufferSize);
+}
+
+
+template<>
+void Mix_<SSETag>(const al::span<const float> InSamples, const al::span<FloatBufferLine> OutBuffer,
+    float *CurrentGains, const float *TargetGains, const size_t Counter, const size_t OutPos)
+{
+    const float delta{(Counter > 0) ? 1.0f / static_cast<float>(Counter) : 0.0f};
+    const auto min_len = minz(Counter, InSamples.size());
+    const auto aligned_len = minz((min_len+3) & ~size_t{3}, InSamples.size()) - min_len;
+
+    for(FloatBufferLine &output : OutBuffer)
+        MixLine(InSamples, al::assume_aligned<16>(output.data()+OutPos), *CurrentGains++,
+            *TargetGains++, delta, min_len, aligned_len, Counter);
+}
+
+template<>
+void Mix_<SSETag>(const al::span<const float> InSamples, float *OutBuffer, float &CurrentGain,
+    const float TargetGain, const size_t Counter)
+{
+    const float delta{(Counter > 0) ? 1.0f / static_cast<float>(Counter) : 0.0f};
+    const auto min_len = minz(Counter, InSamples.size());
+    const auto aligned_len = minz((min_len+3) & ~size_t{3}, InSamples.size()) - min_len;
+
+    MixLine(InSamples, al::assume_aligned<16>(OutBuffer), CurrentGain, TargetGain, delta, min_len,
+        aligned_len, Counter);
+}
diff --git a/core/mixer/mixer_sse2.cpp b/core/mixer/mixer_sse2.cpp
new file mode 100644 (file)
index 0000000..edaaf7a
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2014 by Timothy Arceri <t_arceri@yahoo.com.au>.
+ * 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 <xmmintrin.h>
+#include <emmintrin.h>
+
+#include "alnumeric.h"
+#include "defs.h"
+
+struct SSE2Tag;
+struct LerpTag;
+
+
+#if defined(__GNUC__) && !defined(__clang__) && !defined(__SSE2__)
+#pragma GCC target("sse2")
+#endif
+
+template<>
+void Resample_<LerpTag,SSE2Tag>(const InterpState*, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    ASSUME(frac < MixerFracOne);
+
+    const __m128i increment4{_mm_set1_epi32(static_cast<int>(increment*4))};
+    const __m128 fracOne4{_mm_set1_ps(1.0f/MixerFracOne)};
+    const __m128i fracMask4{_mm_set1_epi32(MixerFracMask)};
+
+    alignas(16) uint pos_[4], frac_[4];
+    InitPosArrays(frac, increment, frac_, pos_);
+    __m128i frac4{_mm_setr_epi32(static_cast<int>(frac_[0]), static_cast<int>(frac_[1]),
+        static_cast<int>(frac_[2]), static_cast<int>(frac_[3]))};
+    __m128i pos4{_mm_setr_epi32(static_cast<int>(pos_[0]), static_cast<int>(pos_[1]),
+        static_cast<int>(pos_[2]), static_cast<int>(pos_[3]))};
+
+    auto dst_iter = dst.begin();
+    for(size_t todo{dst.size()>>2};todo;--todo)
+    {
+        const int pos0{_mm_cvtsi128_si32(pos4)};
+        const int pos1{_mm_cvtsi128_si32(_mm_srli_si128(pos4, 4))};
+        const int pos2{_mm_cvtsi128_si32(_mm_srli_si128(pos4, 8))};
+        const int pos3{_mm_cvtsi128_si32(_mm_srli_si128(pos4, 12))};
+        const __m128 val1{_mm_setr_ps(src[pos0  ], src[pos1  ], src[pos2  ], src[pos3  ])};
+        const __m128 val2{_mm_setr_ps(src[pos0+1], src[pos1+1], src[pos2+1], src[pos3+1])};
+
+        /* val1 + (val2-val1)*mu */
+        const __m128 r0{_mm_sub_ps(val2, val1)};
+        const __m128 mu{_mm_mul_ps(_mm_cvtepi32_ps(frac4), fracOne4)};
+        const __m128 out{_mm_add_ps(val1, _mm_mul_ps(mu, r0))};
+
+        _mm_store_ps(dst_iter, out);
+        dst_iter += 4;
+
+        frac4 = _mm_add_epi32(frac4, increment4);
+        pos4 = _mm_add_epi32(pos4, _mm_srli_epi32(frac4, MixerFracBits));
+        frac4 = _mm_and_si128(frac4, fracMask4);
+    }
+
+    if(size_t todo{dst.size()&3})
+    {
+        src += static_cast<uint>(_mm_cvtsi128_si32(pos4));
+        frac = static_cast<uint>(_mm_cvtsi128_si32(frac4));
+
+        do {
+            *(dst_iter++) = lerpf(src[0], src[1], static_cast<float>(frac) * (1.0f/MixerFracOne));
+
+            frac += increment;
+            src  += frac>>MixerFracBits;
+            frac &= MixerFracMask;
+        } while(--todo);
+    }
+}
diff --git a/core/mixer/mixer_sse3.cpp b/core/mixer/mixer_sse3.cpp
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/core/mixer/mixer_sse41.cpp b/core/mixer/mixer_sse41.cpp
new file mode 100644 (file)
index 0000000..8ccd9fd
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2014 by Timothy Arceri <t_arceri@yahoo.com.au>.
+ * 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 <xmmintrin.h>
+#include <emmintrin.h>
+#include <smmintrin.h>
+
+#include "alnumeric.h"
+#include "defs.h"
+
+struct SSE4Tag;
+struct LerpTag;
+
+
+#if defined(__GNUC__) && !defined(__clang__) && !defined(__SSE4_1__)
+#pragma GCC target("sse4.1")
+#endif
+
+template<>
+void Resample_<LerpTag,SSE4Tag>(const InterpState*, const float *RESTRICT src, uint frac,
+    const uint increment, const al::span<float> dst)
+{
+    ASSUME(frac < MixerFracOne);
+
+    const __m128i increment4{_mm_set1_epi32(static_cast<int>(increment*4))};
+    const __m128 fracOne4{_mm_set1_ps(1.0f/MixerFracOne)};
+    const __m128i fracMask4{_mm_set1_epi32(MixerFracMask)};
+
+    alignas(16) uint pos_[4], frac_[4];
+    InitPosArrays(frac, increment, frac_, pos_);
+    __m128i frac4{_mm_setr_epi32(static_cast<int>(frac_[0]), static_cast<int>(frac_[1]),
+        static_cast<int>(frac_[2]), static_cast<int>(frac_[3]))};
+    __m128i pos4{_mm_setr_epi32(static_cast<int>(pos_[0]), static_cast<int>(pos_[1]),
+        static_cast<int>(pos_[2]), static_cast<int>(pos_[3]))};
+
+    auto dst_iter = dst.begin();
+    for(size_t todo{dst.size()>>2};todo;--todo)
+    {
+        const int pos0{_mm_extract_epi32(pos4, 0)};
+        const int pos1{_mm_extract_epi32(pos4, 1)};
+        const int pos2{_mm_extract_epi32(pos4, 2)};
+        const int pos3{_mm_extract_epi32(pos4, 3)};
+        const __m128 val1{_mm_setr_ps(src[pos0  ], src[pos1  ], src[pos2  ], src[pos3  ])};
+        const __m128 val2{_mm_setr_ps(src[pos0+1], src[pos1+1], src[pos2+1], src[pos3+1])};
+
+        /* val1 + (val2-val1)*mu */
+        const __m128 r0{_mm_sub_ps(val2, val1)};
+        const __m128 mu{_mm_mul_ps(_mm_cvtepi32_ps(frac4), fracOne4)};
+        const __m128 out{_mm_add_ps(val1, _mm_mul_ps(mu, r0))};
+
+        _mm_store_ps(dst_iter, out);
+        dst_iter += 4;
+
+        frac4 = _mm_add_epi32(frac4, increment4);
+        pos4 = _mm_add_epi32(pos4, _mm_srli_epi32(frac4, MixerFracBits));
+        frac4 = _mm_and_si128(frac4, fracMask4);
+    }
+
+    if(size_t todo{dst.size()&3})
+    {
+        /* NOTE: These four elements represent the position *after* the last
+         * four samples, so the lowest element is the next position to
+         * resample.
+         */
+        src += static_cast<uint>(_mm_cvtsi128_si32(pos4));
+        frac = static_cast<uint>(_mm_cvtsi128_si32(frac4));
+
+        do {
+            *(dst_iter++) = lerpf(src[0], src[1], static_cast<float>(frac) * (1.0f/MixerFracOne));
+
+            frac += increment;
+            src  += frac>>MixerFracBits;
+            frac &= MixerFracMask;
+        } while(--todo);
+    }
+}
diff --git a/core/resampler_limits.h b/core/resampler_limits.h
new file mode 100644 (file)
index 0000000..9d4cefd
--- /dev/null
@@ -0,0 +1,12 @@
+#ifndef CORE_RESAMPLER_LIMITS_H
+#define CORE_RESAMPLER_LIMITS_H
+
+/* Maximum number of samples to pad on the ends of a buffer for resampling.
+ * Note that the padding is symmetric (half at the beginning and half at the
+ * end)!
+ */
+constexpr int MaxResamplerPadding{48};
+
+constexpr int MaxResamplerEdge{MaxResamplerPadding >> 1};
+
+#endif /* CORE_RESAMPLER_LIMITS_H */
diff --git a/core/rtkit.cpp b/core/rtkit.cpp
new file mode 100644 (file)
index 0000000..ff944eb
--- /dev/null
@@ -0,0 +1,236 @@
+/*-*- Mode: C; c-basic-offset: 8 -*-*/
+
+/***
+        Copyright 2009 Lennart Poettering
+        Copyright 2010 David Henningsson <diwic@ubuntu.com>
+        Copyright 2021 Chris Robinson
+
+        Permission is hereby granted, free of charge, to any person
+        obtaining a copy of this software and associated documentation files
+        (the "Software"), to deal in the Software without restriction,
+        including without limitation the rights to use, copy, modify, merge,
+        publish, distribute, sublicense, and/or sell copies of the Software,
+        and to permit persons to whom the Software is furnished to do so,
+        subject to the following conditions:
+
+        The above copyright notice and this permission notice shall be
+        included in all copies or substantial portions of the Software.
+
+        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+        EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+        MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+        NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+        BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+        ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+        CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+        SOFTWARE.
+***/
+
+#include "config.h"
+
+#include "rtkit.h"
+
+#include <errno.h>
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <memory>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#ifdef __linux__
+#include <sys/syscall.h>
+#elif defined(__FreeBSD__)
+#include <sys/thr.h>
+#endif
+
+
+namespace dbus {
+
+constexpr int TypeString{'s'};
+constexpr int TypeVariant{'v'};
+constexpr int TypeInt32{'i'};
+constexpr int TypeUInt32{'u'};
+constexpr int TypeInt64{'x'};
+constexpr int TypeUInt64{'t'};
+constexpr int TypeInvalid{'\0'};
+
+struct MessageDeleter {
+    void operator()(DBusMessage *m) { dbus_message_unref(m); }
+};
+using MessagePtr = std::unique_ptr<DBusMessage,MessageDeleter>;
+
+} // namespace dbus
+
+namespace {
+
+inline pid_t _gettid()
+{
+#ifdef __linux__
+    return static_cast<pid_t>(syscall(SYS_gettid));
+#elif defined(__FreeBSD__)
+    long pid{};
+    thr_self(&pid);
+    return static_cast<pid_t>(pid);
+#else
+#warning gettid not available
+    return 0;
+#endif
+}
+
+int translate_error(const char *name)
+{
+    if(strcmp(name, DBUS_ERROR_NO_MEMORY) == 0)
+        return -ENOMEM;
+    if(strcmp(name, DBUS_ERROR_SERVICE_UNKNOWN) == 0
+        || strcmp(name, DBUS_ERROR_NAME_HAS_NO_OWNER) == 0)
+        return -ENOENT;
+    if(strcmp(name, DBUS_ERROR_ACCESS_DENIED) == 0
+        || strcmp(name, DBUS_ERROR_AUTH_FAILED) == 0)
+        return -EACCES;
+    return -EIO;
+}
+
+int rtkit_get_int_property(DBusConnection *connection, const char *propname, long long *propval)
+{
+    dbus::MessagePtr m{dbus_message_new_method_call(RTKIT_SERVICE_NAME, RTKIT_OBJECT_PATH,
+        "org.freedesktop.DBus.Properties", "Get")};
+    if(!m) return -ENOMEM;
+
+    const char *interfacestr = RTKIT_SERVICE_NAME;
+    auto ready = dbus_message_append_args(m.get(),
+        dbus::TypeString, &interfacestr,
+        dbus::TypeString, &propname,
+        dbus::TypeInvalid);
+    if(!ready) return -ENOMEM;
+
+    dbus::Error error;
+    dbus::MessagePtr r{dbus_connection_send_with_reply_and_block(connection, m.get(), -1,
+        &error.get())};
+    if(!r) return translate_error(error->name);
+
+    if(dbus_set_error_from_message(&error.get(), r.get()))
+        return translate_error(error->name);
+
+    int ret{-EBADMSG};
+    DBusMessageIter iter{};
+    dbus_message_iter_init(r.get(), &iter);
+    while(int curtype{dbus_message_iter_get_arg_type(&iter)})
+    {
+        if(curtype == dbus::TypeVariant)
+        {
+            DBusMessageIter subiter{};
+            dbus_message_iter_recurse(&iter, &subiter);
+
+            while((curtype=dbus_message_iter_get_arg_type(&subiter)) != dbus::TypeInvalid)
+            {
+                if(curtype == dbus::TypeInt32)
+                {
+                    dbus_int32_t i32{};
+                    dbus_message_iter_get_basic(&subiter, &i32);
+                    *propval = i32;
+                    ret = 0;
+                }
+
+                if(curtype == dbus::TypeInt64)
+                {
+                    dbus_int64_t i64{};
+                    dbus_message_iter_get_basic(&subiter, &i64);
+                    *propval = i64;
+                    ret = 0;
+                }
+
+                dbus_message_iter_next(&subiter);
+            }
+        }
+        dbus_message_iter_next(&iter);
+    }
+
+    return ret;
+}
+
+} // namespace
+
+int rtkit_get_max_realtime_priority(DBusConnection *connection)
+{
+    long long retval{};
+    int err{rtkit_get_int_property(connection, "MaxRealtimePriority", &retval)};
+    return err < 0 ? err : static_cast<int>(retval);
+}
+
+int rtkit_get_min_nice_level(DBusConnection *connection, int *min_nice_level)
+{
+    long long retval{};
+    int err{rtkit_get_int_property(connection, "MinNiceLevel", &retval)};
+    if(err >= 0) *min_nice_level = static_cast<int>(retval);
+    return err;
+}
+
+long long rtkit_get_rttime_usec_max(DBusConnection *connection)
+{
+    long long retval{};
+    int err{rtkit_get_int_property(connection, "RTTimeUSecMax", &retval)};
+    return err < 0 ? err : retval;
+}
+
+int rtkit_make_realtime(DBusConnection *connection, pid_t thread, int priority)
+{
+    if(thread == 0)
+        thread = _gettid();
+    if(thread == 0)
+        return -ENOTSUP;
+
+    dbus::MessagePtr m{dbus_message_new_method_call(RTKIT_SERVICE_NAME, RTKIT_OBJECT_PATH,
+        "org.freedesktop.RealtimeKit1", "MakeThreadRealtime")};
+    if(!m) return -ENOMEM;
+
+    auto u64 = static_cast<dbus_uint64_t>(thread);
+    auto u32 = static_cast<dbus_uint32_t>(priority);
+    auto ready = dbus_message_append_args(m.get(),
+        dbus::TypeUInt64, &u64,
+        dbus::TypeUInt32, &u32,
+        dbus::TypeInvalid);
+    if(!ready) return -ENOMEM;
+
+    dbus::Error error;
+    dbus::MessagePtr r{dbus_connection_send_with_reply_and_block(connection, m.get(), -1,
+        &error.get())};
+    if(!r) return translate_error(error->name);
+
+    if(dbus_set_error_from_message(&error.get(), r.get()))
+        return translate_error(error->name);
+
+    return 0;
+}
+
+int rtkit_make_high_priority(DBusConnection *connection, pid_t thread, int nice_level)
+{
+    if(thread == 0)
+        thread = _gettid();
+    if(thread == 0)
+        return -ENOTSUP;
+
+    dbus::MessagePtr m{dbus_message_new_method_call(RTKIT_SERVICE_NAME, RTKIT_OBJECT_PATH,
+        "org.freedesktop.RealtimeKit1", "MakeThreadHighPriority")};
+    if(!m) return -ENOMEM;
+
+    auto u64 = static_cast<dbus_uint64_t>(thread);
+    auto s32 = static_cast<dbus_int32_t>(nice_level);
+    auto ready = dbus_message_append_args(m.get(),
+        dbus::TypeUInt64, &u64,
+        dbus::TypeInt32, &s32,
+        dbus::TypeInvalid);
+    if(!ready) return -ENOMEM;
+
+    dbus::Error error;
+    dbus::MessagePtr r{dbus_connection_send_with_reply_and_block(connection, m.get(), -1,
+        &error.get())};
+    if(!r) return translate_error(error->name);
+
+    if(dbus_set_error_from_message(&error.get(), r.get()))
+        return translate_error(error->name);
+
+    return 0;
+}
diff --git a/core/rtkit.h b/core/rtkit.h
new file mode 100644 (file)
index 0000000..d4994e2
--- /dev/null
@@ -0,0 +1,71 @@
+/*-*- Mode: C; c-basic-offset: 8 -*-*/
+
+#ifndef foortkithfoo
+#define foortkithfoo
+
+/***
+        Copyright 2009 Lennart Poettering
+        Copyright 2010 David Henningsson <diwic@ubuntu.com>
+
+        Permission is hereby granted, free of charge, to any person
+        obtaining a copy of this software and associated documentation files
+        (the "Software"), to deal in the Software without restriction,
+        including without limitation the rights to use, copy, modify, merge,
+        publish, distribute, sublicense, and/or sell copies of the Software,
+        and to permit persons to whom the Software is furnished to do so,
+        subject to the following conditions:
+
+        The above copyright notice and this permission notice shall be
+        included in all copies or substantial portions of the Software.
+
+        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+        EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+        MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+        NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+        BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+        ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+        CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+        SOFTWARE.
+***/
+
+#include <sys/types.h>
+
+#include "dbus_wrap.h"
+
+/* This is the reference implementation for a client for
+ * RealtimeKit. You don't have to use this, but if do, just copy these
+ * sources into your repository */
+
+#define RTKIT_SERVICE_NAME "org.freedesktop.RealtimeKit1"
+#define RTKIT_OBJECT_PATH "/org/freedesktop/RealtimeKit1"
+
+/* This is mostly equivalent to sched_setparam(thread, SCHED_RR, {
+ * .sched_priority = priority }). 'thread' needs to be a kernel thread
+ * id as returned by gettid(), not a pthread_t! If 'thread' is 0 the
+ * current thread is used. The returned value is a negative errno
+ * style error code, or 0 on success. */
+int rtkit_make_realtime(DBusConnection *system_bus, pid_t thread, int priority);
+
+/* This is mostly equivalent to setpriority(PRIO_PROCESS, thread,
+ * nice_level). 'thread' needs to be a kernel thread id as returned by
+ * gettid(), not a pthread_t! If 'thread' is 0 the current thread is
+ * used. The returned value is a negative errno style error code, or 0
+ * on success.*/
+int rtkit_make_high_priority(DBusConnection *system_bus, pid_t thread, int nice_level);
+
+/* Return the maximum value of realtime priority available. Realtime requests
+ * above this value will fail. A negative value is an errno style error code.
+ */
+int rtkit_get_max_realtime_priority(DBusConnection *system_bus);
+
+/* Retreive the minimum value of nice level available. High prio requests
+ * below this value will fail. The returned value is a negative errno
+ * style error code, or 0 on success.*/
+int rtkit_get_min_nice_level(DBusConnection *system_bus, int *min_nice_level);
+
+/* Return the maximum value of RLIMIT_RTTIME to set before attempting a
+ * realtime request. A negative value is an errno style error code.
+ */
+long long rtkit_get_rttime_usec_max(DBusConnection *system_bus);
+
+#endif
diff --git a/core/uhjfilter.cpp b/core/uhjfilter.cpp
new file mode 100644 (file)
index 0000000..df50956
--- /dev/null
@@ -0,0 +1,539 @@
+
+#include "config.h"
+
+#include "uhjfilter.h"
+
+#include <algorithm>
+#include <iterator>
+
+#include "alcomplex.h"
+#include "alnumeric.h"
+#include "opthelpers.h"
+#include "phase_shifter.h"
+
+
+UhjQualityType UhjDecodeQuality{UhjQualityType::Default};
+UhjQualityType UhjEncodeQuality{UhjQualityType::Default};
+
+
+namespace {
+
+const PhaseShifterT<UhjLength256> PShiftLq{};
+const PhaseShifterT<UhjLength512> PShiftHq{};
+
+template<size_t N>
+struct GetPhaseShifter;
+template<>
+struct GetPhaseShifter<UhjLength256> { static auto& Get() noexcept { return PShiftLq; } };
+template<>
+struct GetPhaseShifter<UhjLength512> { static auto& Get() noexcept { return PShiftHq; } };
+
+
+constexpr float square(float x) noexcept
+{ return x*x; }
+
+/* Filter coefficients for the 'base' all-pass IIR, which applies a frequency-
+ * dependent phase-shift of N degrees. The output of the filter requires a 1-
+ * sample delay.
+ */
+constexpr std::array<float,4> Filter1Coeff{{
+    square(0.6923878f), square(0.9360654322959f), square(0.9882295226860f),
+    square(0.9987488452737f)
+}};
+/* Filter coefficients for the offset all-pass IIR, which applies a frequency-
+ * dependent phase-shift of N+90 degrees.
+ */
+constexpr std::array<float,4> Filter2Coeff{{
+    square(0.4021921162426f), square(0.8561710882420f), square(0.9722909545651f),
+    square(0.9952884791278f)
+}};
+
+} // namespace
+
+void UhjAllPassFilter::process(const al::span<const float,4> coeffs,
+    const al::span<const float> src, const bool updateState, float *RESTRICT dst)
+{
+    auto state = mState;
+
+    auto proc_sample = [&state,coeffs](float x) noexcept -> float
+    {
+        for(size_t i{0};i < 4;++i)
+        {
+            const float y{x*coeffs[i] + state[i].z[0]};
+            state[i].z[0] = state[i].z[1];
+            state[i].z[1] = y*coeffs[i] - x;
+            x = y;
+        }
+        return x;
+    };
+    std::transform(src.begin(), src.end(), dst, proc_sample);
+    if(updateState) LIKELY mState = state;
+}
+
+
+/* Encoding UHJ from B-Format is done as:
+ *
+ * S = 0.9396926*W + 0.1855740*X
+ * D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y
+ *
+ * Left = (S + D)/2.0
+ * Right = (S - D)/2.0
+ * T = j(-0.1432*W + 0.6512*X) - 0.7071068*Y
+ * Q = 0.9772*Z
+ *
+ * where j is a wide-band +90 degree phase shift. 3-channel UHJ excludes Q,
+ * while 2-channel excludes Q and T.
+ *
+ * The phase shift is done using a linear FIR filter derived from an FFT'd
+ * impulse with the desired shift.
+ */
+
+template<size_t N>
+void UhjEncoder<N>::encode(float *LeftOut, float *RightOut,
+    const al::span<const float*const,3> InSamples, const size_t SamplesToDo)
+{
+    const auto &PShift = GetPhaseShifter<N>::Get();
+
+    ASSUME(SamplesToDo > 0);
+
+    const float *RESTRICT winput{al::assume_aligned<16>(InSamples[0])};
+    const float *RESTRICT xinput{al::assume_aligned<16>(InSamples[1])};
+    const float *RESTRICT yinput{al::assume_aligned<16>(InSamples[2])};
+
+    std::copy_n(winput, SamplesToDo, mW.begin()+sFilterDelay);
+    std::copy_n(xinput, SamplesToDo, mX.begin()+sFilterDelay);
+    std::copy_n(yinput, SamplesToDo, mY.begin()+sFilterDelay);
+
+    /* S = 0.9396926*W + 0.1855740*X */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mS[i] = 0.9396926f*mW[i] + 0.1855740f*mX[i];
+
+    /* Precompute j(-0.3420201*W + 0.5098604*X) and store in mD. */
+    std::transform(winput, winput+SamplesToDo, xinput, mWX.begin() + sWXInOffset,
+        [](const float w, const float x) noexcept -> float
+        { return -0.3420201f*w + 0.5098604f*x; });
+    PShift.process({mD.data(), SamplesToDo}, mWX.data());
+
+    /* D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mD[i] = mD[i] + 0.6554516f*mY[i];
+
+    /* Copy the future samples to the front for next time. */
+    std::copy(mW.cbegin()+SamplesToDo, mW.cbegin()+SamplesToDo+sFilterDelay, mW.begin());
+    std::copy(mX.cbegin()+SamplesToDo, mX.cbegin()+SamplesToDo+sFilterDelay, mX.begin());
+    std::copy(mY.cbegin()+SamplesToDo, mY.cbegin()+SamplesToDo+sFilterDelay, mY.begin());
+    std::copy(mWX.cbegin()+SamplesToDo, mWX.cbegin()+SamplesToDo+sWXInOffset, mWX.begin());
+
+    /* Apply a delay to the existing output to align with the input delay. */
+    auto *delayBuffer = mDirectDelay.data();
+    for(float *buffer : {LeftOut, RightOut})
+    {
+        float *distbuf{al::assume_aligned<16>(delayBuffer->data())};
+        ++delayBuffer;
+
+        float *inout{al::assume_aligned<16>(buffer)};
+        auto inout_end = inout + SamplesToDo;
+        if(SamplesToDo >= sFilterDelay) LIKELY
+        {
+            auto delay_end = std::rotate(inout, inout_end - sFilterDelay, inout_end);
+            std::swap_ranges(inout, delay_end, distbuf);
+        }
+        else
+        {
+            auto delay_start = std::swap_ranges(inout, inout_end, distbuf);
+            std::rotate(distbuf, delay_start, distbuf + sFilterDelay);
+        }
+    }
+
+    /* Combine the direct signal with the produced output. */
+
+    /* Left = (S + D)/2.0 */
+    float *RESTRICT left{al::assume_aligned<16>(LeftOut)};
+    for(size_t i{0};i < SamplesToDo;i++)
+        left[i] += (mS[i] + mD[i]) * 0.5f;
+    /* Right = (S - D)/2.0 */
+    float *RESTRICT right{al::assume_aligned<16>(RightOut)};
+    for(size_t i{0};i < SamplesToDo;i++)
+        right[i] += (mS[i] - mD[i]) * 0.5f;
+}
+
+/* This encoding implementation uses two sets of four chained IIR filters to
+ * produce the desired relative phase shift. The first filter chain produces a
+ * phase shift of varying degrees over a wide range of frequencies, while the
+ * second filter chain produces a phase shift 90 degrees ahead of the first
+ * over the same range. Further details are described here:
+ *
+ * https://web.archive.org/web/20060708031958/http://www.biochem.oulu.fi/~oniemita/dsp/hilbert/
+ *
+ * 2-channel UHJ output requires the use of three filter chains. The S channel
+ * output uses a Filter1 chain on the W and X channel mix, while the D channel
+ * output uses a Filter1 chain on the Y channel plus a Filter2 chain on the W
+ * and X channel mix. This results in the W and X input mix on the D channel
+ * output having the required +90 degree phase shift relative to the other
+ * inputs.
+ */
+void UhjEncoderIIR::encode(float *LeftOut, float *RightOut,
+    const al::span<const float *const, 3> InSamples, const size_t SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    const float *RESTRICT winput{al::assume_aligned<16>(InSamples[0])};
+    const float *RESTRICT xinput{al::assume_aligned<16>(InSamples[1])};
+    const float *RESTRICT yinput{al::assume_aligned<16>(InSamples[2])};
+
+    /* S = 0.9396926*W + 0.1855740*X */
+    std::transform(winput, winput+SamplesToDo, xinput, mTemp.begin(),
+        [](const float w, const float x) noexcept { return 0.9396926f*w + 0.1855740f*x; });
+    mFilter1WX.process(Filter1Coeff, {mTemp.data(), SamplesToDo}, true, mS.data()+1);
+    mS[0] = mDelayWX; mDelayWX = mS[SamplesToDo];
+
+    /* Precompute j(-0.3420201*W + 0.5098604*X) and store in mWX. */
+    std::transform(winput, winput+SamplesToDo, xinput, mTemp.begin(),
+        [](const float w, const float x) noexcept { return -0.3420201f*w + 0.5098604f*x; });
+    mFilter2WX.process(Filter2Coeff, {mTemp.data(), SamplesToDo}, true, mWX.data());
+
+    /* Apply filter1 to Y and store in mD. */
+    mFilter1Y.process(Filter1Coeff, {yinput, SamplesToDo}, SamplesToDo, mD.data()+1);
+    mD[0] = mDelayY; mDelayY = mD[SamplesToDo];
+
+    /* D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mD[i] = mWX[i] + 0.6554516f*mD[i];
+
+    /* Apply the base filter to the existing output to align with the processed
+     * signal.
+     */
+    mFilter1Direct[0].process(Filter1Coeff, {LeftOut, SamplesToDo}, true, mTemp.data()+1);
+    mTemp[0] = mDirectDelay[0]; mDirectDelay[0] = mTemp[SamplesToDo];
+
+    /* Left = (S + D)/2.0 */
+    float *RESTRICT left{al::assume_aligned<16>(LeftOut)};
+    for(size_t i{0};i < SamplesToDo;i++)
+        left[i] = (mS[i] + mD[i])*0.5f + mTemp[i];
+
+    mFilter1Direct[1].process(Filter1Coeff, {RightOut, SamplesToDo}, true, mTemp.data()+1);
+    mTemp[0] = mDirectDelay[1]; mDirectDelay[1] = mTemp[SamplesToDo];
+
+    /* Right = (S - D)/2.0 */
+    float *RESTRICT right{al::assume_aligned<16>(RightOut)};
+    for(size_t i{0};i < SamplesToDo;i++)
+        right[i] = (mS[i] - mD[i])*0.5f + mTemp[i];
+}
+
+
+/* Decoding UHJ is done as:
+ *
+ * S = Left + Right
+ * D = Left - Right
+ *
+ * W = 0.981532*S + 0.197484*j(0.828331*D + 0.767820*T)
+ * X = 0.418496*S - j(0.828331*D + 0.767820*T)
+ * Y = 0.795968*D - 0.676392*T + j(0.186633*S)
+ * Z = 1.023332*Q
+ *
+ * where j is a +90 degree phase shift. 3-channel UHJ excludes Q, while 2-
+ * channel excludes Q and T.
+ */
+template<size_t N>
+void UhjDecoder<N>::decode(const al::span<float*> samples, const size_t samplesToDo,
+    const bool updateState)
+{
+    static_assert(sInputPadding <= sMaxPadding, "Filter padding is too large");
+
+    const auto &PShift = GetPhaseShifter<N>::Get();
+
+    ASSUME(samplesToDo > 0);
+
+    {
+        const float *RESTRICT left{al::assume_aligned<16>(samples[0])};
+        const float *RESTRICT right{al::assume_aligned<16>(samples[1])};
+        const float *RESTRICT t{al::assume_aligned<16>(samples[2])};
+
+        /* S = Left + Right */
+        for(size_t i{0};i < samplesToDo+sInputPadding;++i)
+            mS[i] = left[i] + right[i];
+
+        /* D = Left - Right */
+        for(size_t i{0};i < samplesToDo+sInputPadding;++i)
+            mD[i] = left[i] - right[i];
+
+        /* T */
+        for(size_t i{0};i < samplesToDo+sInputPadding;++i)
+            mT[i] = t[i];
+    }
+
+    float *RESTRICT woutput{al::assume_aligned<16>(samples[0])};
+    float *RESTRICT xoutput{al::assume_aligned<16>(samples[1])};
+    float *RESTRICT youtput{al::assume_aligned<16>(samples[2])};
+
+    /* Precompute j(0.828331*D + 0.767820*T) and store in xoutput. */
+    auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin());
+    std::transform(mD.cbegin(), mD.cbegin()+samplesToDo+sInputPadding, mT.cbegin(), tmpiter,
+        [](const float d, const float t) noexcept { return 0.828331f*d + 0.767820f*t; });
+    if(updateState) LIKELY
+        std::copy_n(mTemp.cbegin()+samplesToDo, mDTHistory.size(), mDTHistory.begin());
+    PShift.process({xoutput, samplesToDo}, mTemp.data());
+
+    /* W = 0.981532*S + 0.197484*j(0.828331*D + 0.767820*T) */
+    for(size_t i{0};i < samplesToDo;++i)
+        woutput[i] = 0.981532f*mS[i] + 0.197484f*xoutput[i];
+    /* X = 0.418496*S - j(0.828331*D + 0.767820*T) */
+    for(size_t i{0};i < samplesToDo;++i)
+        xoutput[i] = 0.418496f*mS[i] - xoutput[i];
+
+    /* Precompute j*S and store in youtput. */
+    tmpiter = std::copy(mSHistory.cbegin(), mSHistory.cend(), mTemp.begin());
+    std::copy_n(mS.cbegin(), samplesToDo+sInputPadding, tmpiter);
+    if(updateState) LIKELY
+        std::copy_n(mTemp.cbegin()+samplesToDo, mSHistory.size(), mSHistory.begin());
+    PShift.process({youtput, samplesToDo}, mTemp.data());
+
+    /* Y = 0.795968*D - 0.676392*T + j(0.186633*S) */
+    for(size_t i{0};i < samplesToDo;++i)
+        youtput[i] = 0.795968f*mD[i] - 0.676392f*mT[i] + 0.186633f*youtput[i];
+
+    if(samples.size() > 3)
+    {
+        float *RESTRICT zoutput{al::assume_aligned<16>(samples[3])};
+        /* Z = 1.023332*Q */
+        for(size_t i{0};i < samplesToDo;++i)
+            zoutput[i] = 1.023332f*zoutput[i];
+    }
+}
+
+void UhjDecoderIIR::decode(const al::span<float*> samples, const size_t samplesToDo,
+    const bool updateState)
+{
+    static_assert(sInputPadding <= sMaxPadding, "Filter padding is too large");
+
+    ASSUME(samplesToDo > 0);
+
+    {
+        const float *RESTRICT left{al::assume_aligned<16>(samples[0])};
+        const float *RESTRICT right{al::assume_aligned<16>(samples[1])};
+
+        /* S = Left + Right */
+        for(size_t i{0};i < samplesToDo;++i)
+            mS[i] = left[i] + right[i];
+
+        /* D = Left - Right */
+        for(size_t i{0};i < samplesToDo;++i)
+            mD[i] = left[i] - right[i];
+    }
+
+    float *RESTRICT woutput{al::assume_aligned<16>(samples[0])};
+    float *RESTRICT xoutput{al::assume_aligned<16>(samples[1])};
+    float *RESTRICT youtput{al::assume_aligned<16>(samples[2])};
+
+    /* Precompute j(0.828331*D + 0.767820*T) and store in xoutput. */
+    std::transform(mD.cbegin(), mD.cbegin()+samplesToDo, youtput, mTemp.begin(),
+        [](const float d, const float t) noexcept { return 0.828331f*d + 0.767820f*t; });
+    mFilter2DT.process(Filter2Coeff, {mTemp.data(), samplesToDo}, updateState, xoutput);
+
+    /* Apply filter1 to S and store in mTemp. */
+    mTemp[0] = mDelayS;
+    mFilter1S.process(Filter1Coeff, {mS.data(), samplesToDo}, updateState, mTemp.data()+1);
+    if(updateState) LIKELY mDelayS = mTemp[samplesToDo];
+
+    /* W = 0.981532*S + 0.197484*j(0.828331*D + 0.767820*T) */
+    for(size_t i{0};i < samplesToDo;++i)
+        woutput[i] = 0.981532f*mTemp[i] + 0.197484f*xoutput[i];
+    /* X = 0.418496*S - j(0.828331*D + 0.767820*T) */
+    for(size_t i{0};i < samplesToDo;++i)
+        xoutput[i] = 0.418496f*mTemp[i] - xoutput[i];
+
+
+    /* Apply filter1 to (0.795968*D - 0.676392*T) and store in mTemp. */
+    std::transform(mD.cbegin(), mD.cbegin()+samplesToDo, youtput, youtput,
+        [](const float d, const float t) noexcept { return 0.795968f*d - 0.676392f*t; });
+    mTemp[0] = mDelayDT;
+    mFilter1DT.process(Filter1Coeff, {youtput, samplesToDo}, updateState, mTemp.data()+1);
+    if(updateState) LIKELY mDelayDT = mTemp[samplesToDo];
+
+    /* Precompute j*S and store in youtput. */
+    mFilter2S.process(Filter2Coeff, {mS.data(), samplesToDo}, updateState, youtput);
+
+    /* Y = 0.795968*D - 0.676392*T + j(0.186633*S) */
+    for(size_t i{0};i < samplesToDo;++i)
+        youtput[i] = mTemp[i] + 0.186633f*youtput[i];
+
+
+    if(samples.size() > 3)
+    {
+        float *RESTRICT zoutput{al::assume_aligned<16>(samples[3])};
+
+        /* Apply filter1 to Q and store in mTemp. */
+        mTemp[0] = mDelayQ;
+        mFilter1Q.process(Filter1Coeff, {zoutput, samplesToDo}, updateState, mTemp.data()+1);
+        if(updateState) LIKELY mDelayQ = mTemp[samplesToDo];
+
+        /* Z = 1.023332*Q */
+        for(size_t i{0};i < samplesToDo;++i)
+            zoutput[i] = 1.023332f*mTemp[i];
+    }
+}
+
+
+/* Super Stereo processing is done as:
+ *
+ * S = Left + Right
+ * D = Left - Right
+ *
+ * W = 0.6098637*S - 0.6896511*j*w*D
+ * X = 0.8624776*S + 0.7626955*j*w*D
+ * Y = 1.6822415*w*D - 0.2156194*j*S
+ *
+ * where j is a +90 degree phase shift. w is a variable control for the
+ * resulting stereo width, with the range 0 <= w <= 0.7.
+ */
+template<size_t N>
+void UhjStereoDecoder<N>::decode(const al::span<float*> samples, const size_t samplesToDo,
+    const bool updateState)
+{
+    static_assert(sInputPadding <= sMaxPadding, "Filter padding is too large");
+
+    const auto &PShift = GetPhaseShifter<N>::Get();
+
+    ASSUME(samplesToDo > 0);
+
+    {
+        const float *RESTRICT left{al::assume_aligned<16>(samples[0])};
+        const float *RESTRICT right{al::assume_aligned<16>(samples[1])};
+
+        for(size_t i{0};i < samplesToDo+sInputPadding;++i)
+            mS[i] = left[i] + right[i];
+
+        /* Pre-apply the width factor to the difference signal D. Smoothly
+         * interpolate when it changes.
+         */
+        const float wtarget{mWidthControl};
+        const float wcurrent{(mCurrentWidth < 0.0f) ? wtarget : mCurrentWidth};
+        if(wtarget == wcurrent || !updateState)
+        {
+            for(size_t i{0};i < samplesToDo+sInputPadding;++i)
+                mD[i] = (left[i] - right[i]) * wcurrent;
+            mCurrentWidth = wcurrent;
+        }
+        else
+        {
+            const float wstep{(wtarget - wcurrent) / static_cast<float>(samplesToDo)};
+            float fi{0.0f};
+            for(size_t i{0};i < samplesToDo;++i)
+            {
+                mD[i] = (left[i] - right[i]) * (wcurrent + wstep*fi);
+                fi += 1.0f;
+            }
+            for(size_t i{samplesToDo};i < samplesToDo+sInputPadding;++i)
+                mD[i] = (left[i] - right[i]) * wtarget;
+            mCurrentWidth = wtarget;
+        }
+    }
+
+    float *RESTRICT woutput{al::assume_aligned<16>(samples[0])};
+    float *RESTRICT xoutput{al::assume_aligned<16>(samples[1])};
+    float *RESTRICT youtput{al::assume_aligned<16>(samples[2])};
+
+    /* Precompute j*D and store in xoutput. */
+    auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin());
+    std::copy_n(mD.cbegin(), samplesToDo+sInputPadding, tmpiter);
+    if(updateState) LIKELY
+        std::copy_n(mTemp.cbegin()+samplesToDo, mDTHistory.size(), mDTHistory.begin());
+    PShift.process({xoutput, samplesToDo}, mTemp.data());
+
+    /* W = 0.6098637*S - 0.6896511*j*w*D */
+    for(size_t i{0};i < samplesToDo;++i)
+        woutput[i] = 0.6098637f*mS[i] - 0.6896511f*xoutput[i];
+    /* X = 0.8624776*S + 0.7626955*j*w*D */
+    for(size_t i{0};i < samplesToDo;++i)
+        xoutput[i] = 0.8624776f*mS[i] + 0.7626955f*xoutput[i];
+
+    /* Precompute j*S and store in youtput. */
+    tmpiter = std::copy(mSHistory.cbegin(), mSHistory.cend(), mTemp.begin());
+    std::copy_n(mS.cbegin(), samplesToDo+sInputPadding, tmpiter);
+    if(updateState) LIKELY
+        std::copy_n(mTemp.cbegin()+samplesToDo, mSHistory.size(), mSHistory.begin());
+    PShift.process({youtput, samplesToDo}, mTemp.data());
+
+    /* Y = 1.6822415*w*D - 0.2156194*j*S */
+    for(size_t i{0};i < samplesToDo;++i)
+        youtput[i] = 1.6822415f*mD[i] - 0.2156194f*youtput[i];
+}
+
+void UhjStereoDecoderIIR::decode(const al::span<float*> samples, const size_t samplesToDo,
+    const bool updateState)
+{
+    static_assert(sInputPadding <= sMaxPadding, "Filter padding is too large");
+
+    ASSUME(samplesToDo > 0);
+
+    {
+        const float *RESTRICT left{al::assume_aligned<16>(samples[0])};
+        const float *RESTRICT right{al::assume_aligned<16>(samples[1])};
+
+        for(size_t i{0};i < samplesToDo;++i)
+            mS[i] = left[i] + right[i];
+
+        /* Pre-apply the width factor to the difference signal D. Smoothly
+         * interpolate when it changes.
+         */
+        const float wtarget{mWidthControl};
+        const float wcurrent{(mCurrentWidth < 0.0f) ? wtarget : mCurrentWidth};
+        if(wtarget == wcurrent || !updateState)
+        {
+            for(size_t i{0};i < samplesToDo;++i)
+                mD[i] = (left[i] - right[i]) * wcurrent;
+            mCurrentWidth = wcurrent;
+        }
+        else
+        {
+            const float wstep{(wtarget - wcurrent) / static_cast<float>(samplesToDo)};
+            float fi{0.0f};
+            for(size_t i{0};i < samplesToDo;++i)
+            {
+                mD[i] = (left[i] - right[i]) * (wcurrent + wstep*fi);
+                fi += 1.0f;
+            }
+            mCurrentWidth = wtarget;
+        }
+    }
+
+    float *RESTRICT woutput{al::assume_aligned<16>(samples[0])};
+    float *RESTRICT xoutput{al::assume_aligned<16>(samples[1])};
+    float *RESTRICT youtput{al::assume_aligned<16>(samples[2])};
+
+    /* Apply filter1 to S and store in mTemp. */
+    mTemp[0] = mDelayS;
+    mFilter1S.process(Filter1Coeff, {mS.data(), samplesToDo}, updateState, mTemp.data()+1);
+    if(updateState) LIKELY mDelayS = mTemp[samplesToDo];
+
+    /* Precompute j*D and store in xoutput. */
+    mFilter2D.process(Filter2Coeff, {mD.data(), samplesToDo}, updateState, xoutput);
+
+    /* W = 0.6098637*S - 0.6896511*j*w*D */
+    for(size_t i{0};i < samplesToDo;++i)
+        woutput[i] = 0.6098637f*mTemp[i] - 0.6896511f*xoutput[i];
+    /* X = 0.8624776*S + 0.7626955*j*w*D */
+    for(size_t i{0};i < samplesToDo;++i)
+        xoutput[i] = 0.8624776f*mTemp[i] + 0.7626955f*xoutput[i];
+
+    /* Precompute j*S and store in youtput. */
+    mFilter2S.process(Filter2Coeff, {mS.data(), samplesToDo}, updateState, youtput);
+
+    /* Apply filter1 to D and store in mTemp. */
+    mTemp[0] = mDelayD;
+    mFilter1D.process(Filter1Coeff, {mD.data(), samplesToDo}, updateState, mTemp.data()+1);
+    if(updateState) LIKELY mDelayD = mTemp[samplesToDo];
+
+    /* Y = 1.6822415*w*D - 0.2156194*j*S */
+    for(size_t i{0};i < samplesToDo;++i)
+        youtput[i] = 1.6822415f*mTemp[i] - 0.2156194f*youtput[i];
+}
+
+
+template struct UhjEncoder<UhjLength256>;
+template struct UhjDecoder<UhjLength256>;
+template struct UhjStereoDecoder<UhjLength256>;
+
+template struct UhjEncoder<UhjLength512>;
+template struct UhjDecoder<UhjLength512>;
+template struct UhjStereoDecoder<UhjLength512>;
diff --git a/core/uhjfilter.h b/core/uhjfilter.h
new file mode 100644 (file)
index 0000000..df30809
--- /dev/null
@@ -0,0 +1,234 @@
+#ifndef CORE_UHJFILTER_H
+#define CORE_UHJFILTER_H
+
+#include <array>
+
+#include "almalloc.h"
+#include "alspan.h"
+#include "bufferline.h"
+
+
+static constexpr size_t UhjLength256{256};
+static constexpr size_t UhjLength512{512};
+
+enum class UhjQualityType : uint8_t {
+    IIR = 0,
+    FIR256,
+    FIR512,
+    Default = IIR
+};
+
+extern UhjQualityType UhjDecodeQuality;
+extern UhjQualityType UhjEncodeQuality;
+
+
+struct UhjAllPassFilter {
+    struct AllPassState {
+        /* Last two delayed components for direct form II. */
+        float z[2];
+    };
+    std::array<AllPassState,4> mState;
+
+    void process(const al::span<const float,4> coeffs, const al::span<const float> src,
+        const bool update, float *RESTRICT dst);
+};
+
+
+struct UhjEncoderBase {
+    virtual ~UhjEncoderBase() = default;
+
+    virtual size_t getDelay() noexcept = 0;
+
+    /**
+     * Encodes a 2-channel UHJ (stereo-compatible) signal from a B-Format input
+     * signal. The input must use FuMa channel ordering and UHJ scaling (FuMa
+     * with an additional +3dB boost).
+     */
+    virtual void encode(float *LeftOut, float *RightOut,
+        const al::span<const float*const,3> InSamples, const size_t SamplesToDo) = 0;
+};
+
+template<size_t N>
+struct UhjEncoder final : public UhjEncoderBase {
+    static constexpr size_t sFilterDelay{N/2};
+
+    /* Delays and processing storage for the input signal. */
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mW{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mX{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mY{};
+
+    alignas(16) std::array<float,BufferLineSize> mS{};
+    alignas(16) std::array<float,BufferLineSize> mD{};
+
+    /* History and temp storage for the FIR filter. New samples should be
+     * written to index sFilterDelay*2 - 1.
+     */
+    static constexpr size_t sWXInOffset{sFilterDelay*2 - 1};
+    alignas(16) std::array<float,BufferLineSize + sFilterDelay*2> mWX{};
+
+    alignas(16) std::array<std::array<float,sFilterDelay>,2> mDirectDelay{};
+
+    size_t getDelay() noexcept override { return sFilterDelay; }
+
+    /**
+     * Encodes a 2-channel UHJ (stereo-compatible) signal from a B-Format input
+     * signal. The input must use FuMa channel ordering and UHJ scaling (FuMa
+     * with an additional +3dB boost).
+     */
+    void encode(float *LeftOut, float *RightOut, const al::span<const float*const,3> InSamples,
+        const size_t SamplesToDo) override;
+
+    DEF_NEWDEL(UhjEncoder)
+};
+
+struct UhjEncoderIIR final : public UhjEncoderBase {
+    static constexpr size_t sFilterDelay{1};
+
+    /* Processing storage for the input signal. */
+    alignas(16) std::array<float,BufferLineSize+1> mS{};
+    alignas(16) std::array<float,BufferLineSize+1> mD{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mWX{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mTemp{};
+    float mDelayWX{}, mDelayY{};
+
+    UhjAllPassFilter mFilter1WX;
+    UhjAllPassFilter mFilter2WX;
+    UhjAllPassFilter mFilter1Y;
+
+    std::array<UhjAllPassFilter,2> mFilter1Direct;
+    std::array<float,2> mDirectDelay{};
+
+    size_t getDelay() noexcept override { return sFilterDelay; }
+
+    /**
+     * Encodes a 2-channel UHJ (stereo-compatible) signal from a B-Format input
+     * signal. The input must use FuMa channel ordering and UHJ scaling (FuMa
+     * with an additional +3dB boost).
+     */
+    void encode(float *LeftOut, float *RightOut, const al::span<const float*const,3> InSamples,
+        const size_t SamplesToDo) override;
+
+    DEF_NEWDEL(UhjEncoderIIR)
+};
+
+
+struct DecoderBase {
+    static constexpr size_t sMaxPadding{256};
+
+    /* For 2-channel UHJ, shelf filters should use these LF responses. */
+    static constexpr float sWLFScale{0.661f};
+    static constexpr float sXYLFScale{1.293f};
+
+    virtual ~DecoderBase() = default;
+
+    virtual void decode(const al::span<float*> samples, const size_t samplesToDo,
+        const bool updateState) = 0;
+
+    /**
+     * The width factor for Super Stereo processing. Can be changed in between
+     * calls to decode, with valid values being between 0...0.7.
+     */
+    float mWidthControl{0.593f};
+};
+
+template<size_t N>
+struct UhjDecoder final : public DecoderBase {
+    /* The number of extra sample frames needed for input. */
+    static constexpr size_t sInputPadding{N/2};
+
+    alignas(16) std::array<float,BufferLineSize+sInputPadding> mS{};
+    alignas(16) std::array<float,BufferLineSize+sInputPadding> mD{};
+    alignas(16) std::array<float,BufferLineSize+sInputPadding> mT{};
+
+    alignas(16) std::array<float,sInputPadding-1> mDTHistory{};
+    alignas(16) std::array<float,sInputPadding-1> mSHistory{};
+
+    alignas(16) std::array<float,BufferLineSize + sInputPadding*2> mTemp{};
+
+    /**
+     * Decodes a 3- or 4-channel UHJ signal into a B-Format signal with FuMa
+     * channel ordering and UHJ scaling. For 3-channel, the 3rd channel may be
+     * attenuated by 'n', where 0 <= n <= 1. So to decode 2-channel UHJ, supply
+     * 3 channels with the 3rd channel silent (n=0). The B-Format signal
+     * reconstructed from 2-channel UHJ should not be run through a normal
+     * B-Format decoder, as it needs different shelf filters.
+     */
+    void decode(const al::span<float*> samples, const size_t samplesToDo,
+        const bool updateState) override;
+
+    DEF_NEWDEL(UhjDecoder)
+};
+
+struct UhjDecoderIIR final : public DecoderBase {
+    /* FIXME: These IIR decoder filters actually have a 1-sample delay on the
+     * non-filtered components, which is not reflected in the source latency
+     * value. sInputPadding is 0, however, because it doesn't need any extra
+     * input samples.
+     */
+    static constexpr size_t sInputPadding{0};
+
+    alignas(16) std::array<float,BufferLineSize> mS{};
+    alignas(16) std::array<float,BufferLineSize> mD{};
+    alignas(16) std::array<float,BufferLineSize+1> mTemp{};
+    float mDelayS{}, mDelayDT{}, mDelayQ{};
+
+    UhjAllPassFilter mFilter1S;
+    UhjAllPassFilter mFilter2DT;
+    UhjAllPassFilter mFilter1DT;
+    UhjAllPassFilter mFilter2S;
+    UhjAllPassFilter mFilter1Q;
+
+    void decode(const al::span<float*> samples, const size_t samplesToDo,
+        const bool updateState) override;
+
+    DEF_NEWDEL(UhjDecoderIIR)
+};
+
+template<size_t N>
+struct UhjStereoDecoder final : public DecoderBase {
+    static constexpr size_t sInputPadding{N/2};
+
+    float mCurrentWidth{-1.0f};
+
+    alignas(16) std::array<float,BufferLineSize+sInputPadding> mS{};
+    alignas(16) std::array<float,BufferLineSize+sInputPadding> mD{};
+
+    alignas(16) std::array<float,sInputPadding-1> mDTHistory{};
+    alignas(16) std::array<float,sInputPadding-1> mSHistory{};
+
+    alignas(16) std::array<float,BufferLineSize + sInputPadding*2> mTemp{};
+
+    /**
+     * Applies Super Stereo processing on a stereo signal to create a B-Format
+     * signal with FuMa channel ordering and UHJ scaling. The samples span
+     * should contain 3 channels, the first two being the left and right stereo
+     * channels, and the third left empty.
+     */
+    void decode(const al::span<float*> samples, const size_t samplesToDo,
+        const bool updateState) override;
+
+    DEF_NEWDEL(UhjStereoDecoder)
+};
+
+struct UhjStereoDecoderIIR final : public DecoderBase {
+    static constexpr size_t sInputPadding{0};
+
+    float mCurrentWidth{-1.0f};
+
+    alignas(16) std::array<float,BufferLineSize> mS{};
+    alignas(16) std::array<float,BufferLineSize> mD{};
+    alignas(16) std::array<float,BufferLineSize+1> mTemp{};
+    float mDelayS{}, mDelayD{};
+
+    UhjAllPassFilter mFilter1S;
+    UhjAllPassFilter mFilter2D;
+    UhjAllPassFilter mFilter1D;
+    UhjAllPassFilter mFilter2S;
+
+    void decode(const al::span<float*> samples, const size_t samplesToDo,
+        const bool updateState) override;
+
+    DEF_NEWDEL(UhjStereoDecoderIIR)
+};
+
+#endif /* CORE_UHJFILTER_H */
diff --git a/core/uiddefs.cpp b/core/uiddefs.cpp
new file mode 100644 (file)
index 0000000..244c01a
--- /dev/null
@@ -0,0 +1,37 @@
+
+#include "config.h"
+
+
+#ifndef AL_NO_UID_DEFS
+
+#if defined(HAVE_GUIDDEF_H) || defined(HAVE_INITGUID_H)
+#define INITGUID
+#include <windows.h>
+#ifdef HAVE_GUIDDEF_H
+#include <guiddef.h>
+#else
+#include <initguid.h>
+#endif
+
+DEFINE_GUID(KSDATAFORMAT_SUBTYPE_PCM,        0x00000001, 0x0000, 0x0010, 0x80,0x00, 0x00,0xaa,0x00,0x38,0x9b,0x71);
+DEFINE_GUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x00000003, 0x0000, 0x0010, 0x80,0x00, 0x00,0xaa,0x00,0x38,0x9b,0x71);
+
+DEFINE_GUID(IID_IDirectSoundNotify,   0xb0210783, 0x89cd, 0x11d0, 0xaf,0x08, 0x00,0xa0,0xc9,0x25,0xcd,0x16);
+
+DEFINE_GUID(CLSID_MMDeviceEnumerator, 0xbcde0395, 0xe52f, 0x467c, 0x8e,0x3d, 0xc4,0x57,0x92,0x91,0x69,0x2e);
+DEFINE_GUID(IID_IMMDeviceEnumerator,  0xa95664d2, 0x9614, 0x4f35, 0xa7,0x46, 0xde,0x8d,0xb6,0x36,0x17,0xe6);
+DEFINE_GUID(IID_IAudioClient,         0x1cb9ad4c, 0xdbfa, 0x4c32, 0xb1,0x78, 0xc2,0xf5,0x68,0xa7,0x03,0xb2);
+DEFINE_GUID(IID_IAudioRenderClient,   0xf294acfc, 0x3146, 0x4483, 0xa7,0xbf, 0xad,0xdc,0xa7,0xc2,0x60,0xe2);
+DEFINE_GUID(IID_IAudioCaptureClient,  0xc8adbd64, 0xe71e, 0x48a0, 0xa4,0xde, 0x18,0x5c,0x39,0x5c,0xd3,0x17);
+
+#ifdef HAVE_WASAPI
+#include <wtypes.h>
+#include <devpropdef.h>
+#include <propkeydef.h>
+DEFINE_DEVPROPKEY(DEVPKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80,0x20, 0x67,0xd1,0x46,0xa8,0x50,0xe0, 14);
+DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_FormFactor, 0x1da5d803, 0xd492, 0x4edd, 0x8c,0x23, 0xe0,0xc0,0xff,0xee,0x7f,0x0e, 0);
+DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_GUID, 0x1da5d803, 0xd492, 0x4edd, 0x8c, 0x23,0xe0, 0xc0,0xff,0xee,0x7f,0x0e, 4 );
+#endif
+#endif
+
+#endif /* AL_NO_UID_DEFS */
diff --git a/core/voice.cpp b/core/voice.cpp
new file mode 100644 (file)
index 0000000..e8fbccc
--- /dev/null
@@ -0,0 +1,1304 @@
+
+#include "config.h"
+
+#include "voice.h"
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cassert>
+#include <climits>
+#include <cstdint>
+#include <iterator>
+#include <memory>
+#include <new>
+#include <stdlib.h>
+#include <utility>
+#include <vector>
+
+#include "albyte.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "ambidefs.h"
+#include "async_event.h"
+#include "buffer_storage.h"
+#include "context.h"
+#include "cpu_caps.h"
+#include "devformat.h"
+#include "device.h"
+#include "filters/biquad.h"
+#include "filters/nfc.h"
+#include "filters/splitter.h"
+#include "fmt_traits.h"
+#include "logging.h"
+#include "mixer.h"
+#include "mixer/defs.h"
+#include "mixer/hrtfdefs.h"
+#include "opthelpers.h"
+#include "resampler_limits.h"
+#include "ringbuffer.h"
+#include "vector.h"
+#include "voice_change.h"
+
+struct CTag;
+#ifdef HAVE_SSE
+struct SSETag;
+#endif
+#ifdef HAVE_NEON
+struct NEONTag;
+#endif
+
+
+static_assert(!(sizeof(DeviceBase::MixerBufferLine)&15),
+    "DeviceBase::MixerBufferLine must be a multiple of 16 bytes");
+static_assert(!(MaxResamplerEdge&3), "MaxResamplerEdge is not a multiple of 4");
+
+static_assert((BufferLineSize-1)/MaxPitch > 0, "MaxPitch is too large for BufferLineSize!");
+static_assert((INT_MAX>>MixerFracBits)/MaxPitch > BufferLineSize,
+    "MaxPitch and/or BufferLineSize are too large for MixerFracBits!");
+
+Resampler ResamplerDefault{Resampler::Cubic};
+
+namespace {
+
+using uint = unsigned int;
+using namespace std::chrono;
+
+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 MixerOutFunc 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 MixerOneFunc SelectMixerOne()
+{
+#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 Voice::InitMixer(al::optional<std::string> resampler)
+{
+    if(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{resampler->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;
+    }
+
+    MixSamplesOut = SelectMixer();
+    MixSamplesOne = SelectMixerOne();
+    MixHrtfBlendSamples = SelectHrtfBlendMixer();
+    MixHrtfSamples = SelectHrtfMixer();
+}
+
+
+namespace {
+
+/* IMA ADPCM Stepsize table */
+constexpr int IMAStep_size[89] = {
+       7,    8,    9,   10,   11,   12,   13,   14,   16,   17,   19,
+      21,   23,   25,   28,   31,   34,   37,   41,   45,   50,   55,
+      60,   66,   73,   80,   88,   97,  107,  118,  130,  143,  157,
+     173,  190,  209,  230,  253,  279,  307,  337,  371,  408,  449,
+     494,  544,  598,  658,  724,  796,  876,  963, 1060, 1166, 1282,
+    1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660,
+    4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493,10442,
+   11487,12635,13899,15289,16818,18500,20350,22358,24633,27086,29794,
+   32767
+};
+
+/* IMA4 ADPCM Codeword decode table */
+constexpr int IMA4Codeword[16] = {
+    1, 3, 5, 7, 9, 11, 13, 15,
+   -1,-3,-5,-7,-9,-11,-13,-15,
+};
+
+/* IMA4 ADPCM Step index adjust decode table */
+constexpr int IMA4Index_adjust[16] = {
+   -1,-1,-1,-1, 2, 4, 6, 8,
+   -1,-1,-1,-1, 2, 4, 6, 8
+};
+
+/* MSADPCM Adaption table */
+constexpr int MSADPCMAdaption[16] = {
+    230, 230, 230, 230, 307, 409, 512, 614,
+    768, 614, 512, 409, 307, 230, 230, 230
+};
+
+/* MSADPCM Adaption Coefficient tables */
+constexpr int MSADPCMAdaptionCoeff[7][2] = {
+    { 256,    0 },
+    { 512, -256 },
+    {   0,    0 },
+    { 192,   64 },
+    { 240,    0 },
+    { 460, -208 },
+    { 392, -232 }
+};
+
+
+void SendSourceStoppedEvent(ContextBase *context, uint id)
+{
+    RingBuffer *ring{context->mAsyncEvents.get()};
+    auto evt_vec = ring->getWriteVector();
+    if(evt_vec.first.len < 1) return;
+
+    AsyncEvent *evt{al::construct_at(reinterpret_cast<AsyncEvent*>(evt_vec.first.buf),
+        AsyncEvent::SourceStateChange)};
+    evt->u.srcstate.id = id;
+    evt->u.srcstate.state = AsyncEvent::SrcState::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();
+}
+
+
+template<FmtType Type>
+inline void LoadSamples(float *RESTRICT dstSamples, const al::byte *src, const size_t srcChan,
+    const size_t srcOffset, const size_t srcStep, const size_t /*samplesPerBlock*/,
+    const size_t samplesToLoad) noexcept
+{
+    constexpr size_t sampleSize{sizeof(typename al::FmtTypeTraits<Type>::Type)};
+    auto s = src + (srcOffset*srcStep + srcChan)*sampleSize;
+
+    al::LoadSampleArray<Type>(dstSamples, s, srcStep, samplesToLoad);
+}
+
+template<>
+inline void LoadSamples<FmtIMA4>(float *RESTRICT dstSamples, const al::byte *src,
+    const size_t srcChan, const size_t srcOffset, const size_t srcStep,
+    const size_t samplesPerBlock, const size_t samplesToLoad) noexcept
+{
+    const size_t blockBytes{((samplesPerBlock-1)/2 + 4)*srcStep};
+
+    /* Skip to the ADPCM block containing the srcOffset sample. */
+    src += srcOffset/samplesPerBlock*blockBytes;
+    /* Calculate how many samples need to be skipped in the block. */
+    size_t skip{srcOffset % samplesPerBlock};
+
+    /* NOTE: This could probably be optimized better. */
+    size_t wrote{0};
+    do {
+        /* Each IMA4 block starts with a signed 16-bit sample, and a signed
+         * 16-bit table index. The table index needs to be clamped.
+         */
+        int sample{src[srcChan*4] | (src[srcChan*4 + 1] << 8)};
+        int index{src[srcChan*4 + 2] | (src[srcChan*4 + 3] << 8)};
+
+        sample = (sample^0x8000) - 32768;
+        index = clampi((index^0x8000) - 32768, 0, al::size(IMAStep_size)-1);
+
+        if(skip == 0)
+        {
+            dstSamples[wrote++] = static_cast<float>(sample) / 32768.0f;
+            if(wrote == samplesToLoad) return;
+        }
+        else
+            --skip;
+
+        auto decode_sample = [&sample,&index](const uint nibble)
+        {
+            sample += IMA4Codeword[nibble] * IMAStep_size[index] / 8;
+            sample = clampi(sample, -32768, 32767);
+
+            index += IMA4Index_adjust[nibble];
+            index = clampi(index, 0, al::size(IMAStep_size)-1);
+
+            return sample;
+        };
+
+        /* The rest of the block is arranged as a series of nibbles, contained
+         * in 4 *bytes* per channel interleaved. So every 8 nibbles we need to
+         * skip 4 bytes per channel to get the next nibbles for this channel.
+         *
+         * First, decode the samples that we need to skip in the block (will
+         * always be less than the block size). They need to be decoded despite
+         * being ignored for proper state on the remaining samples.
+         */
+        const al::byte *nibbleData{src + (srcStep+srcChan)*4};
+        size_t nibbleOffset{0};
+        const size_t startOffset{skip + 1};
+        for(;skip;--skip)
+        {
+            const size_t byteShift{(nibbleOffset&1) * 4};
+            const size_t wordOffset{(nibbleOffset>>1) & ~size_t{3}};
+            const size_t byteOffset{wordOffset*srcStep + ((nibbleOffset>>1)&3u)};
+            ++nibbleOffset;
+
+            std::ignore = decode_sample((nibbleData[byteOffset]>>byteShift) & 15u);
+        }
+
+        /* Second, decode the rest of the block and write to the output, until
+         * the end of the block or the end of output.
+         */
+        const size_t todo{minz(samplesPerBlock-startOffset, samplesToLoad-wrote)};
+        for(size_t i{0};i < todo;++i)
+        {
+            const size_t byteShift{(nibbleOffset&1) * 4};
+            const size_t wordOffset{(nibbleOffset>>1) & ~size_t{3}};
+            const size_t byteOffset{wordOffset*srcStep + ((nibbleOffset>>1)&3u)};
+            ++nibbleOffset;
+
+            const int result{decode_sample((nibbleData[byteOffset]>>byteShift) & 15u)};
+            dstSamples[wrote++] = static_cast<float>(result) / 32768.0f;
+        }
+        if(wrote == samplesToLoad)
+            return;
+
+        src += blockBytes;
+    } while(true);
+}
+
+template<>
+inline void LoadSamples<FmtMSADPCM>(float *RESTRICT dstSamples, const al::byte *src,
+    const size_t srcChan, const size_t srcOffset, const size_t srcStep,
+    const size_t samplesPerBlock, const size_t samplesToLoad) noexcept
+{
+    const size_t blockBytes{((samplesPerBlock-2)/2 + 7)*srcStep};
+
+    src += srcOffset/samplesPerBlock*blockBytes;
+    size_t skip{srcOffset % samplesPerBlock};
+
+    size_t wrote{0};
+    do {
+        /* Each MS ADPCM block starts with an 8-bit block predictor, used to
+         * dictate how the two sample history values are mixed with the decoded
+         * sample, and an initial signed 16-bit delta value which scales the
+         * nibble sample value. This is followed by the two initial 16-bit
+         * sample history values.
+         */
+        const al::byte *input{src};
+        const uint8_t blockpred{std::min(input[srcChan], uint8_t{6})};
+        input += srcStep;
+        int delta{input[2*srcChan + 0] | (input[2*srcChan + 1] << 8)};
+        input += srcStep*2;
+
+        int sampleHistory[2]{};
+        sampleHistory[0] = input[2*srcChan + 0] | (input[2*srcChan + 1]<<8);
+        input += srcStep*2;
+        sampleHistory[1] = input[2*srcChan + 0] | (input[2*srcChan + 1]<<8);
+        input += srcStep*2;
+
+        const auto coeffs = al::as_span(MSADPCMAdaptionCoeff[blockpred]);
+        delta = (delta^0x8000) - 32768;
+        sampleHistory[0] = (sampleHistory[0]^0x8000) - 32768;
+        sampleHistory[1] = (sampleHistory[1]^0x8000) - 32768;
+
+        /* The second history sample is "older", so it's the first to be
+         * written out.
+         */
+        if(skip == 0)
+        {
+            dstSamples[wrote++] = static_cast<float>(sampleHistory[1]) / 32768.0f;
+            if(wrote == samplesToLoad) return;
+            dstSamples[wrote++] = static_cast<float>(sampleHistory[0]) / 32768.0f;
+            if(wrote == samplesToLoad) return;
+        }
+        else if(skip == 1)
+        {
+            --skip;
+            dstSamples[wrote++] = static_cast<float>(sampleHistory[0]) / 32768.0f;
+            if(wrote == samplesToLoad) return;
+        }
+        else
+            skip -= 2;
+
+        auto decode_sample = [&sampleHistory,&delta,coeffs](const int nibble)
+        {
+            int pred{(sampleHistory[0]*coeffs[0] + sampleHistory[1]*coeffs[1]) / 256};
+            pred += ((nibble^0x08) - 0x08) * delta;
+            pred  = clampi(pred, -32768, 32767);
+
+            sampleHistory[1] = sampleHistory[0];
+            sampleHistory[0] = pred;
+
+            delta = (MSADPCMAdaption[nibble] * delta) / 256;
+            delta = maxi(16, delta);
+
+            return pred;
+        };
+
+        /* The rest of the block is a series of nibbles, interleaved per-
+         * channel. First, skip samples.
+         */
+        const size_t startOffset{skip + 2};
+        size_t nibbleOffset{srcChan};
+        for(;skip;--skip)
+        {
+            const size_t byteOffset{nibbleOffset>>1};
+            const size_t byteShift{((nibbleOffset&1)^1) * 4};
+            nibbleOffset += srcStep;
+
+            std::ignore = decode_sample((input[byteOffset]>>byteShift) & 15);
+        }
+
+        /* Now decode the rest of the block, until the end of the block or the
+         * dst buffer is filled.
+         */
+        const size_t todo{minz(samplesPerBlock-startOffset, samplesToLoad-wrote)};
+        for(size_t j{0};j < todo;++j)
+        {
+            const size_t byteOffset{nibbleOffset>>1};
+            const size_t byteShift{((nibbleOffset&1)^1) * 4};
+            nibbleOffset += srcStep;
+
+            const int sample{decode_sample((input[byteOffset]>>byteShift) & 15)};
+            dstSamples[wrote++] = static_cast<float>(sample) / 32768.0f;
+        }
+        if(wrote == samplesToLoad)
+            return;
+
+        src += blockBytes;
+    } while(true);
+}
+
+void LoadSamples(float *dstSamples, const al::byte *src, const size_t srcChan,
+    const size_t srcOffset, const FmtType srcType, const size_t srcStep,
+    const size_t samplesPerBlock, const size_t samplesToLoad) noexcept
+{
+#define HANDLE_FMT(T) case T:                                                 \
+    LoadSamples<T>(dstSamples, src, srcChan, srcOffset, srcStep,              \
+        samplesPerBlock, samplesToLoad);                                      \
+    break
+
+    switch(srcType)
+    {
+    HANDLE_FMT(FmtUByte);
+    HANDLE_FMT(FmtShort);
+    HANDLE_FMT(FmtFloat);
+    HANDLE_FMT(FmtDouble);
+    HANDLE_FMT(FmtMulaw);
+    HANDLE_FMT(FmtAlaw);
+    HANDLE_FMT(FmtIMA4);
+    HANDLE_FMT(FmtMSADPCM);
+    }
+#undef HANDLE_FMT
+}
+
+void LoadBufferStatic(VoiceBufferItem *buffer, VoiceBufferItem *bufferLoopItem,
+    const size_t dataPosInt, const FmtType sampleType, const size_t srcChannel,
+    const size_t srcStep, size_t samplesLoaded, const size_t samplesToLoad,
+    float *voiceSamples)
+{
+    if(!bufferLoopItem)
+    {
+        /* Load what's left to play from the buffer */
+        if(buffer->mSampleLen > dataPosInt) LIKELY
+        {
+            const size_t buffer_remaining{buffer->mSampleLen - dataPosInt};
+            const size_t remaining{minz(samplesToLoad-samplesLoaded, buffer_remaining)};
+            LoadSamples(voiceSamples+samplesLoaded, buffer->mSamples, srcChannel, dataPosInt,
+                sampleType, srcStep, buffer->mBlockAlign, remaining);
+            samplesLoaded += remaining;
+        }
+
+        if(const size_t toFill{samplesToLoad - samplesLoaded})
+        {
+            auto srcsamples = voiceSamples + samplesLoaded;
+            std::fill_n(srcsamples, toFill, *(srcsamples-1));
+        }
+    }
+    else
+    {
+        const size_t loopStart{buffer->mLoopStart};
+        const size_t loopEnd{buffer->mLoopEnd};
+        ASSUME(loopEnd > loopStart);
+
+        const size_t intPos{(dataPosInt < loopEnd) ? dataPosInt
+            : (((dataPosInt-loopStart)%(loopEnd-loopStart)) + loopStart)};
+
+        /* Load what's left of this loop iteration */
+        const size_t remaining{minz(samplesToLoad-samplesLoaded, loopEnd-dataPosInt)};
+        LoadSamples(voiceSamples+samplesLoaded, buffer->mSamples, srcChannel, intPos, sampleType,
+            srcStep, buffer->mBlockAlign, remaining);
+        samplesLoaded += remaining;
+
+        /* Load repeats of the loop to fill the buffer. */
+        const size_t loopSize{loopEnd - loopStart};
+        while(const size_t toFill{minz(samplesToLoad - samplesLoaded, loopSize)})
+        {
+            LoadSamples(voiceSamples+samplesLoaded, buffer->mSamples, srcChannel, loopStart,
+                sampleType, srcStep, buffer->mBlockAlign, toFill);
+            samplesLoaded += toFill;
+        }
+    }
+}
+
+void LoadBufferCallback(VoiceBufferItem *buffer, const size_t dataPosInt,
+    const size_t numCallbackSamples, const FmtType sampleType, const size_t srcChannel,
+    const size_t srcStep, size_t samplesLoaded, const size_t samplesToLoad, float *voiceSamples)
+{
+    /* Load what's left to play from the buffer */
+    if(numCallbackSamples > dataPosInt) LIKELY
+    {
+        const size_t remaining{minz(samplesToLoad-samplesLoaded, numCallbackSamples-dataPosInt)};
+        LoadSamples(voiceSamples+samplesLoaded, buffer->mSamples, srcChannel, dataPosInt,
+            sampleType, srcStep, buffer->mBlockAlign, remaining);
+        samplesLoaded += remaining;
+    }
+
+    if(const size_t toFill{samplesToLoad - samplesLoaded})
+    {
+        auto srcsamples = voiceSamples + samplesLoaded;
+        std::fill_n(srcsamples, toFill, *(srcsamples-1));
+    }
+}
+
+void LoadBufferQueue(VoiceBufferItem *buffer, VoiceBufferItem *bufferLoopItem,
+    size_t dataPosInt, const FmtType sampleType, const size_t srcChannel,
+    const size_t srcStep, size_t samplesLoaded, const size_t samplesToLoad,
+    float *voiceSamples)
+{
+    /* Crawl the buffer queue to fill in the temp buffer */
+    while(buffer && samplesLoaded != samplesToLoad)
+    {
+        if(dataPosInt >= buffer->mSampleLen)
+        {
+            dataPosInt -= buffer->mSampleLen;
+            buffer = buffer->mNext.load(std::memory_order_acquire);
+            if(!buffer) buffer = bufferLoopItem;
+            continue;
+        }
+
+        const size_t remaining{minz(samplesToLoad-samplesLoaded, buffer->mSampleLen-dataPosInt)};
+        LoadSamples(voiceSamples+samplesLoaded, buffer->mSamples, srcChannel, dataPosInt,
+            sampleType, srcStep, buffer->mBlockAlign, remaining);
+
+        samplesLoaded += remaining;
+        if(samplesLoaded == samplesToLoad)
+            break;
+
+        dataPosInt = 0;
+        buffer = buffer->mNext.load(std::memory_order_acquire);
+        if(!buffer) buffer = bufferLoopItem;
+    }
+    if(const size_t toFill{samplesToLoad - samplesLoaded})
+    {
+        auto srcsamples = voiceSamples + samplesLoaded;
+        std::fill_n(srcsamples, toFill, *(srcsamples-1));
+    }
+}
+
+
+void DoHrtfMix(const float *samples, const uint DstBufferSize, DirectParams &parms,
+    const float TargetGain, const uint Counter, uint OutPos, const bool IsPlaying,
+    DeviceBase *Device)
+{
+    const uint IrSize{Device->mIrSize};
+    auto &HrtfSamples = Device->HrtfSourceData;
+    auto &AccumSamples = Device->HrtfAccumData;
+
+    /* 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. */
+    if(IsPlaying) LIKELY
+        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 = lerpf(parms.Hrtf.Old.Gain, TargetGain, a);
+        }
+
+        MixHrtfFilter hrtfparams{
+            parms.Hrtf.Target.Coeffs,
+            parms.Hrtf.Target.Delay,
+            0.0f, 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 = lerpf(parms.Hrtf.Old.Gain, TargetGain, a);
+        }
+
+        MixHrtfFilter hrtfparams{
+            parms.Hrtf.Target.Coeffs,
+            parms.Hrtf.Target.Delay,
+            parms.Hrtf.Old.Gain,
+            (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, DeviceBase *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, ContextBase *Context, const nanoseconds deviceTime,
+    const uint SamplesToDo)
+{
+    static constexpr std::array<float,MAX_OUTPUT_CHANNELS> SilentTarget{};
+
+    ASSUME(SamplesToDo > 0);
+
+    DeviceBase *Device{Context->mDevice};
+    const uint NumSends{Device->NumAuxSends};
+
+    /* Get voice info */
+    int 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 uint increment{mStep};
+    if(increment < 1) UNLIKELY
+    {
+        /* 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;
+    }
+
+    /* If the static voice's current position is beyond the buffer loop end
+     * position, disable looping.
+     */
+    if(mFlags.test(VoiceIsStatic) && BufferLoopItem)
+    {
+        if(DataPosInt >= 0 && static_cast<uint>(DataPosInt) >= BufferListItem->mLoopEnd)
+            BufferLoopItem = nullptr;
+    }
+
+    uint OutPos{0u};
+
+    /* Check if we're doing a delayed start, and we start in this update. */
+    if(mStartTime > deviceTime) UNLIKELY
+    {
+        /* If the voice is supposed to be stopping but hasn't actually started
+         * yet, make sure its stopped.
+         */
+        if(vstate == Stopping)
+        {
+            mPlayState.store(Stopped, std::memory_order_release);
+            return;
+        }
+
+        /* If the start time is too far ahead, don't bother. */
+        auto diff = mStartTime - deviceTime;
+        if(diff >= seconds{1})
+            return;
+
+        /* Get the number of samples ahead of the current time that output
+         * should start at. Skip this update if it's beyond the output sample
+         * count.
+         *
+         * Round the start position to a multiple of 4, which some mixers want.
+         * This makes the start time accurate to 4 samples. This could be made
+         * sample-accurate by forcing non-SIMD functions on the first run.
+         */
+        seconds::rep sampleOffset{duration_cast<seconds>(diff * Device->Frequency).count()};
+        sampleOffset = (sampleOffset+2) & ~seconds::rep{3};
+        if(sampleOffset >= SamplesToDo)
+            return;
+
+        OutPos = static_cast<uint>(sampleOffset);
+    }
+
+    /* Calculate the number of samples to mix, and the number of (resampled)
+     * samples that need to be loaded (mixing samples and decoder padding).
+     */
+    const uint samplesToMix{SamplesToDo - OutPos};
+    const uint samplesToLoad{samplesToMix + mDecoderPadding};
+
+    /* Get a span of pointers to hold the floating point, deinterlaced,
+     * resampled buffer data to be mixed.
+     */
+    std::array<float*,DeviceBase::MixerChannelsMax> SamplePointers;
+    const al::span<float*> MixingSamples{SamplePointers.data(), mChans.size()};
+    auto get_bufferline = [](DeviceBase::MixerBufferLine &bufline) noexcept -> float*
+    { return bufline.data(); };
+    std::transform(Device->mSampleData.end() - mChans.size(), Device->mSampleData.end(),
+        MixingSamples.begin(), get_bufferline);
+
+    /* If there's a matching sample step and no phase offset, use a simple copy
+     * for resampling.
+     */
+    const ResamplerFunc Resample{(increment == MixerFracOne && DataPosFrac == 0)
+        ? ResamplerFunc{[](const InterpState*, const float *RESTRICT src, uint, const uint,
+            const al::span<float> dst) { std::copy_n(src, dst.size(), dst.begin()); }}
+        : mResampler};
+
+    /* UHJ2 and SuperStereo only have 2 buffer channels, but 3 mixing channels
+     * (3rd channel is generated from decoding).
+     */
+    const size_t realChannels{(mFmtChannels == FmtUHJ2 || mFmtChannels == FmtSuperStereo) ? 2u
+        : MixingSamples.size()};
+    for(size_t chan{0};chan < realChannels;++chan)
+    {
+        using ResBufType = decltype(DeviceBase::mResampleData);
+        static constexpr uint srcSizeMax{static_cast<uint>(ResBufType{}.size()-MaxResamplerEdge)};
+
+        const auto prevSamples = al::as_span(mPrevSamples[chan]);
+        const auto resampleBuffer = std::copy(prevSamples.cbegin(), prevSamples.cend(),
+            Device->mResampleData.begin()) - MaxResamplerEdge;
+        int intPos{DataPosInt};
+        uint fracPos{DataPosFrac};
+
+        /* Load samples for this channel from the available buffer(s), with
+         * resampling.
+         */
+        for(uint samplesLoaded{0};samplesLoaded < samplesToLoad;)
+        {
+            /* Calculate the number of dst samples that can be loaded this
+             * iteration, given the available resampler buffer size, and the
+             * number of src samples that are needed to load it.
+             */
+            auto calc_buffer_sizes = [fracPos,increment](uint dstBufferSize)
+            {
+                /* If ext=true, calculate the last written dst pos from the dst
+                 * count, convert to the last read src pos, then add one to get
+                 * the src count.
+                 *
+                 * If ext=false, convert the dst count to src count directly.
+                 *
+                 * Without this, the src count could be short by one when
+                 * increment < 1.0, or not have a full src at the end when
+                 * increment > 1.0.
+                 */
+                const bool ext{increment <= MixerFracOne};
+                uint64_t dataSize64{dstBufferSize - ext};
+                dataSize64 = (dataSize64*increment + fracPos) >> MixerFracBits;
+                /* Also include resampler padding. */
+                dataSize64 += ext + MaxResamplerEdge;
+
+                if(dataSize64 <= srcSizeMax)
+                    return std::make_pair(dstBufferSize, static_cast<uint>(dataSize64));
+
+                /* If the source size got saturated, we can't fill the desired
+                 * dst size. Figure out how many dst samples we can fill.
+                 */
+                dataSize64 = srcSizeMax - MaxResamplerEdge;
+                dataSize64 = ((dataSize64<<MixerFracBits) - fracPos) / increment;
+                if(dataSize64 < dstBufferSize)
+                {
+                    /* Some resamplers require the destination being 16-byte
+                     * aligned, so limit to a multiple of 4 samples to maintain
+                     * alignment if we need to do another iteration after this.
+                     */
+                    dstBufferSize = static_cast<uint>(dataSize64) & ~3u;
+                }
+                return std::make_pair(dstBufferSize, srcSizeMax);
+            };
+            const auto bufferSizes = calc_buffer_sizes(samplesToLoad - samplesLoaded);
+            const auto dstBufferSize = bufferSizes.first;
+            const auto srcBufferSize = bufferSizes.second;
+
+            /* Load the necessary samples from the given buffer(s). */
+            if(!BufferListItem)
+            {
+                const uint avail{minu(srcBufferSize, MaxResamplerEdge)};
+                const uint tofill{maxu(srcBufferSize, MaxResamplerEdge)};
+
+                /* When loading from a voice that ended prematurely, only take
+                 * the samples that get closest to 0 amplitude. This helps
+                 * certain sounds fade out better.
+                 */
+                auto abs_lt = [](const float lhs, const float rhs) noexcept -> bool
+                { return std::abs(lhs) < std::abs(rhs); };
+                auto srciter = std::min_element(resampleBuffer, resampleBuffer+avail, abs_lt);
+
+                std::fill(srciter+1, resampleBuffer+tofill, *srciter);
+            }
+            else
+            {
+                size_t srcSampleDelay{0};
+                if(intPos < 0) UNLIKELY
+                {
+                    /* If the current position is negative, there's that many
+                     * silent samples to load before using the buffer.
+                     */
+                    srcSampleDelay = static_cast<uint>(-intPos);
+                    if(srcSampleDelay >= srcBufferSize)
+                    {
+                        /* If the number of silent source samples exceeds the
+                         * number to load, the output will be silent.
+                         */
+                        std::fill_n(MixingSamples[chan]+samplesLoaded, dstBufferSize, 0.0f);
+                        std::fill_n(resampleBuffer, srcBufferSize, 0.0f);
+                        goto skip_resample;
+                    }
+
+                    std::fill_n(resampleBuffer, srcSampleDelay, 0.0f);
+                }
+                const uint uintPos{static_cast<uint>(maxi(intPos, 0))};
+
+                if(mFlags.test(VoiceIsStatic))
+                    LoadBufferStatic(BufferListItem, BufferLoopItem, uintPos, mFmtType, chan,
+                        mFrameStep, srcSampleDelay, srcBufferSize, al::to_address(resampleBuffer));
+                else if(mFlags.test(VoiceIsCallback))
+                {
+                    const uint callbackBase{mCallbackBlockBase * mSamplesPerBlock};
+                    const size_t bufferOffset{uintPos - callbackBase};
+                    const size_t needSamples{bufferOffset + srcBufferSize - srcSampleDelay};
+                    const size_t needBlocks{(needSamples + mSamplesPerBlock-1) / mSamplesPerBlock};
+                    if(!mFlags.test(VoiceCallbackStopped) && needBlocks > mNumCallbackBlocks)
+                    {
+                        const size_t byteOffset{mNumCallbackBlocks*mBytesPerBlock};
+                        const size_t needBytes{(needBlocks-mNumCallbackBlocks)*mBytesPerBlock};
+
+                        const int gotBytes{BufferListItem->mCallback(BufferListItem->mUserData,
+                            &BufferListItem->mSamples[byteOffset], static_cast<int>(needBytes))};
+                        if(gotBytes < 0)
+                            mFlags.set(VoiceCallbackStopped);
+                        else if(static_cast<uint>(gotBytes) < needBytes)
+                        {
+                            mFlags.set(VoiceCallbackStopped);
+                            mNumCallbackBlocks += static_cast<uint>(gotBytes) / mBytesPerBlock;
+                        }
+                        else
+                            mNumCallbackBlocks = static_cast<uint>(needBlocks);
+                    }
+                    const size_t numSamples{uint{mNumCallbackBlocks} * mSamplesPerBlock};
+                    LoadBufferCallback(BufferListItem, bufferOffset, numSamples, mFmtType, chan,
+                        mFrameStep, srcSampleDelay, srcBufferSize, al::to_address(resampleBuffer));
+                }
+                else
+                    LoadBufferQueue(BufferListItem, BufferLoopItem, uintPos, mFmtType, chan,
+                        mFrameStep, srcSampleDelay, srcBufferSize, al::to_address(resampleBuffer));
+            }
+
+            Resample(&mResampleState, al::to_address(resampleBuffer), fracPos, increment,
+                {MixingSamples[chan]+samplesLoaded, dstBufferSize});
+
+            /* Store the last source samples used for next time. */
+            if(vstate == Playing) LIKELY
+            {
+                /* Only store samples for the end of the mix, excluding what
+                 * gets loaded for decoder padding.
+                 */
+                const uint loadEnd{samplesLoaded + dstBufferSize};
+                if(samplesToMix > samplesLoaded && samplesToMix <= loadEnd) LIKELY
+                {
+                    const size_t dstOffset{samplesToMix - samplesLoaded};
+                    const size_t srcOffset{(dstOffset*increment + fracPos) >> MixerFracBits};
+                    std::copy_n(resampleBuffer-MaxResamplerEdge+srcOffset, prevSamples.size(),
+                        prevSamples.begin());
+                }
+            }
+
+        skip_resample:
+            samplesLoaded += dstBufferSize;
+            if(samplesLoaded < samplesToLoad)
+            {
+                fracPos += dstBufferSize*increment;
+                const uint srcOffset{fracPos >> MixerFracBits};
+                fracPos &= MixerFracMask;
+                intPos += srcOffset;
+
+                /* If more samples need to be loaded, copy the back of the
+                 * resampleBuffer to the front to reuse it. prevSamples isn't
+                 * reliable since it's only updated for the end of the mix.
+                 */
+                std::copy(resampleBuffer-MaxResamplerEdge+srcOffset,
+                    resampleBuffer+MaxResamplerEdge+srcOffset, resampleBuffer-MaxResamplerEdge);
+            }
+        }
+    }
+    for(auto &samples : MixingSamples.subspan(realChannels))
+        std::fill_n(samples, samplesToLoad, 0.0f);
+
+    if(mDecoder)
+        mDecoder->decode(MixingSamples, samplesToMix, (vstate==Playing));
+
+    if(mFlags.test(VoiceIsAmbisonic))
+    {
+        auto voiceSamples = MixingSamples.begin();
+        for(auto &chandata : mChans)
+        {
+            chandata.mAmbiSplitter.processScale({*voiceSamples, samplesToMix},
+                chandata.mAmbiHFScale, chandata.mAmbiLFScale);
+            ++voiceSamples;
+        }
+    }
+
+    const uint Counter{mFlags.test(VoiceIsFading) ? minu(samplesToMix, 64u) : 0u};
+    if(!Counter)
+    {
+        /* No fading, just overwrite the old/current params. */
+        for(auto &chandata : mChans)
+        {
+            {
+                DirectParams &parms = chandata.mDryParams;
+                if(!mFlags.test(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;
+            }
+        }
+    }
+
+    auto voiceSamples = MixingSamples.begin();
+    for(auto &chandata : mChans)
+    {
+        /* Now filter and mix to the appropriate outputs. */
+        const al::span<float,BufferLineSize> FilterBuf{Device->FilteredData};
+        {
+            DirectParams &parms = chandata.mDryParams;
+            const float *samples{DoFilters(parms.LowPass, parms.HighPass, FilterBuf.data(),
+                {*voiceSamples, samplesToMix}, mDirect.FilterType)};
+
+            if(mFlags.test(VoiceHasHrtf))
+            {
+                const float TargetGain{parms.Hrtf.Target.Gain * (vstate == Playing)};
+                DoHrtfMix(samples, samplesToMix, parms, TargetGain, Counter, OutPos,
+                    (vstate == Playing), Device);
+            }
+            else
+            {
+                const float *TargetGains{(vstate == Playing) ? parms.Gains.Target.data()
+                    : SilentTarget.data()};
+                if(mFlags.test(VoiceHasNfc))
+                    DoNfcMix({samples, samplesToMix}, mDirect.Buffer.data(), parms,
+                        TargetGains, Counter, OutPos, Device);
+                else
+                    MixSamples({samples, samplesToMix}, 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.data(),
+                {*voiceSamples, samplesToMix}, mSend[send].FilterType)};
+
+            const float *TargetGains{(vstate == Playing) ? parms.Gains.Target.data()
+                : SilentTarget.data()};
+            MixSamples({samples, samplesToMix}, mSend[send].Buffer,
+                parms.Gains.Current.data(), TargetGains, Counter, OutPos);
+        }
+
+        ++voiceSamples;
+    }
+
+    mFlags.set(VoiceIsFading);
+
+    /* Don't update positions and buffers if we were stopping. */
+    if(vstate == Stopping) UNLIKELY
+    {
+        mPlayState.store(Stopped, std::memory_order_release);
+        return;
+    }
+
+    /* Update voice positions and buffers as needed. */
+    DataPosFrac += increment*samplesToMix;
+    const uint SrcSamplesDone{DataPosFrac>>MixerFracBits};
+    DataPosInt  += SrcSamplesDone;
+    DataPosFrac &= MixerFracMask;
+
+    uint buffers_done{0u};
+    if(BufferListItem && DataPosInt >= 0) LIKELY
+    {
+        if(mFlags.test(VoiceIsStatic))
+        {
+            if(BufferLoopItem)
+            {
+                /* Handle looping static source */
+                const uint LoopStart{BufferListItem->mLoopStart};
+                const uint LoopEnd{BufferListItem->mLoopEnd};
+                uint DataPosUInt{static_cast<uint>(DataPosInt)};
+                if(DataPosUInt >= LoopEnd)
+                {
+                    assert(LoopEnd > LoopStart);
+                    DataPosUInt = ((DataPosUInt-LoopStart)%(LoopEnd-LoopStart)) + LoopStart;
+                    DataPosInt = static_cast<int>(DataPosUInt);
+                }
+            }
+            else
+            {
+                /* Handle non-looping static source */
+                if(static_cast<uint>(DataPosInt) >= BufferListItem->mSampleLen)
+                    BufferListItem = nullptr;
+            }
+        }
+        else if(mFlags.test(VoiceIsCallback))
+        {
+            /* Handle callback buffer source */
+            const uint currentBlock{static_cast<uint>(DataPosInt) / mSamplesPerBlock};
+            const uint blocksDone{currentBlock - mCallbackBlockBase};
+            if(blocksDone < mNumCallbackBlocks)
+            {
+                const size_t byteOffset{blocksDone*mBytesPerBlock};
+                const size_t byteEnd{mNumCallbackBlocks*mBytesPerBlock};
+                al::byte *data{BufferListItem->mSamples};
+                std::copy(data+byteOffset, data+byteEnd, data);
+                mNumCallbackBlocks -= blocksDone;
+                mCallbackBlockBase += blocksDone;
+            }
+            else
+            {
+                BufferListItem = nullptr;
+                mNumCallbackBlocks = 0;
+                mCallbackBlockBase += blocksDone;
+            }
+        }
+        else
+        {
+            /* Handle streaming source */
+            do {
+                if(BufferListItem->mSampleLen > static_cast<uint>(DataPosInt))
+                    break;
+
+                DataPosInt -= BufferListItem->mSampleLen;
+
+                ++buffers_done;
+                BufferListItem = BufferListItem->mNext.load(std::memory_order_relaxed);
+                if(!BufferListItem) BufferListItem = BufferLoopItem;
+            } while(BufferListItem);
+        }
+    }
+
+    /* Capture the source ID in case it gets 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 auto enabledevt = Context->mEnabledEvts.load(std::memory_order_acquire);
+    if(buffers_done > 0 && enabledevt.test(AsyncEvent::BufferCompleted))
+    {
+        RingBuffer *ring{Context->mAsyncEvents.get()};
+        auto evt_vec = ring->getWriteVector();
+        if(evt_vec.first.len > 0)
+        {
+            AsyncEvent *evt{al::construct_at(reinterpret_cast<AsyncEvent*>(evt_vec.first.buf),
+                AsyncEvent::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.test(AsyncEvent::SourceStateChange))
+            SendSourceStoppedEvent(Context, SourceID);
+    }
+}
+
+void Voice::prepare(DeviceBase *device)
+{
+    /* Even if storing really high order ambisonics, we only mix channels for
+     * orders up to the device order. The rest are simply dropped.
+     */
+    uint num_channels{(mFmtChannels == FmtUHJ2 || mFmtChannels == FmtSuperStereo) ? 3 :
+        ChannelsFromFmt(mFmtChannels, minu(mAmbiOrder, device->mAmbiOrder))};
+    if(num_channels > device->mSampleData.size()) UNLIKELY
+    {
+        ERR("Unexpected channel count: %u (limit: %zu, %d:%d)\n", num_channels,
+            device->mSampleData.size(), mFmtChannels, mAmbiOrder);
+        num_channels = static_cast<uint>(device->mSampleData.size());
+    }
+    if(mChans.capacity() > 2 && num_channels < mChans.capacity())
+    {
+        decltype(mChans){}.swap(mChans);
+        decltype(mPrevSamples){}.swap(mPrevSamples);
+    }
+    mChans.reserve(maxu(2, num_channels));
+    mChans.resize(num_channels);
+    mPrevSamples.reserve(maxu(2, num_channels));
+    mPrevSamples.resize(num_channels);
+
+    mDecoder = nullptr;
+    mDecoderPadding = 0;
+    if(mFmtChannels == FmtSuperStereo)
+    {
+        switch(UhjDecodeQuality)
+        {
+        case UhjQualityType::IIR:
+            mDecoder = std::make_unique<UhjStereoDecoderIIR>();
+            mDecoderPadding = UhjStereoDecoderIIR::sInputPadding;
+            break;
+        case UhjQualityType::FIR256:
+            mDecoder = std::make_unique<UhjStereoDecoder<UhjLength256>>();
+            mDecoderPadding = UhjStereoDecoder<UhjLength256>::sInputPadding;
+            break;
+        case UhjQualityType::FIR512:
+            mDecoder = std::make_unique<UhjStereoDecoder<UhjLength512>>();
+            mDecoderPadding = UhjStereoDecoder<UhjLength512>::sInputPadding;
+            break;
+        }
+    }
+    else if(IsUHJ(mFmtChannels))
+    {
+        switch(UhjDecodeQuality)
+        {
+        case UhjQualityType::IIR:
+            mDecoder = std::make_unique<UhjDecoderIIR>();
+            mDecoderPadding = UhjDecoderIIR::sInputPadding;
+            break;
+        case UhjQualityType::FIR256:
+            mDecoder = std::make_unique<UhjDecoder<UhjLength256>>();
+            mDecoderPadding = UhjDecoder<UhjLength256>::sInputPadding;
+            break;
+        case UhjQualityType::FIR512:
+            mDecoder = std::make_unique<UhjDecoder<UhjLength512>>();
+            mDecoderPadding = UhjDecoder<UhjLength512>::sInputPadding;
+            break;
+        }
+    }
+
+    /* Clear the stepping value explicitly so the mixer knows not to mix this
+     * until the update gets applied.
+     */
+    mStep = 0;
+
+    /* Make sure the sample history is cleared. */
+    std::fill(mPrevSamples.begin(), mPrevSamples.end(), HistoryLine{});
+
+    if(mFmtChannels == FmtUHJ2 && !device->mUhjEncoder)
+    {
+        /* 2-channel UHJ needs different shelf filters. However, we can't just
+         * use different shelf filters after mixing it, given any old speaker
+         * setup the user has. To make this work, we apply the expected shelf
+         * filters for decoding UHJ2 to quad (only needs LF scaling), and act
+         * as if those 4 quad channels are encoded right back into B-Format.
+         *
+         * This isn't perfect, but without an entirely separate and limited
+         * UHJ2 path, it's better than nothing.
+         *
+         * Note this isn't needed with UHJ output (UHJ2->B-Format->UHJ2 is
+         * identity, so don't mess with it).
+         */
+        const BandSplitter splitter{device->mXOverFreq / static_cast<float>(device->Frequency)};
+        for(auto &chandata : mChans)
+        {
+            chandata.mAmbiHFScale = 1.0f;
+            chandata.mAmbiLFScale = 1.0f;
+            chandata.mAmbiSplitter = splitter;
+            chandata.mDryParams = DirectParams{};
+            chandata.mDryParams.NFCtrlFilter = device->mNFCtrlFilter;
+            std::fill_n(chandata.mWetParams.begin(), device->NumAuxSends, SendParams{});
+        }
+        mChans[0].mAmbiLFScale = DecoderBase::sWLFScale;
+        mChans[1].mAmbiLFScale = DecoderBase::sXYLFScale;
+        mChans[2].mAmbiLFScale = DecoderBase::sXYLFScale;
+        mFlags.set(VoiceIsAmbisonic);
+    }
+    /* Don't need to set the VoiceIsAmbisonic flag if the device is not higher
+     * order than the voice. No HF scaling is necessary to mix it.
+     */
+    else if(mAmbiOrder && device->mAmbiOrder > mAmbiOrder)
+    {
+        const uint8_t *OrderFromChan{Is2DAmbisonic(mFmtChannels) ?
+            AmbiIndex::OrderFrom2DChannel().data() : AmbiIndex::OrderFromChannel().data()};
+        const auto scales = AmbiScale::GetHFOrderScales(mAmbiOrder, device->mAmbiOrder,
+            device->m2DMixing);
+
+        const BandSplitter splitter{device->mXOverFreq / static_cast<float>(device->Frequency)};
+        for(auto &chandata : mChans)
+        {
+            chandata.mAmbiHFScale = scales[*(OrderFromChan++)];
+            chandata.mAmbiLFScale = 1.0f;
+            chandata.mAmbiSplitter = splitter;
+            chandata.mDryParams = DirectParams{};
+            chandata.mDryParams.NFCtrlFilter = device->mNFCtrlFilter;
+            std::fill_n(chandata.mWetParams.begin(), device->NumAuxSends, SendParams{});
+        }
+        mFlags.set(VoiceIsAmbisonic);
+    }
+    else
+    {
+        for(auto &chandata : mChans)
+        {
+            chandata.mDryParams = DirectParams{};
+            chandata.mDryParams.NFCtrlFilter = device->mNFCtrlFilter;
+            std::fill_n(chandata.mWetParams.begin(), device->NumAuxSends, SendParams{});
+        }
+        mFlags.reset(VoiceIsAmbisonic);
+    }
+}
diff --git a/core/voice.h b/core/voice.h
new file mode 100644 (file)
index 0000000..57ee7b0
--- /dev/null
@@ -0,0 +1,280 @@
+#ifndef CORE_VOICE_H
+#define CORE_VOICE_H
+
+#include <array>
+#include <atomic>
+#include <bitset>
+#include <chrono>
+#include <memory>
+#include <stddef.h>
+#include <string>
+
+#include "albyte.h"
+#include "almalloc.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "bufferline.h"
+#include "buffer_storage.h"
+#include "devformat.h"
+#include "filters/biquad.h"
+#include "filters/nfc.h"
+#include "filters/splitter.h"
+#include "mixer/defs.h"
+#include "mixer/hrtfdefs.h"
+#include "resampler_limits.h"
+#include "uhjfilter.h"
+#include "vector.h"
+
+struct ContextBase;
+struct DeviceBase;
+struct EffectSlot;
+enum class DistanceModel : unsigned char;
+
+using uint = unsigned int;
+
+
+#define MAX_SENDS  6
+
+
+enum class SpatializeMode : unsigned char {
+    Off,
+    On,
+    Auto
+};
+
+enum class DirectMode : unsigned char {
+    Off,
+    DropMismatch,
+    RemixMismatch
+};
+
+
+constexpr uint MaxPitch{10};
+
+
+enum {
+    AF_None = 0,
+    AF_LowPass = 1,
+    AF_HighPass = 2,
+    AF_BandPass = AF_LowPass | AF_HighPass
+};
+
+
+struct DirectParams {
+    BiquadFilter LowPass;
+    BiquadFilter HighPass;
+
+    NfcFilter NFCtrlFilter;
+
+    struct {
+        HrtfFilter Old;
+        HrtfFilter Target;
+        alignas(16) std::array<float,HrtfHistoryLength> History;
+    } Hrtf;
+
+    struct {
+        std::array<float,MAX_OUTPUT_CHANNELS> Current;
+        std::array<float,MAX_OUTPUT_CHANNELS> Target;
+    } Gains;
+};
+
+struct SendParams {
+    BiquadFilter LowPass;
+    BiquadFilter HighPass;
+
+    struct {
+        std::array<float,MaxAmbiChannels> Current;
+        std::array<float,MaxAmbiChannels> Target;
+    } Gains;
+};
+
+
+struct VoiceBufferItem {
+    std::atomic<VoiceBufferItem*> mNext{nullptr};
+
+    CallbackType mCallback{nullptr};
+    void *mUserData{nullptr};
+
+    uint mBlockAlign{0u};
+    uint mSampleLen{0u};
+    uint mLoopStart{0u};
+    uint mLoopEnd{0u};
+
+    al::byte *mSamples{nullptr};
+};
+
+
+struct VoiceProps {
+    float Pitch;
+    float Gain;
+    float OuterGain;
+    float MinGain;
+    float MaxGain;
+    float InnerAngle;
+    float OuterAngle;
+    float RefDistance;
+    float MaxDistance;
+    float RolloffFactor;
+    std::array<float,3> Position;
+    std::array<float,3> Velocity;
+    std::array<float,3> Direction;
+    std::array<float,3> OrientAt;
+    std::array<float,3> OrientUp;
+    bool HeadRelative;
+    DistanceModel mDistanceModel;
+    Resampler mResampler;
+    DirectMode DirectChannels;
+    SpatializeMode mSpatializeMode;
+
+    bool DryGainHFAuto;
+    bool WetGainAuto;
+    bool WetGainHFAuto;
+    float OuterGainHF;
+
+    float AirAbsorptionFactor;
+    float RoomRolloffFactor;
+    float DopplerFactor;
+
+    std::array<float,2> StereoPan;
+
+    float Radius;
+    float EnhWidth;
+
+    /** Direct filter and auxiliary send info. */
+    struct {
+        float Gain;
+        float GainHF;
+        float HFReference;
+        float GainLF;
+        float LFReference;
+    } Direct;
+    struct SendData {
+        EffectSlot *Slot;
+        float Gain;
+        float GainHF;
+        float HFReference;
+        float GainLF;
+        float LFReference;
+    } Send[MAX_SENDS];
+};
+
+struct VoicePropsItem : public VoiceProps {
+    std::atomic<VoicePropsItem*> next{nullptr};
+
+    DEF_NEWDEL(VoicePropsItem)
+};
+
+enum : uint {
+    VoiceIsStatic,
+    VoiceIsCallback,
+    VoiceIsAmbisonic,
+    VoiceCallbackStopped,
+    VoiceIsFading,
+    VoiceHasHrtf,
+    VoiceHasNfc,
+
+    VoiceFlagCount
+};
+
+struct Voice {
+    enum State {
+        Stopped,
+        Playing,
+        Stopping,
+        Pending
+    };
+
+    std::atomic<VoicePropsItem*> mUpdate{nullptr};
+
+    VoiceProps mProps;
+
+    std::atomic<uint> mSourceID{0u};
+    std::atomic<State> mPlayState{Stopped};
+    std::atomic<bool> mPendingChange{false};
+
+    /**
+     * Source offset in samples, relative to the currently playing buffer, NOT
+     * the whole queue.
+     */
+    std::atomic<int> mPosition;
+    /** Fractional (fixed-point) offset to the next sample. */
+    std::atomic<uint> mPositionFrac;
+
+    /* Current buffer queue item being played. */
+    std::atomic<VoiceBufferItem*> mCurrentBuffer;
+
+    /* Buffer queue item to loop to at end of queue (will be NULL for non-
+     * looping voices).
+     */
+    std::atomic<VoiceBufferItem*> mLoopBuffer;
+
+    std::chrono::nanoseconds mStartTime{};
+
+    /* Properties for the attached buffer(s). */
+    FmtChannels mFmtChannels;
+    FmtType mFmtType;
+    uint mFrequency;
+    uint mFrameStep; /**< In steps of the sample type size. */
+    uint mBytesPerBlock; /**< Or for PCM formats, BytesPerFrame. */
+    uint mSamplesPerBlock; /**< Always 1 for PCM formats. */
+    AmbiLayout mAmbiLayout;
+    AmbiScaling mAmbiScaling;
+    uint mAmbiOrder;
+
+    std::unique_ptr<DecoderBase> mDecoder;
+    uint mDecoderPadding{};
+
+    /** Current target parameters used for mixing. */
+    uint mStep{0};
+
+    ResamplerFunc mResampler;
+
+    InterpState mResampleState;
+
+    std::bitset<VoiceFlagCount> mFlags{};
+    uint mNumCallbackBlocks{0};
+    uint mCallbackBlockBase{0};
+
+    struct TargetData {
+        int FilterType;
+        al::span<FloatBufferLine> Buffer;
+    };
+    TargetData mDirect;
+    std::array<TargetData,MAX_SENDS> mSend;
+
+    /* The first MaxResamplerPadding/2 elements are the sample history from the
+     * previous mix, with an additional MaxResamplerPadding/2 elements that are
+     * now current (which may be overwritten if the buffer data is still
+     * available).
+     */
+    using HistoryLine = std::array<float,MaxResamplerPadding>;
+    al::vector<HistoryLine,16> mPrevSamples{2};
+
+    struct ChannelData {
+        float mAmbiHFScale, mAmbiLFScale;
+        BandSplitter mAmbiSplitter;
+
+        DirectParams mDryParams;
+        std::array<SendParams,MAX_SENDS> mWetParams;
+    };
+    al::vector<ChannelData> mChans{2};
+
+    Voice() = default;
+    ~Voice() = default;
+
+    Voice(const Voice&) = delete;
+    Voice& operator=(const Voice&) = delete;
+
+    void mix(const State vstate, ContextBase *Context, const std::chrono::nanoseconds deviceTime,
+        const uint SamplesToDo);
+
+    void prepare(DeviceBase *device);
+
+    static void InitMixer(al::optional<std::string> resampler);
+
+    DEF_NEWDEL(Voice)
+};
+
+extern Resampler ResamplerDefault;
+
+#endif /* CORE_VOICE_H */
diff --git a/core/voice_change.h b/core/voice_change.h
new file mode 100644 (file)
index 0000000..ddc6186
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef VOICE_CHANGE_H
+#define VOICE_CHANGE_H
+
+#include <atomic>
+
+#include "almalloc.h"
+
+struct Voice;
+
+using uint = unsigned int;
+
+
+enum class VChangeState {
+    Reset,
+    Stop,
+    Play,
+    Pause,
+    Restart
+};
+struct VoiceChange {
+    Voice *mOldVoice{nullptr};
+    Voice *mVoice{nullptr};
+    uint mSourceID{0};
+    VChangeState mState{};
+
+    std::atomic<VoiceChange*> mNext{nullptr};
+
+    DEF_NEWDEL(VoiceChange)
+};
+
+#endif /* VOICE_CHANGE_H */
diff --git a/docs/3D7.1.txt b/docs/3D7.1.txt
new file mode 100644 (file)
index 0000000..b7249c2
--- /dev/null
@@ -0,0 +1,75 @@
+Overview
+========
+
+3D7.1 is a custom speaker layout designed by Simon Goodwin at Codemasters[1].
+Typical surround sound setups, like quad, 5.1, 6.1, and 7.1, only produce audio
+on a 2D horizontal plane with no verticality, which means the envelopment of
+"surround" sound is limited to left, right, front, and back panning. Sounds
+that should come from above or below will still only play in 2D since there is
+no height difference in the speaker array.
+
+To work around this, 3D7.1 was designed so that some speakers are placed higher
+than the listener while others are lower, in a particular configuration that
+tries to provide balanced output and maintain some compatibility with existing
+audio content and software. Software that recognizes this setup, or can be
+configured for it, can then take advantage of the height difference and
+increase the perception of verticality for true 3D audio. The result is that
+sounds can be perceived as coming from left, right, front, and back, as well as
+up and down.
+
+[1] http://www.codemasters.com/research/3D_sound_for_3D_games.pdf
+
+
+Hardware Setup
+==============
+
+Setting up 3D7.1 requires an audio device capable of raw 8-channel or 7.1
+output, along with a 7.1 speaker kit. The speakers should be hooked up to the
+device in the usual way, with front-left and front-right output going to the
+front-left and front-right speakers, etc. The placement of the speakers should
+be set up according to the table below. Azimuth is the horizontal angle in
+degrees, with 0 directly in front and positive values go /left/, and elevation
+is the vertical angle in degrees, with 0 at head level and positive values go
+/up/.
+
+------------------------------------------------------------
+- Speaker label | Azimuth | Elevation |      New label     -
+------------------------------------------------------------
+- Front left    |    51   |     24    |   Upper front left -
+- Front right   |   -51   |     24    |  Upper front right -
+- Front center  |     0   |      0    |       Front center -
+- Subwoofer/LFE |   N/A   |    N/A    |      Subwoofer/LFE -
+- Side left     |   129   |    -24    |    Lower back left -
+- Side right    |  -129   |    -24    |   Lower back right -
+- Back left     |   180   |     55    |  Upper back center -
+- Back right    |     0   |    -55    | Lower front center -
+------------------------------------------------------------
+
+Note that this speaker layout *IS NOT* compatible with standard 7.1 content.
+Audio that should be played from the back will come out at the wrong location
+since the back speakers are placed in the lower front and upper back positions.
+However, this speaker layout *IS* more or less compatible with standard 5.1
+content. Though slightly tilted, to a listener sitting a bit further back from
+the center, the front and side speakers will be close enough to their intended
+locations that the output won't be too off.
+
+
+Software Setup
+==============
+
+To enable 3D7.1 on OpenAL Soft, first make sure the audio device is configured
+for 7.1 output. Then in the alsoft-config utility, for the Channels setting
+choose "3D7.1 Surround" from the drop-down list. And that's it. Any application
+using OpenAL Soft can take advantage of fully 3D audio, and multi-channel
+sounds will be properly remixed for the speaker layout.
+
+Note that care must be taken that the audio device is not treated as a "true"
+7.1 device by non-3D7.1-capable applications. In particular, the audio server
+should not try to upmix stereo and 5.1 content to "fill out" the back speakers,
+and non-3D7.1 apps should be set to either stereo or 5.1 output.
+
+As such, if your system is capable of it, it may be useful to define a virtual
+5.1 device that maps the front, side, and LFE channels to the main device for
+output and disables upmixing, then use that virtual 5.1 device for apps that do
+normal stereo or surround sound output, and use the main device for apps that
+understand 3D7.1 output.
diff --git a/docs/ambdec.txt b/docs/ambdec.txt
new file mode 100644 (file)
index 0000000..a301004
--- /dev/null
@@ -0,0 +1,192 @@
+AmbDec Configuration Files
+==========================
+
+AmbDec configuration files were developed by Fons Adriaensen as part of the
+AmbDec program <http://kokkinizita.linuxaudio.org/linuxaudio/index.html>.
+
+The file works by specifying a decoder matrix or matrices which transform
+ambisonic channels into speaker feeds. Single-band decoders specify a single
+matrix that transforms all frequencies, while dual-band decoders specifies two
+matrices where one transforms low frequency sounds and the other transforms
+high frequency sounds. See docs/ambisonics.txt for more general information
+about ambisonics.
+
+Starting with OpenAL Soft 1.18, version 3 of the file format is supported as a
+means of specifying custom surround sound speaker layouts. These configuration
+files are also used to enable per-speaker distance compensation.
+
+
+File Format
+===========
+
+As of this writing, there is no official documentation of the .ambdec file
+format. However, the format as OpenAL Soft sees it is as follows:
+
+The file is plain text. Comments start with a hash/pound character (#). There
+may be any amount of whitespace in between the option and parameter values.
+Strings are *not* enclosed in quotation marks.
+
+/description <desc:string>
+Specifies a text description of the configuration. Ignored by OpenAL Soft.
+
+/version <ver:int>
+Declares the format version used by the configuration file. OpenAL Soft
+currently only supports version 3.
+
+/dec/chan_mask <mask:hex>
+Specifies a hexadecimal mask value of ambisonic input channels used by this
+decoder. Counting up from the least significant bit, bit 0 maps to Ambisonic
+Channel Number (ACN) 0, bit 1 maps to ACN 1, etc. As an example, a value of 'b'
+enables bits 0, 1, and 3 (1011 in binary), which correspond to ACN 0, 1, and 3
+(first-order horizontal).
+
+/dec/freq_bands <count:int>
+Specifies the number of frequency bands used by the decoder. This must be 1 for
+single-band or 2 for dual-band.
+
+/dec/speakers <count:int>
+Specifies the number of output speakers to decode to.
+
+/dec/coeff_scale <type:string>
+Specifies the scaling used by the decoder coefficients. Currently recognized
+types are fuma, sn3d, and n3d, for Furse-Malham (FuMa), semi-normalized (SN3D),
+and fully normalized (N3D) scaling, respectively.
+
+/opt/input_scale <name:string>
+Specifies the scaling used by the ambisonic input data. As OpenAL Soft renders
+the data itself and knows the scaling, this is ignored.
+
+/opt/nfeff_comp <dir:string>
+Specifies whether near-field effect compensation is off (not applied at all),
+applied on input (faster, less accurate with varying speaker distances) or
+output (slower, more accurate with varying speaker distances). Ignored by
+OpenAL Soft.
+
+/opt/delay_comp <onoff:bool>
+Specifies whether delay compensation is applied for output. This is used to
+correct for time variations caused by different speaker distances. As OpenAL
+Soft has its own config option for this, this is ignored.
+
+/opt/level_comp <onoff:bool>
+Specifies whether gain compensation is applied for output. This is used to
+correct for volume variations caused by different speaker distances. As OpenAL
+Soft has its own config option for this, this is ignored.
+
+/opt/xover_freq <freq:float>
+Specifies the crossover frequency for dual-band decoders. Frequencies less than
+this are fed to the low-frequency matrix, and frequencies greater than this are
+fed to the high-frequency matrix. Unused for single-band decoders.
+
+/opt/xover_ratio <decibels:float>
+Specifies the volume ratio between the frequency bands. Values greater than 0
+decrease the low-frequency output by half the specified value and increase the
+high-frequency output by half the specified value, while values less than 0
+increase the low-frequency output and decrease the high-frequency output to
+similar effect. Unused for single-band decoders.
+
+/speakers/{
+Begins the output speaker definitions. A speaker is defined using the add_spkr
+command, and there must be a matching number of speaker definitions as the
+specified speaker count. The definitions are ended with a "/}".
+
+add_spkr <id:string> <dist:float> <azi:float> <elev:float> <connection:string>
+Defines an output speaker. The ID is a string identifier for the output speaker
+(see Speaker IDs below). The distance is in meters from the center-point of the
+physical speaker array. The azimuth is the horizontal angle of the speaker, in
+degrees, where 0 is directly front and positive values go left. The elevation
+is the vertical angle of the speaker, in degrees, where 0 is directly front and
+positive goes upward. The connection string is the JACK port name the speaker
+should connect to. Currently, OpenAL Soft uses the ID and distance, and ignores
+the rest.
+
+/lfmatrix/{
+Begins the low-frequency decoder matrix definition. The definition should
+include an order_gain command to specify the base gain for the ambisonic
+orders. Each matrix row is defined using the add_row command, and there must be
+a matching number of rows as the number of speakers. Additionally the row
+definitions are in the same order as the speaker definitions. The definitions
+are ended with a "/}". Only valid for dual-band decoders.
+
+/hfmatrix/{
+Begins the high-frequency decoder matrix definition. The definition should
+include an order_gain command to specify the base gain for the ambisonic
+orders. Each matrix row is defined using the add_row command, and there must be
+a matching number of rows as the number of speakers, Additionally the row
+definitions are in the same order as the speaker definitions. The definitions
+are ended with a "/}". Only valid for dual-band decoders.
+
+/matrix/{
+Begins the decoder matrix definition. The definition should include an
+order_gain command to specify the base gain for the ambisonic orders. Each
+matrix row is defined using the add_row command, and there must be a matching
+number of rows as the number of speakers. Additionally the row definitions are
+in the same order as the speaker definitions. The definitions are ended with a
+"/}". Only valid for single-band decoders.
+
+order_gain <gain:float> <gain:float> <gain:float> <gain:float>
+Specifies the base gain for the zeroth-, first-, second-, and third-order
+coefficients in the given matrix, automatically scaling the related
+coefficients. This should be specified at the beginning of the matrix
+definition.
+
+add_row <coefficient:float> ...
+Specifies a row of coefficients for the matrix. There should be one coefficient
+for each enabled bit in the channel mask, and corresponds to the matching ACN
+channel.
+
+/end
+Marks the end of the configuration file.
+
+
+Speaker IDs
+===========
+
+The AmbDec program uses the speaker ID as a label to display in its config
+dialog, but does not otherwise use it for any particular purpose. However,
+since OpenAL Soft needs to match a speaker definition to an output channel, the
+speaker ID is used to identify what output channel it correspond to. Therefore,
+OpenAL Soft requires these channel labels to be recognized:
+
+LF = Front left
+RF = Front right
+LS = Side left
+RS = Side right
+LB = Back left
+RB = Back right
+CE = Front center
+CB = Back center
+LFT = Top front left
+RFT = Top front right
+LBT = Top back left
+RBT = Top back right
+
+Additionally, configuration files for surround51 will acknowledge back speakers
+for side channels, to avoid issues with a configuration expecting 5.1 to use
+the side channels when the device is configured for back, or vice-versa.
+
+Furthermore, OpenAL Soft does not require a speaker definition for each output
+channel the configuration is used with. So for example a 5.1 configuration may
+omit a front center speaker definition, in which case the front center output
+channel will not contribute to the ambisonic decode (though OpenAL Soft will
+still use it in certain scenarios, such as the AL_EFFECT_DEDICATED_DIALOGUE
+effect).
+
+
+Creating Configuration Files
+============================
+
+Configuration files can be created or modified by hand in a text editor. The
+AmbDec program also has a GUI for creating and editing them. However, these
+methods rely on you having the coefficients to fill in... they won't be
+generated for you.
+
+Another option is to use the Ambisonic Decoder Toolbox
+<https://bitbucket.org/ambidecodertoolbox/adt.git>. This is a collection of
+MATLAB and GNU Octave scripts that can generate AmbDec configuration files from
+an array of speaker definitions (labels and positions). If you're familiar with
+using MATLAB or GNU Octave, this may be a good option.
+
+There are plans for OpenAL Soft to include a utility to generate coefficients
+and make configuration files. However, calculating proper coefficients for
+anything other than regular or semi-regular speaker setups is somewhat of a
+black art, so may take some time.
diff --git a/docs/ambisonics.txt b/docs/ambisonics.txt
new file mode 100644 (file)
index 0000000..b1b111d
--- /dev/null
@@ -0,0 +1,112 @@
+OpenAL Soft's renderer has advanced quite a bit since its start with panned
+stereo output. Among these advancements is support for surround sound output,
+using psychoacoustic modeling and more accurate plane wave reconstruction. The
+concepts in use may not be immediately obvious to people just getting into 3D
+audio, or people who only have more indirect experience through the use of 3D
+audio APIs, so this document aims to introduce the ideas and purpose of
+Ambisonics as used by OpenAL Soft.
+
+
+What Is It?
+===========
+
+Originally developed in the 1970s by Michael Gerzon and a team others,
+Ambisonics was created as a means of recording and playing back 3D sound.
+Taking advantage of the way sound waves propogate, it is possible to record a
+fully 3D soundfield using as few as 4 channels (or even just 3, if you don't
+mind dropping down to 2 dimensions like many surround sound systems are). This
+representation is called B-Format. It was designed to handle audio independent
+of any specific speaker layout, so with a proper decoder the same recording can
+be played back on a variety of speaker setups, from quadraphonic and hexagonal
+to cubic and other periphonic (with height) layouts.
+
+Although it was developed decades ago, various factors held ambisonics back
+from really taking hold in the consumer market. However, given the solid
+theories backing it, as well as the potential and practical benefits on offer,
+it continued to be a topic of research over the years, with improvements being
+made over the original design. One of the improvements made is the use of
+Spherical Harmonics to increase the number of channels for greater spatial
+definition. Where the original 4-channel design is termed as "First-Order
+Ambisonics", or FOA, the increased channel count through the use of Spherical
+Harmonics is termed as "Higher-Order Ambisonics", or HOA. The details of higher
+order ambisonics are out of the scope of this document, but know that the added
+channels are still independent of any speaker layout, and aim to further
+improve the spatial detail for playback.
+
+Today, the processing power available on even low-end computers means real-time
+Ambisonics processing is possible. Not only can decoders be implemented in
+software, but so can encoders, synthesizing a soundfield using multiple panned
+sources, thus taking advantage of what ambisonics offers in a virtual audio
+environment.
+
+
+How Does It Help?
+=================
+
+Positional sound has come a long way from pan-pot stereo (aka pair-wise).
+Although useful at the time, the issues became readily apparent when trying to
+extend it for surround sound. Pan-pot doesn't work as well for depth (front-
+back) or vertical panning, it has a rather small "sweet spot" (the area the
+head needs to be in to perceive the sound in its intended direction), and it
+misses key distance-related details of sound waves.
+
+Ambisonics takes a different approach. It uses all available speakers to help
+localize a sound, and it also takes into account how the brain localizes low
+frequency sounds compared to high frequency ones -- a so-called psychoacoustic
+model. It may seem counter-intuitive (if a sound is coming from the front-left,
+surely just play it on the front-left speaker?), but to properly model a sound
+coming from where a speaker doesn't exist, more needs to be done to construct a
+proper sound wave that's perceived to come from the intended direction. Doing
+this creates a larger sweet spot, allowing the perceived sound direction to
+remain correct over a larger area around the center of the speakers.
+
+In addition, Ambisonics can encode the near-field effect of sounds, effectively
+capturing the sound distance. The near-field effect is a subtle low-frequency
+boost as a result of wave-front curvature, and properly compensating for this
+occuring with the output speakers (as well as emulating it with a synthesized
+soundfield) can create an improved sense of distance for sounds that move near
+or far.
+
+
+How Is It Used?
+===============
+
+As a 3D audio API, OpenAL is tasked with playing 3D sound as best it can with
+the speaker setup the user has. Since the OpenAL API doesn't expose discrete
+playback speaker feeds, an implementation has a lot of leeway with how to deal
+with the audio before it's played back for the user to hear. Consequently,
+OpenAL Soft (or any other OpenAL implementation that wishes to) can render
+using Ambisonics and decode the ambisonic mix for a high level of accuracy over
+what simple pan-pot could provide.
+
+In addition to surround sound output, Ambisonics also has benefits with stereo
+output. 2-channel UHJ is a stereo-compatible format that encodes some surround
+sound information using a wide-band 90-degree phase shift filter. This is
+generated by taking the ambisonic mix and deriving a front-stereo mix with
+with the rear sounds filtered in with it. Although the result is not as good as
+3-channel (2D) B-Format, it has the distinct advantage of only using 2 channels
+and being compatible with stereo output. This means it will sound just fine
+when played as-is through a normal stereo device, or it may optionally be fed
+to a properly configured surround sound receiver which can extract the encoded
+information and restore some of the original surround sound signal.
+
+
+What Are Its Limitations?
+=========================
+
+As good as Ambisonics is, it's not a magic bullet that can overcome all
+problems. One of the bigger issues it has is dealing with irregular speaker
+setups, such as 5.1 surround sound. The problem mainly lies in the imbalanced
+speaker positioning -- there are three speakers within the front 60-degree area
+(meaning only 30-degree gaps in between each of the three speakers), while only
+two speakers cover the back 140-degree area, leaving 80-degree gaps on the
+sides. It should be noted that this problem is inherent to the speaker layout
+itself; there isn't much that can be done to get an optimal surround sound
+response, with ambisonics or not. It will do the best it can, but there are
+trade-offs between detail and accuracy.
+
+Another issue lies with HRTF. While it's certainly possible to play an
+ambisonic mix using HRTF and retain a sense of 3D sound, doing so with a high
+degree of spatial detail requires a fair amount of resources, in both memory
+and processing time. And even with it, mixing sounds with HRTF directly will
+still be better for positional accuracy.
diff --git a/docs/env-vars.txt b/docs/env-vars.txt
new file mode 100644 (file)
index 0000000..815a309
--- /dev/null
@@ -0,0 +1,89 @@
+Useful Environment Variables
+
+Below is a list of environment variables that can be set to aid with running or
+debugging apps that use OpenAL Soft. They should be set before the app is run.
+
+*** Logging ***
+
+ALSOFT_LOGLEVEL
+Specifies the amount of logging OpenAL Soft will write out:
+0 - Effectively disables all logging
+1 - Prints out errors only
+2 - Prints out warnings and errors
+3 - Prints out additional information, as well as warnings and errors
+
+ALSOFT_LOGFILE
+Specifies a filename that logged output will be written to. Note that the file
+will be first cleared when logging is initialized.
+
+*** Overrides ***
+
+ALSOFT_CONF
+Specifies an additional configuration file to load settings from. These
+settings will take precedence over the global and user configs, but not other
+environment variable settings.
+
+ALSOFT_DRIVERS
+Overrides the drivers config option. This specifies which backend drivers to
+consider or not consider for use. Please see the drivers option in
+alsoftrc.sample for a list of available drivers.
+
+ALSOFT_DEFAULT_REVERB
+Specifies the default reverb preset to apply to sources. Please see the
+default-reverb option in alsoftrc.sample for additional information and a list
+of available presets.
+
+ALSOFT_TRAP_AL_ERROR
+Set to "true" or "1" to force trapping AL errors. Like the trap-al-error config
+option, this will raise a SIGTRAP signal (or a breakpoint exception under
+Windows) when a context-level error is generated. Useful when run under a
+debugger as it will break execution right when the error occurs, making it
+easier to track the cause.
+
+ALSOFT_TRAP_ALC_ERROR
+Set to "true" or "1" to force trapping ALC errors. Like the trap-alc-error
+config option, this will raise a SIGTRAP signal (or a breakpoint exception
+under Windows) when a device-level error is generated. Useful when run under a
+debugger as it will break execution right when the error occurs, making it
+easier to track the cause.
+
+ALSOFT_TRAP_ERROR
+Set to "true" or "1" to force trapping both ALC and AL errors.
+
+*** Compatibility ***
+
+__ALSOFT_HALF_ANGLE_CONES
+Older versions of OpenAL Soft incorrectly calculated the cone angles to range
+between 0 and 180 degrees, instead of the expected range of 0 to 360 degrees.
+Setting this to "true" or "1" restores the old buggy behavior, for apps that
+were written to expect the incorrect range.
+
+__ALSOFT_ENABLE_SUB_DATA_EXT
+The more widely used AL_EXT_SOURCE_RADIUS extension is incompatible with the
+now-defunct AL_SOFT_buffer_sub_data extension. Setting this to "true" or "1"
+restores the AL_SOFT_buffer_sub_data extension for apps that require it,
+disabling AL_EXT_SOURCE_RADIUS.
+
+__ALSOFT_REVERSE_Z
+Applications that don't natively use OpenAL's coordinate system have to convert
+to it before passing in 3D coordinates. Depending on how exactly this is done,
+it can cause correct output for stereo but incorrect Z panning for surround
+sound (i.e., sounds that are supposed to be behind you sound like they're in
+front, and vice-versa). Setting this to "true" or "1" will negate the localized
+Z coordinate to flip front/back panning for 3D sources.
+
+__ALSOFT_REVERSE_Y
+Same as for __ALSOFT_REVERSE_Z, but for Y (up/down) panning.
+
+__ALSOFT_REVERSE_X
+Same as for __ALSOFT_REVERSE_Z, but for X (left/right) panning.
+
+__ALSOFT_SUSPEND_CONTEXT
+Due to the OpenAL spec not being very clear about them, behavior of the
+alcSuspendContext and alcProcessContext methods has varied, and because of
+that, previous versions of OpenAL Soft had them no-op. Creative's hardware
+drivers and the Rapture3D driver, however, use these methods to batch changes,
+which some applications make use of to protect against partial updates. In an
+attempt to standardize on that behavior, OpenAL Soft has changed those methods
+accordingly. Setting this to "ignore" restores the previous no-op behavior for
+applications that interact poorly with the new behavior.
diff --git a/docs/hrtf.txt b/docs/hrtf.txt
new file mode 100644 (file)
index 0000000..0ea27ca
--- /dev/null
@@ -0,0 +1,81 @@
+HRTF Support
+============
+
+Starting with OpenAL Soft 1.14, HRTFs can be used to enable enhanced
+spatialization for both 3D (mono) and multi-channel sources, when used with
+headphones/stereo output. This can be enabled using the 'hrtf' config option.
+
+For multi-channel sources this creates a virtual speaker effect, making it
+sound as if speakers provide a discrete position for each channel around the
+listener. For mono sources this provides much more versatility in the perceived
+placement of sounds, making it seem as though they are coming from all around,
+including above and below the listener, instead of just to the front, back, and
+sides.
+
+The default data set is based on the KEMAR HRTF data provided by MIT, which can
+be found at <http://sound.media.mit.edu/resources/KEMAR.html>.
+
+
+Custom HRTF Data Sets
+=====================
+
+OpenAL Soft also provides an option to use user-specified data sets, in
+addition to or in place of the default set. This allows users to provide data
+sets that could be better suited for their heads, or to work with stereo
+speakers instead of headphones, for example.
+
+The file format is specified below. It uses little-endian byte order.
+
+==
+ALchar   magic[8] = "MinPHR03";
+ALuint   sampleRate;
+ALubyte  channelType; /* Can be 0 (mono) or 1 (stereo). */
+ALubyte  hrirSize;    /* Can be 8 to 128 in steps of 8. */
+ALubyte  fdCount;     /* Can be 1 to 16. */
+
+struct {
+    ALushort distance;        /* Can be 50mm to 2500mm. */
+    ALubyte evCount;          /* Can be 5 to 128. */
+    ALubyte azCount[evCount]; /* Each can be 1 to 128. */
+} fields[fdCount];
+
+/* NOTE: ALbyte3 is a packed 24-bit sample type,
+ * hrirCount is the sum of all azCounts.
+ * channels can be 1 (mono) or 2 (stereo) depending on channelType.
+ */
+ALbyte3 coefficients[hrirCount][hrirSize][channels];
+ALubyte delays[hrirCount][channels]; /* Each can be 0 to 63. */
+==
+
+The data layout is as follows:
+
+The file first starts with the 8-byte marker, "MinPHR03", to identify it as an
+HRTF data set. This is followed by an unsigned 32-bit integer, specifying the
+sample rate the data set is designed for (OpenAL Soft will resample the HRIRs
+if the output device's playback rate doesn't match).
+
+Afterward, an unsigned 8-bit integer specifies the channel type, which can be 0
+(mono, single-channel) or 1 (stereo, dual-channel). After this is another 8-bit
+integer which specifies how many sample points (or finite impulse response
+filter coefficients) make up each HRIR.
+
+The following unsigned 8-bit integer specifies the number of fields used by the
+data set, which must be in descending order (farthest first, closest last).
+Then for each field an unsigned 16-bit short specifies the distance for that
+field in millimeters, followed by an 8-bit integer for the number of
+elevations.  These elevations start at the bottom (-90 degrees), and increment
+upwards.  Following this is an array of unsigned 8-bit integers, one for each
+elevation which specifies the number of azimuths (and thus HRIRs) that make up
+each elevation.  Azimuths start clockwise from the front, constructing a full
+circle.  Mono HRTFs use the same HRIRs for both ears by reversing the azimuth
+calculation (ie. left = angle, right = 360-angle).
+
+The actual coefficients follow. Each coefficient is a signed 24-bit sample.
+Stereo HRTFs interleave left/right ear coefficients.  The HRIRs must be
+minimum-phase.  This allows the use of a smaller filter length, reducing
+computation.
+
+After the coefficients is an array of unsigned 8-bit delay values as 6.2 fixed-
+point integers, one for each HRIR (with stereo HRTFs interleaving left/right
+ear delays). This is the propagation delay in samples a signal must wait before
+being convolved with the corresponding minimum-phase HRIR filter.
diff --git a/examples/alconvolve.c b/examples/alconvolve.c
new file mode 100644 (file)
index 0000000..93fd2eb
--- /dev/null
@@ -0,0 +1,594 @@
+/*
+ * OpenAL Convolution Reverb Example
+ *
+ * Copyright (c) 2020 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains an example for applying convolution reverb to a source. */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+
+#ifndef AL_SOFT_convolution_reverb
+#define AL_SOFT_convolution_reverb
+#define AL_EFFECT_CONVOLUTION_REVERB_SOFT        0xA000
+#endif
+
+
+/* Filter object functions */
+static LPALGENFILTERS alGenFilters;
+static LPALDELETEFILTERS alDeleteFilters;
+static LPALISFILTER alIsFilter;
+static LPALFILTERI alFilteri;
+static LPALFILTERIV alFilteriv;
+static LPALFILTERF alFilterf;
+static LPALFILTERFV alFilterfv;
+static LPALGETFILTERI alGetFilteri;
+static LPALGETFILTERIV alGetFilteriv;
+static LPALGETFILTERF alGetFilterf;
+static LPALGETFILTERFV alGetFilterfv;
+
+/* Effect object functions */
+static LPALGENEFFECTS alGenEffects;
+static LPALDELETEEFFECTS alDeleteEffects;
+static LPALISEFFECT alIsEffect;
+static LPALEFFECTI alEffecti;
+static LPALEFFECTIV alEffectiv;
+static LPALEFFECTF alEffectf;
+static LPALEFFECTFV alEffectfv;
+static LPALGETEFFECTI alGetEffecti;
+static LPALGETEFFECTIV alGetEffectiv;
+static LPALGETEFFECTF alGetEffectf;
+static LPALGETEFFECTFV alGetEffectfv;
+
+/* Auxiliary Effect Slot object functions */
+static LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots;
+static LPALDELETEAUXILIARYEFFECTSLOTS alDeleteAuxiliaryEffectSlots;
+static LPALISAUXILIARYEFFECTSLOT alIsAuxiliaryEffectSlot;
+static LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti;
+static LPALAUXILIARYEFFECTSLOTIV alAuxiliaryEffectSlotiv;
+static LPALAUXILIARYEFFECTSLOTF alAuxiliaryEffectSlotf;
+static LPALAUXILIARYEFFECTSLOTFV alAuxiliaryEffectSlotfv;
+static LPALGETAUXILIARYEFFECTSLOTI alGetAuxiliaryEffectSloti;
+static LPALGETAUXILIARYEFFECTSLOTIV alGetAuxiliaryEffectSlotiv;
+static LPALGETAUXILIARYEFFECTSLOTF alGetAuxiliaryEffectSlotf;
+static LPALGETAUXILIARYEFFECTSLOTFV alGetAuxiliaryEffectSlotfv;
+
+
+/* This stuff defines a simple streaming player object, the same as alstream.c.
+ * Comments are removed for brevity, see alstream.c for more details.
+ */
+#define NUM_BUFFERS 4
+#define BUFFER_SAMPLES 8192
+
+typedef struct StreamPlayer {
+    ALuint buffers[NUM_BUFFERS];
+    ALuint source;
+
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    float *membuf;
+
+    ALenum format;
+} StreamPlayer;
+
+static StreamPlayer *NewPlayer(void)
+{
+    StreamPlayer *player;
+
+    player = calloc(1, sizeof(*player));
+    assert(player != NULL);
+
+    alGenBuffers(NUM_BUFFERS, player->buffers);
+    assert(alGetError() == AL_NO_ERROR && "Could not create buffers");
+
+    alGenSources(1, &player->source);
+    assert(alGetError() == AL_NO_ERROR && "Could not create source");
+
+    alSource3i(player->source, AL_POSITION, 0, 0, -1);
+    alSourcei(player->source, AL_SOURCE_RELATIVE, AL_TRUE);
+    alSourcei(player->source, AL_ROLLOFF_FACTOR, 0);
+    assert(alGetError() == AL_NO_ERROR && "Could not set source parameters");
+
+    return player;
+}
+
+static void ClosePlayerFile(StreamPlayer *player)
+{
+    if(player->sndfile)
+        sf_close(player->sndfile);
+    player->sndfile = NULL;
+
+    free(player->membuf);
+    player->membuf = NULL;
+}
+
+static void DeletePlayer(StreamPlayer *player)
+{
+    ClosePlayerFile(player);
+
+    alDeleteSources(1, &player->source);
+    alDeleteBuffers(NUM_BUFFERS, player->buffers);
+    if(alGetError() != AL_NO_ERROR)
+        fprintf(stderr, "Failed to delete object IDs\n");
+
+    memset(player, 0, sizeof(*player));
+    free(player);
+}
+
+static int OpenPlayerFile(StreamPlayer *player, const char *filename)
+{
+    size_t frame_size;
+
+    ClosePlayerFile(player);
+
+    player->sndfile = sf_open(filename, SFM_READ, &player->sfinfo);
+    if(!player->sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(NULL));
+        return 0;
+    }
+
+    player->format = AL_NONE;
+    if(player->sfinfo.channels == 1)
+        player->format = AL_FORMAT_MONO_FLOAT32;
+    else if(player->sfinfo.channels == 2)
+        player->format = AL_FORMAT_STEREO_FLOAT32;
+    else if(player->sfinfo.channels == 6)
+        player->format = AL_FORMAT_51CHN32;
+    else if(player->sfinfo.channels == 3)
+    {
+        if(sf_command(player->sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            player->format = AL_FORMAT_BFORMAT2D_FLOAT32;
+    }
+    else if(player->sfinfo.channels == 4)
+    {
+        if(sf_command(player->sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            player->format = AL_FORMAT_BFORMAT3D_FLOAT32;
+    }
+    if(!player->format)
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", player->sfinfo.channels);
+        sf_close(player->sndfile);
+        player->sndfile = NULL;
+        return 0;
+    }
+
+    frame_size = (size_t)(BUFFER_SAMPLES * player->sfinfo.channels) * sizeof(float);
+    player->membuf = malloc(frame_size);
+
+    return 1;
+}
+
+static int StartPlayer(StreamPlayer *player)
+{
+    ALsizei i;
+
+    alSourceRewind(player->source);
+    alSourcei(player->source, AL_BUFFER, 0);
+
+    for(i = 0;i < NUM_BUFFERS;i++)
+    {
+        sf_count_t slen = sf_readf_float(player->sndfile, player->membuf, BUFFER_SAMPLES);
+        if(slen < 1) break;
+
+        slen *= player->sfinfo.channels * (sf_count_t)sizeof(float);
+        alBufferData(player->buffers[i], player->format, player->membuf, (ALsizei)slen,
+            player->sfinfo.samplerate);
+    }
+    if(alGetError() != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Error buffering for playback\n");
+        return 0;
+    }
+
+    alSourceQueueBuffers(player->source, i, player->buffers);
+    alSourcePlay(player->source);
+    if(alGetError() != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Error starting playback\n");
+        return 0;
+    }
+
+    return 1;
+}
+
+static int UpdatePlayer(StreamPlayer *player)
+{
+    ALint processed, state;
+
+    alGetSourcei(player->source, AL_SOURCE_STATE, &state);
+    alGetSourcei(player->source, AL_BUFFERS_PROCESSED, &processed);
+    if(alGetError() != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Error checking source state\n");
+        return 0;
+    }
+
+    while(processed > 0)
+    {
+        ALuint bufid;
+        sf_count_t slen;
+
+        alSourceUnqueueBuffers(player->source, 1, &bufid);
+        processed--;
+
+        slen = sf_readf_float(player->sndfile, player->membuf, BUFFER_SAMPLES);
+        if(slen > 0)
+        {
+            slen *= player->sfinfo.channels * (sf_count_t)sizeof(float);
+            alBufferData(bufid, player->format, player->membuf, (ALsizei)slen,
+                player->sfinfo.samplerate);
+            alSourceQueueBuffers(player->source, 1, &bufid);
+        }
+        if(alGetError() != AL_NO_ERROR)
+        {
+            fprintf(stderr, "Error buffering data\n");
+            return 0;
+        }
+    }
+
+    if(state != AL_PLAYING && state != AL_PAUSED)
+    {
+        ALint queued;
+
+        alGetSourcei(player->source, AL_BUFFERS_QUEUED, &queued);
+        if(queued == 0)
+            return 0;
+
+        alSourcePlay(player->source);
+        if(alGetError() != AL_NO_ERROR)
+        {
+            fprintf(stderr, "Error restarting playback\n");
+            return 0;
+        }
+    }
+
+    return 1;
+}
+
+
+/* CreateEffect creates a new OpenAL effect object with a convolution reverb
+ * type, and returns the new effect ID.
+ */
+static ALuint CreateEffect(void)
+{
+    ALuint effect = 0;
+    ALenum err;
+
+    printf("Using Convolution Reverb\n");
+
+    /* Create the effect object and set the convolution reverb effect type. */
+    alGenEffects(1, &effect);
+    alEffecti(effect, AL_EFFECT_TYPE, AL_EFFECT_CONVOLUTION_REVERB_SOFT);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL error: %s\n", alGetString(err));
+        if(alIsEffect(effect))
+            alDeleteEffects(1, &effect);
+        return 0;
+    }
+
+    return effect;
+}
+
+/* LoadBuffer loads the named audio file into an OpenAL buffer object, and
+ * returns the new buffer ID.
+ */
+static ALuint LoadSound(const char *filename)
+{
+    const char *namepart;
+    ALenum err, format;
+    ALuint buffer;
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    float *membuf;
+    sf_count_t num_frames;
+    ALsizei num_bytes;
+
+    /* Open the audio file and check that it's usable. */
+    sndfile = sf_open(filename, SFM_READ, &sfinfo);
+    if(!sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(sndfile));
+        return 0;
+    }
+    if(sfinfo.frames < 1 || sfinfo.frames > (sf_count_t)(INT_MAX/sizeof(float))/sfinfo.channels)
+    {
+        fprintf(stderr, "Bad sample count in %s (%" PRId64 ")\n", filename, sfinfo.frames);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Get the sound format, and figure out the OpenAL format. Use floats since
+     * impulse responses will usually have more than 16-bit precision.
+     */
+    format = AL_NONE;
+    if(sfinfo.channels == 1)
+        format = AL_FORMAT_MONO_FLOAT32;
+    else if(sfinfo.channels == 2)
+        format = AL_FORMAT_STEREO_FLOAT32;
+    else if(sfinfo.channels == 3)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT2D_FLOAT32;
+    }
+    else if(sfinfo.channels == 4)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT3D_FLOAT32;
+    }
+    if(!format)
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", sfinfo.channels);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    namepart = strrchr(filename, '/');
+    if(namepart || (namepart=strrchr(filename, '\\')))
+        namepart++;
+    else
+        namepart = filename;
+    printf("Loading: %s (%s, %dhz, %" PRId64 " samples / %.2f seconds)\n", namepart,
+        FormatName(format), sfinfo.samplerate, sfinfo.frames,
+        (double)sfinfo.frames / sfinfo.samplerate);
+    fflush(stdout);
+
+    /* Decode the whole audio file to a buffer. */
+    membuf = malloc((size_t)(sfinfo.frames * sfinfo.channels) * sizeof(float));
+
+    num_frames = sf_readf_float(sndfile, membuf, sfinfo.frames);
+    if(num_frames < 1)
+    {
+        free(membuf);
+        sf_close(sndfile);
+        fprintf(stderr, "Failed to read samples in %s (%" PRId64 ")\n", filename, num_frames);
+        return 0;
+    }
+    num_bytes = (ALsizei)(num_frames * sfinfo.channels) * (ALsizei)sizeof(float);
+
+    /* Buffer the audio data into a new buffer object, then free the data and
+     * close the file.
+     */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    alBufferData(buffer, format, membuf, num_bytes, sfinfo.samplerate);
+
+    free(membuf);
+    sf_close(sndfile);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(buffer && alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+int main(int argc, char **argv)
+{
+    ALuint ir_buffer, filter, effect, slot;
+    StreamPlayer *player;
+    int i;
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name>] <impulse response file> "
+            "<[-dry | -nodry] filename>...\n", argv[0]);
+        return 1;
+    }
+
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    if(!alIsExtensionPresent("AL_SOFTX_convolution_reverb"))
+    {
+        CloseAL();
+        fprintf(stderr, "Error: Convolution revern not supported\n");
+        return 1;
+    }
+
+    if(argc < 2)
+    {
+        CloseAL();
+        fprintf(stderr, "Error: Missing impulse response or sound files\n");
+        return 1;
+    }
+
+    /* Define a macro to help load the function pointers. */
+#define LOAD_PROC(T, x)  ((x) = FUNCTION_CAST(T, alGetProcAddress(#x)))
+    LOAD_PROC(LPALGENFILTERS, alGenFilters);
+    LOAD_PROC(LPALDELETEFILTERS, alDeleteFilters);
+    LOAD_PROC(LPALISFILTER, alIsFilter);
+    LOAD_PROC(LPALFILTERI, alFilteri);
+    LOAD_PROC(LPALFILTERIV, alFilteriv);
+    LOAD_PROC(LPALFILTERF, alFilterf);
+    LOAD_PROC(LPALFILTERFV, alFilterfv);
+    LOAD_PROC(LPALGETFILTERI, alGetFilteri);
+    LOAD_PROC(LPALGETFILTERIV, alGetFilteriv);
+    LOAD_PROC(LPALGETFILTERF, alGetFilterf);
+    LOAD_PROC(LPALGETFILTERFV, alGetFilterfv);
+
+    LOAD_PROC(LPALGENEFFECTS, alGenEffects);
+    LOAD_PROC(LPALDELETEEFFECTS, alDeleteEffects);
+    LOAD_PROC(LPALISEFFECT, alIsEffect);
+    LOAD_PROC(LPALEFFECTI, alEffecti);
+    LOAD_PROC(LPALEFFECTIV, alEffectiv);
+    LOAD_PROC(LPALEFFECTF, alEffectf);
+    LOAD_PROC(LPALEFFECTFV, alEffectfv);
+    LOAD_PROC(LPALGETEFFECTI, alGetEffecti);
+    LOAD_PROC(LPALGETEFFECTIV, alGetEffectiv);
+    LOAD_PROC(LPALGETEFFECTF, alGetEffectf);
+    LOAD_PROC(LPALGETEFFECTFV, alGetEffectfv);
+
+    LOAD_PROC(LPALGENAUXILIARYEFFECTSLOTS, alGenAuxiliaryEffectSlots);
+    LOAD_PROC(LPALDELETEAUXILIARYEFFECTSLOTS, alDeleteAuxiliaryEffectSlots);
+    LOAD_PROC(LPALISAUXILIARYEFFECTSLOT, alIsAuxiliaryEffectSlot);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTI, alAuxiliaryEffectSloti);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTIV, alAuxiliaryEffectSlotiv);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTF, alAuxiliaryEffectSlotf);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTFV, alAuxiliaryEffectSlotfv);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTI, alGetAuxiliaryEffectSloti);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTIV, alGetAuxiliaryEffectSlotiv);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTF, alGetAuxiliaryEffectSlotf);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTFV, alGetAuxiliaryEffectSlotfv);
+#undef LOAD_PROC
+
+    /* Load the reverb into an effect. */
+    effect = CreateEffect();
+    if(!effect)
+    {
+        CloseAL();
+        return 1;
+    }
+
+    /* Load the impulse response sound into a buffer. */
+    ir_buffer = LoadSound(argv[0]);
+    if(!ir_buffer)
+    {
+        alDeleteEffects(1, &effect);
+        CloseAL();
+        return 1;
+    }
+
+    /* Create the effect slot object. This is what "plays" an effect on sources
+     * that connect to it.
+     */
+    slot = 0;
+    alGenAuxiliaryEffectSlots(1, &slot);
+
+    /* Set the impulse response sound buffer on the effect slot. This allows
+     * effects to access it as needed. In this case, convolution reverb uses it
+     * as the filter source. NOTE: Unlike the effect object, the buffer *is*
+     * kept referenced and may not be changed or deleted as long as it's set,
+     * just like with a source. When another buffer is set, or the effect slot
+     * is deleted, the buffer reference is released.
+     *
+     * The effect slot's gain is reduced because the impulse responses I've
+     * tested with result in excessively loud reverb. Is that normal? Even with
+     * this, it seems a bit on the loud side.
+     *
+     * Also note: unlike standard or EAX reverb, there is no automatic
+     * attenuation of a source's reverb response with distance, so the reverb
+     * will remain full volume regardless of a given sound's distance from the
+     * listener. You can use a send filter to alter a given source's
+     * contribution to reverb.
+     */
+    alAuxiliaryEffectSloti(slot, AL_BUFFER, (ALint)ir_buffer);
+    alAuxiliaryEffectSlotf(slot, AL_EFFECTSLOT_GAIN, 1.0f / 16.0f);
+    alAuxiliaryEffectSloti(slot, AL_EFFECTSLOT_EFFECT, (ALint)effect);
+    assert(alGetError()==AL_NO_ERROR && "Failed to set effect slot");
+
+    /* Create a filter that can silence the dry path. */
+    filter = 0;
+    alGenFilters(1, &filter);
+    alFilteri(filter, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
+    alFilterf(filter, AL_LOWPASS_GAIN, 0.0f);
+
+    player = NewPlayer();
+    /* Connect the player's source to the effect slot. */
+    alSource3i(player->source, AL_AUXILIARY_SEND_FILTER, (ALint)slot, 0, AL_FILTER_NULL);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Play each file listed on the command line */
+    for(i = 1;i < argc;i++)
+    {
+        const char *namepart;
+
+        if(argc-i > 1)
+        {
+            if(strcasecmp(argv[i], "-nodry") == 0)
+            {
+                alSourcei(player->source, AL_DIRECT_FILTER, (ALint)filter);
+                ++i;
+            }
+            else if(strcasecmp(argv[i], "-dry") == 0)
+            {
+                alSourcei(player->source, AL_DIRECT_FILTER, AL_FILTER_NULL);
+                ++i;
+            }
+        }
+
+        if(!OpenPlayerFile(player, argv[i]))
+            continue;
+
+        namepart = strrchr(argv[i], '/');
+        if(namepart || (namepart=strrchr(argv[i], '\\')))
+            namepart++;
+        else
+            namepart = argv[i];
+
+        printf("Playing: %s (%s, %dhz)\n", namepart, FormatName(player->format),
+            player->sfinfo.samplerate);
+        fflush(stdout);
+
+        if(!StartPlayer(player))
+        {
+            ClosePlayerFile(player);
+            continue;
+        }
+
+        while(UpdatePlayer(player))
+            al_nssleep(10000000);
+
+        ClosePlayerFile(player);
+    }
+    printf("Done.\n");
+
+    /* All files done. Delete the player and effect resources, and close down
+     * OpenAL.
+     */
+    DeletePlayer(player);
+    player = NULL;
+
+    alDeleteAuxiliaryEffectSlots(1, &slot);
+    alDeleteEffects(1, &effect);
+    alDeleteFilters(1, &filter);
+    alDeleteBuffers(1, &ir_buffer);
+
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/alffplay.cpp b/examples/alffplay.cpp
new file mode 100644 (file)
index 0000000..ae40a51
--- /dev/null
@@ -0,0 +1,2181 @@
+/*
+ * An example showing how to play a stream sync'd to video, using ffmpeg.
+ *
+ * Requires C++14.
+ */
+
+#include <condition_variable>
+#include <functional>
+#include <algorithm>
+#include <iostream>
+#include <utility>
+#include <iomanip>
+#include <cstdint>
+#include <cstring>
+#include <cstdlib>
+#include <atomic>
+#include <cerrno>
+#include <chrono>
+#include <cstdio>
+#include <future>
+#include <memory>
+#include <string>
+#include <thread>
+#include <vector>
+#include <array>
+#include <cmath>
+#include <deque>
+#include <mutex>
+#include <ratio>
+
+#ifdef __GNUC__
+_Pragma("GCC diagnostic push")
+_Pragma("GCC diagnostic ignored \"-Wconversion\"")
+_Pragma("GCC diagnostic ignored \"-Wold-style-cast\"")
+#endif
+extern "C" {
+#include "libavcodec/avcodec.h"
+#include "libavformat/avformat.h"
+#include "libavformat/avio.h"
+#include "libavformat/version.h"
+#include "libavutil/avutil.h"
+#include "libavutil/error.h"
+#include "libavutil/frame.h"
+#include "libavutil/mem.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+#include "libavutil/samplefmt.h"
+#include "libavutil/time.h"
+#include "libavutil/version.h"
+#include "libavutil/channel_layout.h"
+#include "libswscale/swscale.h"
+#include "libswresample/swresample.h"
+
+constexpr auto AVNoPtsValue = AV_NOPTS_VALUE;
+constexpr auto AVErrorEOF = AVERROR_EOF;
+
+struct SwsContext;
+}
+
+#define SDL_MAIN_HANDLED
+#include "SDL.h"
+#ifdef __GNUC__
+_Pragma("GCC diagnostic pop")
+#endif
+
+#include "AL/alc.h"
+#include "AL/al.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+
+namespace {
+
+inline constexpr int64_t operator "" _i64(unsigned long long int n) noexcept { return static_cast<int64_t>(n); }
+
+#ifndef M_PI
+#define M_PI (3.14159265358979323846)
+#endif
+
+using fixed32 = std::chrono::duration<int64_t,std::ratio<1,(1_i64<<32)>>;
+using nanoseconds = std::chrono::nanoseconds;
+using microseconds = std::chrono::microseconds;
+using milliseconds = std::chrono::milliseconds;
+using seconds = std::chrono::seconds;
+using seconds_d64 = std::chrono::duration<double>;
+using std::chrono::duration_cast;
+
+const std::string AppName{"alffplay"};
+
+ALenum DirectOutMode{AL_FALSE};
+bool EnableWideStereo{false};
+bool EnableUhj{false};
+bool EnableSuperStereo{false};
+bool DisableVideo{false};
+LPALGETSOURCEI64VSOFT alGetSourcei64vSOFT;
+LPALCGETINTEGER64VSOFT alcGetInteger64vSOFT;
+LPALEVENTCONTROLSOFT alEventControlSOFT;
+LPALEVENTCALLBACKSOFT alEventCallbackSOFT;
+
+LPALBUFFERCALLBACKSOFT alBufferCallbackSOFT;
+
+const seconds AVNoSyncThreshold{10};
+
+#define VIDEO_PICTURE_QUEUE_SIZE 24
+
+const seconds_d64 AudioSyncThreshold{0.03};
+const milliseconds AudioSampleCorrectionMax{50};
+/* Averaging filter coefficient for audio sync. */
+#define AUDIO_DIFF_AVG_NB 20
+const double AudioAvgFilterCoeff{std::pow(0.01, 1.0/AUDIO_DIFF_AVG_NB)};
+/* Per-buffer size, in time */
+constexpr milliseconds AudioBufferTime{20};
+/* Buffer total size, in time (should be divisible by the buffer time) */
+constexpr milliseconds AudioBufferTotalTime{800};
+constexpr auto AudioBufferCount = AudioBufferTotalTime / AudioBufferTime;
+
+enum {
+    FF_MOVIE_DONE_EVENT = SDL_USEREVENT
+};
+
+enum class SyncMaster {
+    Audio,
+    Video,
+    External,
+
+    Default = Audio
+};
+
+
+inline microseconds get_avtime()
+{ return microseconds{av_gettime()}; }
+
+/* Define unique_ptrs to auto-cleanup associated ffmpeg objects. */
+struct AVIOContextDeleter {
+    void operator()(AVIOContext *ptr) { avio_closep(&ptr); }
+};
+using AVIOContextPtr = std::unique_ptr<AVIOContext,AVIOContextDeleter>;
+
+struct AVFormatCtxDeleter {
+    void operator()(AVFormatContext *ptr) { avformat_close_input(&ptr); }
+};
+using AVFormatCtxPtr = std::unique_ptr<AVFormatContext,AVFormatCtxDeleter>;
+
+struct AVCodecCtxDeleter {
+    void operator()(AVCodecContext *ptr) { avcodec_free_context(&ptr); }
+};
+using AVCodecCtxPtr = std::unique_ptr<AVCodecContext,AVCodecCtxDeleter>;
+
+struct AVPacketDeleter {
+    void operator()(AVPacket *pkt) { av_packet_free(&pkt); }
+};
+using AVPacketPtr = std::unique_ptr<AVPacket,AVPacketDeleter>;
+
+struct AVFrameDeleter {
+    void operator()(AVFrame *ptr) { av_frame_free(&ptr); }
+};
+using AVFramePtr = std::unique_ptr<AVFrame,AVFrameDeleter>;
+
+struct SwrContextDeleter {
+    void operator()(SwrContext *ptr) { swr_free(&ptr); }
+};
+using SwrContextPtr = std::unique_ptr<SwrContext,SwrContextDeleter>;
+
+struct SwsContextDeleter {
+    void operator()(SwsContext *ptr) { sws_freeContext(ptr); }
+};
+using SwsContextPtr = std::unique_ptr<SwsContext,SwsContextDeleter>;
+
+
+struct ChannelLayout : public AVChannelLayout {
+    ChannelLayout() : AVChannelLayout{} { }
+    ~ChannelLayout() { av_channel_layout_uninit(this); }
+};
+
+
+template<size_t SizeLimit>
+class DataQueue {
+    std::mutex mPacketMutex, mFrameMutex;
+    std::condition_variable mPacketCond;
+    std::condition_variable mInFrameCond, mOutFrameCond;
+
+    std::deque<AVPacketPtr> mPackets;
+    size_t mTotalSize{0};
+    bool mFinished{false};
+
+    AVPacketPtr getPacket()
+    {
+        std::unique_lock<std::mutex> plock{mPacketMutex};
+        while(mPackets.empty() && !mFinished)
+            mPacketCond.wait(plock);
+        if(mPackets.empty())
+            return nullptr;
+
+        auto ret = std::move(mPackets.front());
+        mPackets.pop_front();
+        mTotalSize -= static_cast<unsigned int>(ret->size);
+        return ret;
+    }
+
+public:
+    int sendPacket(AVCodecContext *codecctx)
+    {
+        AVPacketPtr packet{getPacket()};
+
+        int ret{};
+        {
+            std::unique_lock<std::mutex> flock{mFrameMutex};
+            while((ret=avcodec_send_packet(codecctx, packet.get())) == AVERROR(EAGAIN))
+                mInFrameCond.wait_for(flock, milliseconds{50});
+        }
+        mOutFrameCond.notify_one();
+
+        if(!packet)
+        {
+            if(!ret) return AVErrorEOF;
+            std::cerr<< "Failed to send flush packet: "<<ret <<std::endl;
+            return ret;
+        }
+        if(ret < 0)
+            std::cerr<< "Failed to send packet: "<<ret <<std::endl;
+        return ret;
+    }
+
+    int receiveFrame(AVCodecContext *codecctx, AVFrame *frame)
+    {
+        int ret{};
+        {
+            std::unique_lock<std::mutex> flock{mFrameMutex};
+            while((ret=avcodec_receive_frame(codecctx, frame)) == AVERROR(EAGAIN))
+                mOutFrameCond.wait_for(flock, milliseconds{50});
+        }
+        mInFrameCond.notify_one();
+        return ret;
+    }
+
+    void setFinished()
+    {
+        {
+            std::lock_guard<std::mutex> _{mPacketMutex};
+            mFinished = true;
+        }
+        mPacketCond.notify_one();
+    }
+
+    void flush()
+    {
+        {
+            std::lock_guard<std::mutex> _{mPacketMutex};
+            mFinished = true;
+
+            mPackets.clear();
+            mTotalSize = 0;
+        }
+        mPacketCond.notify_one();
+    }
+
+    bool put(const AVPacket *pkt)
+    {
+        {
+            std::unique_lock<std::mutex> lock{mPacketMutex};
+            if(mTotalSize >= SizeLimit || mFinished)
+                return false;
+
+            mPackets.push_back(AVPacketPtr{av_packet_alloc()});
+            if(av_packet_ref(mPackets.back().get(), pkt) != 0)
+            {
+                mPackets.pop_back();
+                return true;
+            }
+
+            mTotalSize += static_cast<unsigned int>(mPackets.back()->size);
+        }
+        mPacketCond.notify_one();
+        return true;
+    }
+};
+
+
+struct MovieState;
+
+struct AudioState {
+    MovieState &mMovie;
+
+    AVStream *mStream{nullptr};
+    AVCodecCtxPtr mCodecCtx;
+
+    DataQueue<2*1024*1024> mQueue;
+
+    /* Used for clock difference average computation */
+    seconds_d64 mClockDiffAvg{0};
+
+    /* Time of the next sample to be buffered */
+    nanoseconds mCurrentPts{0};
+
+    /* Device clock time that the stream started at. */
+    nanoseconds mDeviceStartTime{nanoseconds::min()};
+
+    /* Decompressed sample frame, and swresample context for conversion */
+    AVFramePtr    mDecodedFrame;
+    SwrContextPtr mSwresCtx;
+
+    /* Conversion format, for what gets fed to OpenAL */
+    uint64_t       mDstChanLayout{0};
+    AVSampleFormat mDstSampleFmt{AV_SAMPLE_FMT_NONE};
+
+    /* Storage of converted samples */
+    uint8_t *mSamples{nullptr};
+    int mSamplesLen{0}; /* In samples */
+    int mSamplesPos{0};
+    int mSamplesMax{0};
+
+    std::unique_ptr<uint8_t[]> mBufferData;
+    size_t mBufferDataSize{0};
+    std::atomic<size_t> mReadPos{0};
+    std::atomic<size_t> mWritePos{0};
+
+    /* OpenAL format */
+    ALenum mFormat{AL_NONE};
+    ALuint mFrameSize{0};
+
+    std::mutex mSrcMutex;
+    std::condition_variable mSrcCond;
+    std::atomic_flag mConnected;
+    ALuint mSource{0};
+    std::array<ALuint,AudioBufferCount> mBuffers{};
+    ALuint mBufferIdx{0};
+
+    AudioState(MovieState &movie) : mMovie(movie)
+    { mConnected.test_and_set(std::memory_order_relaxed); }
+    ~AudioState()
+    {
+        if(mSource)
+            alDeleteSources(1, &mSource);
+        if(mBuffers[0])
+            alDeleteBuffers(static_cast<ALsizei>(mBuffers.size()), mBuffers.data());
+
+        av_freep(&mSamples);
+    }
+
+    static void AL_APIENTRY eventCallbackC(ALenum eventType, ALuint object, ALuint param,
+        ALsizei length, const ALchar *message, void *userParam)
+    { static_cast<AudioState*>(userParam)->eventCallback(eventType, object, param, length, message); }
+    void eventCallback(ALenum eventType, ALuint object, ALuint param, ALsizei length,
+        const ALchar *message);
+
+    static ALsizei AL_APIENTRY bufferCallbackC(void *userptr, void *data, ALsizei size)
+    { return static_cast<AudioState*>(userptr)->bufferCallback(data, size); }
+    ALsizei bufferCallback(void *data, ALsizei size);
+
+    nanoseconds getClockNoLock();
+    nanoseconds getClock()
+    {
+        std::lock_guard<std::mutex> lock{mSrcMutex};
+        return getClockNoLock();
+    }
+
+    bool startPlayback();
+
+    int getSync();
+    int decodeFrame();
+    bool readAudio(uint8_t *samples, unsigned int length, int &sample_skip);
+    bool readAudio(int sample_skip);
+
+    int handler();
+};
+
+struct VideoState {
+    MovieState &mMovie;
+
+    AVStream *mStream{nullptr};
+    AVCodecCtxPtr mCodecCtx;
+
+    DataQueue<14*1024*1024> mQueue;
+
+    /* The pts of the currently displayed frame, and the time (av_gettime) it
+     * was last updated - used to have running video pts
+     */
+    nanoseconds mDisplayPts{0};
+    microseconds mDisplayPtsTime{microseconds::min()};
+    std::mutex mDispPtsMutex;
+
+    /* Swscale context for format conversion */
+    SwsContextPtr mSwscaleCtx;
+
+    struct Picture {
+        AVFramePtr mFrame{};
+        nanoseconds mPts{nanoseconds::min()};
+    };
+    std::array<Picture,VIDEO_PICTURE_QUEUE_SIZE> mPictQ;
+    std::atomic<size_t> mPictQRead{0u}, mPictQWrite{1u};
+    std::mutex mPictQMutex;
+    std::condition_variable mPictQCond;
+
+    SDL_Texture *mImage{nullptr};
+    int mWidth{0}, mHeight{0}; /* Full texture size */
+    bool mFirstUpdate{true};
+
+    std::atomic<bool> mEOS{false};
+    std::atomic<bool> mFinalUpdate{false};
+
+    VideoState(MovieState &movie) : mMovie(movie) { }
+    ~VideoState()
+    {
+        if(mImage)
+            SDL_DestroyTexture(mImage);
+        mImage = nullptr;
+    }
+
+    nanoseconds getClock();
+
+    void display(SDL_Window *screen, SDL_Renderer *renderer, AVFrame *frame);
+    void updateVideo(SDL_Window *screen, SDL_Renderer *renderer, bool redraw);
+    int handler();
+};
+
+struct MovieState {
+    AVIOContextPtr mIOContext;
+    AVFormatCtxPtr mFormatCtx;
+
+    SyncMaster mAVSyncType{SyncMaster::Default};
+
+    microseconds mClockBase{microseconds::min()};
+
+    std::atomic<bool> mQuit{false};
+
+    AudioState mAudio;
+    VideoState mVideo;
+
+    std::mutex mStartupMutex;
+    std::condition_variable mStartupCond;
+    bool mStartupDone{false};
+
+    std::thread mParseThread;
+    std::thread mAudioThread;
+    std::thread mVideoThread;
+
+    std::string mFilename;
+
+    MovieState(std::string fname)
+      : mAudio(*this), mVideo(*this), mFilename(std::move(fname))
+    { }
+    ~MovieState()
+    {
+        stop();
+        if(mParseThread.joinable())
+            mParseThread.join();
+    }
+
+    static int decode_interrupt_cb(void *ctx);
+    bool prepare();
+    void setTitle(SDL_Window *window);
+    void stop();
+
+    nanoseconds getClock();
+
+    nanoseconds getMasterClock();
+
+    nanoseconds getDuration();
+
+    int streamComponentOpen(unsigned int stream_index);
+    int parse_handler();
+};
+
+
+nanoseconds AudioState::getClockNoLock()
+{
+    // The audio clock is the timestamp of the sample currently being heard.
+    if(alcGetInteger64vSOFT)
+    {
+        // If device start time = min, we aren't playing yet.
+        if(mDeviceStartTime == nanoseconds::min())
+            return nanoseconds::zero();
+
+        // Get the current device clock time and latency.
+        auto device = alcGetContextsDevice(alcGetCurrentContext());
+        ALCint64SOFT devtimes[2]{0,0};
+        alcGetInteger64vSOFT(device, ALC_DEVICE_CLOCK_LATENCY_SOFT, 2, devtimes);
+        auto latency = nanoseconds{devtimes[1]};
+        auto device_time = nanoseconds{devtimes[0]};
+
+        // The clock is simply the current device time relative to the recorded
+        // start time. We can also subtract the latency to get more a accurate
+        // position of where the audio device actually is in the output stream.
+        return device_time - mDeviceStartTime - latency;
+    }
+
+    if(mBufferDataSize > 0)
+    {
+        if(mDeviceStartTime == nanoseconds::min())
+            return nanoseconds::zero();
+
+        /* With a callback buffer and no device clock, mDeviceStartTime is
+         * actually the timestamp of the first sample frame played. The audio
+         * clock, then, is that plus the current source offset.
+         */
+        ALint64SOFT offset[2];
+        if(alGetSourcei64vSOFT)
+            alGetSourcei64vSOFT(mSource, AL_SAMPLE_OFFSET_LATENCY_SOFT, offset);
+        else
+        {
+            ALint ioffset;
+            alGetSourcei(mSource, AL_SAMPLE_OFFSET, &ioffset);
+            offset[0] = ALint64SOFT{ioffset} << 32;
+            offset[1] = 0;
+        }
+        /* NOTE: The source state must be checked last, in case an underrun
+         * occurs and the source stops between getting the state and retrieving
+         * the offset+latency.
+         */
+        ALint status;
+        alGetSourcei(mSource, AL_SOURCE_STATE, &status);
+
+        nanoseconds pts{};
+        if(status == AL_PLAYING || status == AL_PAUSED)
+            pts = mDeviceStartTime - nanoseconds{offset[1]} +
+                duration_cast<nanoseconds>(fixed32{offset[0] / mCodecCtx->sample_rate});
+        else
+        {
+            /* If the source is stopped, the pts of the next sample to be heard
+             * is the pts of the next sample to be buffered, minus the amount
+             * already in the buffer ready to play.
+             */
+            const size_t woffset{mWritePos.load(std::memory_order_acquire)};
+            const size_t roffset{mReadPos.load(std::memory_order_relaxed)};
+            const size_t readable{((woffset >= roffset) ? woffset : (mBufferDataSize+woffset)) -
+                roffset};
+
+            pts = mCurrentPts - nanoseconds{seconds{readable/mFrameSize}}/mCodecCtx->sample_rate;
+        }
+
+        return pts;
+    }
+
+    /* The source-based clock is based on 4 components:
+     * 1 - The timestamp of the next sample to buffer (mCurrentPts)
+     * 2 - The length of the source's buffer queue
+     *     (AudioBufferTime*AL_BUFFERS_QUEUED)
+     * 3 - The offset OpenAL is currently at in the source (the first value
+     *     from AL_SAMPLE_OFFSET_LATENCY_SOFT)
+     * 4 - The latency between OpenAL and the DAC (the second value from
+     *     AL_SAMPLE_OFFSET_LATENCY_SOFT)
+     *
+     * Subtracting the length of the source queue from the next sample's
+     * timestamp gives the timestamp of the sample at the start of the source
+     * queue. Adding the source offset to that results in the timestamp for the
+     * sample at OpenAL's current position, and subtracting the source latency
+     * from that gives the timestamp of the sample currently at the DAC.
+     */
+    nanoseconds pts{mCurrentPts};
+    if(mSource)
+    {
+        ALint64SOFT offset[2];
+        if(alGetSourcei64vSOFT)
+            alGetSourcei64vSOFT(mSource, AL_SAMPLE_OFFSET_LATENCY_SOFT, offset);
+        else
+        {
+            ALint ioffset;
+            alGetSourcei(mSource, AL_SAMPLE_OFFSET, &ioffset);
+            offset[0] = ALint64SOFT{ioffset} << 32;
+            offset[1] = 0;
+        }
+        ALint queued, status;
+        alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued);
+        alGetSourcei(mSource, AL_SOURCE_STATE, &status);
+
+        /* If the source is AL_STOPPED, then there was an underrun and all
+         * buffers are processed, so ignore the source queue. The audio thread
+         * will put the source into an AL_INITIAL state and clear the queue
+         * when it starts recovery.
+         */
+        if(status != AL_STOPPED)
+        {
+            pts -= AudioBufferTime*queued;
+            pts += duration_cast<nanoseconds>(fixed32{offset[0] / mCodecCtx->sample_rate});
+        }
+        /* Don't offset by the latency if the source isn't playing. */
+        if(status == AL_PLAYING)
+            pts -= nanoseconds{offset[1]};
+    }
+
+    return std::max(pts, nanoseconds::zero());
+}
+
+bool AudioState::startPlayback()
+{
+    const size_t woffset{mWritePos.load(std::memory_order_acquire)};
+    const size_t roffset{mReadPos.load(std::memory_order_relaxed)};
+    const size_t readable{((woffset >= roffset) ? woffset : (mBufferDataSize+woffset)) -
+        roffset};
+
+    if(mBufferDataSize > 0)
+    {
+        if(readable == 0)
+            return false;
+        if(!alcGetInteger64vSOFT)
+            mDeviceStartTime = mCurrentPts -
+                nanoseconds{seconds{readable/mFrameSize}}/mCodecCtx->sample_rate;
+    }
+    else
+    {
+        ALint queued{};
+        alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued);
+        if(queued == 0) return false;
+    }
+
+    alSourcePlay(mSource);
+    if(alcGetInteger64vSOFT)
+    {
+        /* Subtract the total buffer queue time from the current pts to get the
+         * pts of the start of the queue.
+         */
+        int64_t srctimes[2]{0,0};
+        alGetSourcei64vSOFT(mSource, AL_SAMPLE_OFFSET_CLOCK_SOFT, srctimes);
+        auto device_time = nanoseconds{srctimes[1]};
+        auto src_offset = duration_cast<nanoseconds>(fixed32{srctimes[0]}) /
+            mCodecCtx->sample_rate;
+
+        /* The mixer may have ticked and incremented the device time and sample
+         * offset, so subtract the source offset from the device time to get
+         * the device time the source started at. Also subtract startpts to get
+         * the device time the stream would have started at to reach where it
+         * is now.
+         */
+        if(mBufferDataSize > 0)
+        {
+            nanoseconds startpts{mCurrentPts -
+                nanoseconds{seconds{readable/mFrameSize}}/mCodecCtx->sample_rate};
+            mDeviceStartTime = device_time - src_offset - startpts;
+        }
+        else
+        {
+            nanoseconds startpts{mCurrentPts - AudioBufferTotalTime};
+            mDeviceStartTime = device_time - src_offset - startpts;
+        }
+    }
+    return true;
+}
+
+int AudioState::getSync()
+{
+    if(mMovie.mAVSyncType == SyncMaster::Audio)
+        return 0;
+
+    auto ref_clock = mMovie.getMasterClock();
+    auto diff = ref_clock - getClockNoLock();
+
+    if(!(diff < AVNoSyncThreshold && diff > -AVNoSyncThreshold))
+    {
+        /* Difference is TOO big; reset accumulated average */
+        mClockDiffAvg = seconds_d64::zero();
+        return 0;
+    }
+
+    /* Accumulate the diffs */
+    mClockDiffAvg = mClockDiffAvg*AudioAvgFilterCoeff + diff;
+    auto avg_diff = mClockDiffAvg*(1.0 - AudioAvgFilterCoeff);
+    if(avg_diff < AudioSyncThreshold/2.0 && avg_diff > -AudioSyncThreshold)
+        return 0;
+
+    /* Constrain the per-update difference to avoid exceedingly large skips */
+    diff = std::min<nanoseconds>(diff, AudioSampleCorrectionMax);
+    return static_cast<int>(duration_cast<seconds>(diff*mCodecCtx->sample_rate).count());
+}
+
+int AudioState::decodeFrame()
+{
+    do {
+        while(int ret{mQueue.receiveFrame(mCodecCtx.get(), mDecodedFrame.get())})
+        {
+            if(ret == AVErrorEOF) return 0;
+            std::cerr<< "Failed to receive frame: "<<ret <<std::endl;
+        }
+    } while(mDecodedFrame->nb_samples <= 0);
+
+    /* If provided, update w/ pts */
+    if(mDecodedFrame->best_effort_timestamp != AVNoPtsValue)
+        mCurrentPts = duration_cast<nanoseconds>(seconds_d64{av_q2d(mStream->time_base) *
+            static_cast<double>(mDecodedFrame->best_effort_timestamp)});
+
+    if(mDecodedFrame->nb_samples > mSamplesMax)
+    {
+        av_freep(&mSamples);
+        av_samples_alloc(&mSamples, nullptr, mCodecCtx->ch_layout.nb_channels,
+            mDecodedFrame->nb_samples, mDstSampleFmt, 0);
+        mSamplesMax = mDecodedFrame->nb_samples;
+    }
+    /* Return the amount of sample frames converted */
+    int data_size{swr_convert(mSwresCtx.get(), &mSamples, mDecodedFrame->nb_samples,
+        const_cast<const uint8_t**>(mDecodedFrame->data), mDecodedFrame->nb_samples)};
+
+    av_frame_unref(mDecodedFrame.get());
+    return data_size;
+}
+
+/* Duplicates the sample at in to out, count times. The frame size is a
+ * multiple of the template type size.
+ */
+template<typename T>
+static void sample_dup(uint8_t *out, const uint8_t *in, size_t count, size_t frame_size)
+{
+    auto *sample = reinterpret_cast<const T*>(in);
+    auto *dst = reinterpret_cast<T*>(out);
+
+    /* NOTE: frame_size is a multiple of sizeof(T). */
+    size_t type_mult{frame_size / sizeof(T)};
+    if(type_mult == 1)
+        std::fill_n(dst, count, *sample);
+    else for(size_t i{0};i < count;++i)
+    {
+        for(size_t j{0};j < type_mult;++j)
+            dst[i*type_mult + j] = sample[j];
+    }
+}
+
+static void sample_dup(uint8_t *out, const uint8_t *in, size_t count, size_t frame_size)
+{
+    if((frame_size&7) == 0)
+        sample_dup<uint64_t>(out, in, count, frame_size);
+    else if((frame_size&3) == 0)
+        sample_dup<uint32_t>(out, in, count, frame_size);
+    else if((frame_size&1) == 0)
+        sample_dup<uint16_t>(out, in, count, frame_size);
+    else
+        sample_dup<uint8_t>(out, in, count, frame_size);
+}
+
+bool AudioState::readAudio(uint8_t *samples, unsigned int length, int &sample_skip)
+{
+    unsigned int audio_size{0};
+
+    /* Read the next chunk of data, refill the buffer, and queue it
+     * on the source */
+    length /= mFrameSize;
+    while(mSamplesLen > 0 && audio_size < length)
+    {
+        unsigned int rem{length - audio_size};
+        if(mSamplesPos >= 0)
+        {
+            const auto len = static_cast<unsigned int>(mSamplesLen - mSamplesPos);
+            if(rem > len) rem = len;
+            std::copy_n(mSamples + static_cast<unsigned int>(mSamplesPos)*mFrameSize,
+                rem*mFrameSize, samples);
+        }
+        else
+        {
+            rem = std::min(rem, static_cast<unsigned int>(-mSamplesPos));
+
+            /* Add samples by copying the first sample */
+            sample_dup(samples, mSamples, rem, mFrameSize);
+        }
+
+        mSamplesPos += rem;
+        mCurrentPts += nanoseconds{seconds{rem}} / mCodecCtx->sample_rate;
+        samples += rem*mFrameSize;
+        audio_size += rem;
+
+        while(mSamplesPos >= mSamplesLen)
+        {
+            mSamplesLen = decodeFrame();
+            mSamplesPos = std::min(mSamplesLen, sample_skip);
+            if(mSamplesLen <= 0) break;
+
+            sample_skip -= mSamplesPos;
+
+            // Adjust the device start time and current pts by the amount we're
+            // skipping/duplicating, so that the clock remains correct for the
+            // current stream position.
+            auto skip = nanoseconds{seconds{mSamplesPos}} / mCodecCtx->sample_rate;
+            mDeviceStartTime -= skip;
+            mCurrentPts += skip;
+        }
+    }
+    if(audio_size <= 0)
+        return false;
+
+    if(audio_size < length)
+    {
+        const unsigned int rem{length - audio_size};
+        std::fill_n(samples, rem*mFrameSize,
+            (mDstSampleFmt == AV_SAMPLE_FMT_U8) ? 0x80 : 0x00);
+        mCurrentPts += nanoseconds{seconds{rem}} / mCodecCtx->sample_rate;
+    }
+    return true;
+}
+
+bool AudioState::readAudio(int sample_skip)
+{
+    size_t woffset{mWritePos.load(std::memory_order_acquire)};
+    const size_t roffset{mReadPos.load(std::memory_order_relaxed)};
+    while(mSamplesLen > 0)
+    {
+        const size_t nsamples{((roffset > woffset) ? roffset-woffset-1
+            : (roffset == 0) ? (mBufferDataSize-woffset-1)
+            : (mBufferDataSize-woffset)) / mFrameSize};
+        if(!nsamples) break;
+
+        if(mSamplesPos < 0)
+        {
+            const size_t rem{std::min<size_t>(nsamples, static_cast<ALuint>(-mSamplesPos))};
+
+            sample_dup(&mBufferData[woffset], mSamples, rem, mFrameSize);
+            woffset += rem * mFrameSize;
+            if(woffset == mBufferDataSize) woffset = 0;
+            mWritePos.store(woffset, std::memory_order_release);
+
+            mCurrentPts += nanoseconds{seconds{rem}} / mCodecCtx->sample_rate;
+            mSamplesPos += static_cast<int>(rem);
+            continue;
+        }
+
+        const size_t rem{std::min<size_t>(nsamples, static_cast<ALuint>(mSamplesLen-mSamplesPos))};
+        const size_t boffset{static_cast<ALuint>(mSamplesPos) * size_t{mFrameSize}};
+        const size_t nbytes{rem * mFrameSize};
+
+        memcpy(&mBufferData[woffset], mSamples + boffset, nbytes);
+        woffset += nbytes;
+        if(woffset == mBufferDataSize) woffset = 0;
+        mWritePos.store(woffset, std::memory_order_release);
+
+        mCurrentPts += nanoseconds{seconds{rem}} / mCodecCtx->sample_rate;
+        mSamplesPos += static_cast<int>(rem);
+
+        while(mSamplesPos >= mSamplesLen)
+        {
+            mSamplesLen = decodeFrame();
+            mSamplesPos = std::min(mSamplesLen, sample_skip);
+            if(mSamplesLen <= 0) return false;
+
+            sample_skip -= mSamplesPos;
+
+            auto skip = nanoseconds{seconds{mSamplesPos}} / mCodecCtx->sample_rate;
+            mDeviceStartTime -= skip;
+            mCurrentPts += skip;
+        }
+    }
+
+    return true;
+}
+
+
+void AL_APIENTRY AudioState::eventCallback(ALenum eventType, ALuint object, ALuint param,
+    ALsizei length, const ALchar *message)
+{
+    if(eventType == AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT)
+    {
+        /* Temporarily lock the source mutex to ensure it's not between
+         * checking the processed count and going to sleep.
+         */
+        std::unique_lock<std::mutex>{mSrcMutex}.unlock();
+        mSrcCond.notify_one();
+        return;
+    }
+
+    std::cout<< "\n---- AL Event on AudioState "<<this<<" ----\nEvent: ";
+    switch(eventType)
+    {
+    case AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT: std::cout<< "Buffer completed"; break;
+    case AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT: std::cout<< "Source state changed"; break;
+    case AL_EVENT_TYPE_DISCONNECTED_SOFT: std::cout<< "Disconnected"; break;
+    default:
+        std::cout<< "0x"<<std::hex<<std::setw(4)<<std::setfill('0')<<eventType<<std::dec<<
+            std::setw(0)<<std::setfill(' '); break;
+    }
+    std::cout<< "\n"
+        "Object ID: "<<object<<"\n"
+        "Parameter: "<<param<<"\n"
+        "Message: "<<std::string{message, static_cast<ALuint>(length)}<<"\n----"<<
+        std::endl;
+
+    if(eventType == AL_EVENT_TYPE_DISCONNECTED_SOFT)
+    {
+        {
+            std::lock_guard<std::mutex> lock{mSrcMutex};
+            mConnected.clear(std::memory_order_release);
+        }
+        mSrcCond.notify_one();
+    }
+}
+
+ALsizei AudioState::bufferCallback(void *data, ALsizei size)
+{
+    ALsizei got{0};
+
+    size_t roffset{mReadPos.load(std::memory_order_acquire)};
+    while(got < size)
+    {
+        const size_t woffset{mWritePos.load(std::memory_order_relaxed)};
+        if(woffset == roffset) break;
+
+        size_t todo{((woffset < roffset) ? mBufferDataSize : woffset) - roffset};
+        todo = std::min<size_t>(todo, static_cast<ALuint>(size-got));
+
+        memcpy(data, &mBufferData[roffset], todo);
+        data = static_cast<ALbyte*>(data) + todo;
+        got += static_cast<ALsizei>(todo);
+
+        roffset += todo;
+        if(roffset == mBufferDataSize)
+            roffset = 0;
+    }
+    mReadPos.store(roffset, std::memory_order_release);
+
+    return got;
+}
+
+int AudioState::handler()
+{
+    std::unique_lock<std::mutex> srclock{mSrcMutex, std::defer_lock};
+    milliseconds sleep_time{AudioBufferTime / 3};
+
+    struct EventControlManager {
+        const std::array<ALenum,3> evt_types{{
+            AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT, AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT,
+            AL_EVENT_TYPE_DISCONNECTED_SOFT}};
+
+        EventControlManager(milliseconds &sleep_time)
+        {
+            if(alEventControlSOFT)
+            {
+                alEventControlSOFT(static_cast<ALsizei>(evt_types.size()), evt_types.data(),
+                    AL_TRUE);
+                alEventCallbackSOFT(&AudioState::eventCallbackC, this);
+                sleep_time = AudioBufferTotalTime;
+            }
+        }
+        ~EventControlManager()
+        {
+            if(alEventControlSOFT)
+            {
+                alEventControlSOFT(static_cast<ALsizei>(evt_types.size()), evt_types.data(),
+                    AL_FALSE);
+                alEventCallbackSOFT(nullptr, nullptr);
+            }
+        }
+    };
+    EventControlManager event_controller{sleep_time};
+
+    std::unique_ptr<uint8_t[]> samples;
+    ALsizei buffer_len{0};
+
+    /* Find a suitable format for OpenAL. */
+    mDstChanLayout = 0;
+    mFormat = AL_NONE;
+    if((mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLT || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLTP
+            || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_DBL
+            || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_DBLP
+            || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_S32
+            || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_S32P
+            || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_S64
+            || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_S64P)
+        && alIsExtensionPresent("AL_EXT_FLOAT32"))
+    {
+        mDstSampleFmt = AV_SAMPLE_FMT_FLT;
+        mFrameSize = 4;
+        if(mCodecCtx->ch_layout.order == AV_CHANNEL_ORDER_NATIVE)
+        {
+            if(alIsExtensionPresent("AL_EXT_MCFORMATS"))
+            {
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_7POINT1)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 8;
+                    mFormat = alGetEnumValue("AL_FORMAT_71CHN32");
+                }
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_5POINT1
+                    || mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_5POINT1_BACK)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 6;
+                    mFormat = alGetEnumValue("AL_FORMAT_51CHN32");
+                }
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_QUAD)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 4;
+                    mFormat = alGetEnumValue("AL_FORMAT_QUAD32");
+                }
+            }
+            if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_MONO)
+            {
+                mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                mFrameSize *= 1;
+                mFormat = AL_FORMAT_MONO_FLOAT32;
+            }
+        }
+        else if(mCodecCtx->ch_layout.order == AV_CHANNEL_ORDER_AMBISONIC
+            && alIsExtensionPresent("AL_EXT_BFORMAT"))
+        {
+            /* Calculate what should be the ambisonic order from the number of
+             * channels, and confirm that's the number of channels. Opus allows
+             * an optional non-diegetic stereo stream with the B-Format stream,
+             * which we can ignore, so check for that too.
+             */
+            auto order = static_cast<int>(std::sqrt(mCodecCtx->ch_layout.nb_channels)) - 1;
+            int channels{(order+1) * (order+1)};
+            if(channels == mCodecCtx->ch_layout.nb_channels
+                || channels+2 == mCodecCtx->ch_layout.nb_channels)
+            {
+                /* OpenAL only supports first-order with AL_EXT_BFORMAT, which
+                 * is 4 channels for 3D buffers.
+                 */
+                mFrameSize *= 4;
+                mFormat = alGetEnumValue("AL_FORMAT_BFORMAT3D_FLOAT32");
+            }
+        }
+        if(!mFormat || mFormat == -1)
+        {
+            mDstChanLayout = AV_CH_LAYOUT_STEREO;
+            mFrameSize *= 2;
+            mFormat = EnableUhj ? AL_FORMAT_UHJ2CHN_FLOAT32_SOFT : AL_FORMAT_STEREO_FLOAT32;
+        }
+    }
+    if(mCodecCtx->sample_fmt == AV_SAMPLE_FMT_U8 || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_U8P)
+    {
+        mDstSampleFmt = AV_SAMPLE_FMT_U8;
+        mFrameSize = 1;
+        if(mCodecCtx->ch_layout.order == AV_CHANNEL_ORDER_NATIVE)
+        {
+            if(alIsExtensionPresent("AL_EXT_MCFORMATS"))
+            {
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_7POINT1)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 8;
+                    mFormat = alGetEnumValue("AL_FORMAT_71CHN8");
+                }
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_5POINT1
+                    || mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_5POINT1_BACK)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 6;
+                    mFormat = alGetEnumValue("AL_FORMAT_51CHN8");
+                }
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_QUAD)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 4;
+                    mFormat = alGetEnumValue("AL_FORMAT_QUAD8");
+                }
+            }
+            if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_MONO)
+            {
+                mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                mFrameSize *= 1;
+                mFormat = AL_FORMAT_MONO8;
+            }
+        }
+        else if(mCodecCtx->ch_layout.order == AV_CHANNEL_ORDER_AMBISONIC
+            && alIsExtensionPresent("AL_EXT_BFORMAT"))
+        {
+            auto order = static_cast<int>(std::sqrt(mCodecCtx->ch_layout.nb_channels)) - 1;
+            int channels{(order+1) * (order+1)};
+            if(channels == mCodecCtx->ch_layout.nb_channels
+                || channels+2 == mCodecCtx->ch_layout.nb_channels)
+            {
+                mFrameSize *= 4;
+                mFormat = alGetEnumValue("AL_FORMAT_BFORMAT3D_8");
+            }
+        }
+        if(!mFormat || mFormat == -1)
+        {
+            mDstChanLayout = AV_CH_LAYOUT_STEREO;
+            mFrameSize *= 2;
+            mFormat = EnableUhj ? AL_FORMAT_UHJ2CHN8_SOFT : AL_FORMAT_STEREO8;
+        }
+    }
+    if(!mFormat || mFormat == -1)
+    {
+        mDstSampleFmt = AV_SAMPLE_FMT_S16;
+        mFrameSize = 2;
+        if(mCodecCtx->ch_layout.order == AV_CHANNEL_ORDER_NATIVE)
+        {
+            if(alIsExtensionPresent("AL_EXT_MCFORMATS"))
+            {
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_7POINT1)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 8;
+                    mFormat = alGetEnumValue("AL_FORMAT_71CHN16");
+                }
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_5POINT1
+                    || mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_5POINT1_BACK)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 6;
+                    mFormat = alGetEnumValue("AL_FORMAT_51CHN16");
+                }
+                if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_QUAD)
+                {
+                    mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                    mFrameSize *= 4;
+                    mFormat = alGetEnumValue("AL_FORMAT_QUAD16");
+                }
+            }
+            if(mCodecCtx->ch_layout.u.mask == AV_CH_LAYOUT_MONO)
+            {
+                mDstChanLayout = mCodecCtx->ch_layout.u.mask;
+                mFrameSize *= 1;
+                mFormat = AL_FORMAT_MONO16;
+            }
+        }
+        else if(mCodecCtx->ch_layout.order == AV_CHANNEL_ORDER_AMBISONIC
+            && alIsExtensionPresent("AL_EXT_BFORMAT"))
+        {
+            auto order = static_cast<int>(std::sqrt(mCodecCtx->ch_layout.nb_channels)) - 1;
+            int channels{(order+1) * (order+1)};
+            if(channels == mCodecCtx->ch_layout.nb_channels
+                || channels+2 == mCodecCtx->ch_layout.nb_channels)
+            {
+                mFrameSize *= 4;
+                mFormat = alGetEnumValue("AL_FORMAT_BFORMAT3D_16");
+            }
+        }
+        if(!mFormat || mFormat == -1)
+        {
+            mDstChanLayout = AV_CH_LAYOUT_STEREO;
+            mFrameSize *= 2;
+            mFormat = EnableUhj ? AL_FORMAT_UHJ2CHN16_SOFT : AL_FORMAT_STEREO16;
+        }
+    }
+
+    mSamples = nullptr;
+    mSamplesMax = 0;
+    mSamplesPos = 0;
+    mSamplesLen = 0;
+
+    mDecodedFrame.reset(av_frame_alloc());
+    if(!mDecodedFrame)
+    {
+        std::cerr<< "Failed to allocate audio frame" <<std::endl;
+        return 0;
+    }
+
+    /* Note that ffmpeg assumes AmbiX (ACN layout, SN3D normalization). */
+    const bool has_bfmt_ex{alIsExtensionPresent("AL_SOFT_bformat_ex") != AL_FALSE};
+    const ALenum ambi_layout{AL_ACN_SOFT};
+    const ALenum ambi_scale{AL_SN3D_SOFT};
+
+    if(!mDstChanLayout)
+    {
+        /* OpenAL only supports first-order ambisonics with AL_EXT_BFORMAT, so
+         * we have to drop any extra channels.
+         */
+        ChannelLayout layout{};
+        av_channel_layout_from_string(&layout, "ambisonic 1");
+
+        SwrContext *ps{};
+        int err{swr_alloc_set_opts2(&ps, &layout, mDstSampleFmt, mCodecCtx->sample_rate,
+            &mCodecCtx->ch_layout, mCodecCtx->sample_fmt, mCodecCtx->sample_rate, 0, nullptr)};
+        mSwresCtx.reset(ps);
+        if(err != 0)
+        {
+            char errstr[AV_ERROR_MAX_STRING_SIZE]{};
+            std::cerr<< "Failed to allocate SwrContext: "
+                <<av_make_error_string(errstr, AV_ERROR_MAX_STRING_SIZE, err) <<std::endl;
+            return 0;
+        }
+
+        if(has_bfmt_ex)
+            std::cout<< "Found AL_SOFT_bformat_ex" <<std::endl;
+        else
+        {
+            std::cout<< "Found AL_EXT_BFORMAT" <<std::endl;
+            /* Without AL_SOFT_bformat_ex, OpenAL only supports FuMa channel
+             * ordering and normalization, so a custom matrix is needed to
+             * scale and reorder the source from AmbiX.
+             */
+            std::vector<double> mtx(64*64, 0.0);
+            mtx[0 + 0*64] = std::sqrt(0.5);
+            mtx[3 + 1*64] = 1.0;
+            mtx[1 + 2*64] = 1.0;
+            mtx[2 + 3*64] = 1.0;
+            swr_set_matrix(mSwresCtx.get(), mtx.data(), 64);
+        }
+    }
+    else
+    {
+        ChannelLayout layout{};
+        av_channel_layout_from_mask(&layout, mDstChanLayout);
+
+        SwrContext *ps{};
+        int err{swr_alloc_set_opts2(&ps, &layout, mDstSampleFmt, mCodecCtx->sample_rate,
+            &mCodecCtx->ch_layout, mCodecCtx->sample_fmt, mCodecCtx->sample_rate, 0, nullptr)};
+        mSwresCtx.reset(ps);
+        if(err != 0)
+        {
+            char errstr[AV_ERROR_MAX_STRING_SIZE]{};
+            std::cerr<< "Failed to allocate SwrContext: "
+                <<av_make_error_string(errstr, AV_ERROR_MAX_STRING_SIZE, err) <<std::endl;
+            return 0;
+        }
+    }
+    if(int err{swr_init(mSwresCtx.get())})
+    {
+        char errstr[AV_ERROR_MAX_STRING_SIZE]{};
+        std::cerr<< "Failed to initialize audio converter: "
+            <<av_make_error_string(errstr, AV_ERROR_MAX_STRING_SIZE, err) <<std::endl;
+        return 0;
+    }
+
+    alGenBuffers(static_cast<ALsizei>(mBuffers.size()), mBuffers.data());
+    alGenSources(1, &mSource);
+
+    if(DirectOutMode)
+        alSourcei(mSource, AL_DIRECT_CHANNELS_SOFT, DirectOutMode);
+    if(EnableWideStereo)
+    {
+        const float angles[2]{static_cast<float>(M_PI / 3.0), static_cast<float>(-M_PI / 3.0)};
+        alSourcefv(mSource, AL_STEREO_ANGLES, angles);
+    }
+    if(has_bfmt_ex)
+    {
+        for(ALuint bufid : mBuffers)
+        {
+            alBufferi(bufid, AL_AMBISONIC_LAYOUT_SOFT, ambi_layout);
+            alBufferi(bufid, AL_AMBISONIC_SCALING_SOFT, ambi_scale);
+        }
+    }
+#ifdef AL_SOFT_UHJ
+    if(EnableSuperStereo)
+        alSourcei(mSource, AL_STEREO_MODE_SOFT, AL_SUPER_STEREO_SOFT);
+#endif
+
+    if(alGetError() != AL_NO_ERROR)
+        return 0;
+
+    bool callback_ok{false};
+    if(alBufferCallbackSOFT)
+    {
+        alBufferCallbackSOFT(mBuffers[0], mFormat, mCodecCtx->sample_rate, bufferCallbackC, this);
+        alSourcei(mSource, AL_BUFFER, static_cast<ALint>(mBuffers[0]));
+        if(alGetError() != AL_NO_ERROR)
+        {
+            fprintf(stderr, "Failed to set buffer callback\n");
+            alSourcei(mSource, AL_BUFFER, 0);
+        }
+        else
+        {
+            mBufferDataSize = static_cast<size_t>(duration_cast<seconds>(mCodecCtx->sample_rate *
+                AudioBufferTotalTime).count()) * mFrameSize;
+            mBufferData = std::make_unique<uint8_t[]>(mBufferDataSize);
+            std::fill_n(mBufferData.get(), mBufferDataSize, uint8_t{});
+
+            mReadPos.store(0, std::memory_order_relaxed);
+            mWritePos.store(mBufferDataSize/mFrameSize/2*mFrameSize, std::memory_order_relaxed);
+
+            ALCint refresh{};
+            alcGetIntegerv(alcGetContextsDevice(alcGetCurrentContext()), ALC_REFRESH, 1, &refresh);
+            sleep_time = milliseconds{seconds{1}} / refresh;
+            callback_ok = true;
+        }
+    }
+    if(!callback_ok)
+        buffer_len = static_cast<int>(duration_cast<seconds>(mCodecCtx->sample_rate *
+            AudioBufferTime).count() * mFrameSize);
+    if(buffer_len > 0)
+        samples = std::make_unique<uint8_t[]>(static_cast<ALuint>(buffer_len));
+
+    /* Prefill the codec buffer. */
+    auto packet_sender = [this]()
+    {
+        while(1)
+        {
+            const int ret{mQueue.sendPacket(mCodecCtx.get())};
+            if(ret == AVErrorEOF) break;
+        }
+    };
+    auto sender = std::async(std::launch::async, packet_sender);
+
+    srclock.lock();
+    if(alcGetInteger64vSOFT)
+    {
+        int64_t devtime{};
+        alcGetInteger64vSOFT(alcGetContextsDevice(alcGetCurrentContext()), ALC_DEVICE_CLOCK_SOFT,
+            1, &devtime);
+        mDeviceStartTime = nanoseconds{devtime} - mCurrentPts;
+    }
+
+    mSamplesLen = decodeFrame();
+    if(mSamplesLen > 0)
+    {
+        mSamplesPos = std::min(mSamplesLen, getSync());
+
+        auto skip = nanoseconds{seconds{mSamplesPos}} / mCodecCtx->sample_rate;
+        mDeviceStartTime -= skip;
+        mCurrentPts += skip;
+    }
+
+    while(1)
+    {
+        if(mMovie.mQuit.load(std::memory_order_relaxed))
+        {
+            /* If mQuit is set, drain frames until we can't get more audio,
+             * indicating we've reached the flush packet and the packet sender
+             * will also quit.
+             */
+            do {
+                mSamplesLen = decodeFrame();
+                mSamplesPos = mSamplesLen;
+            } while(mSamplesLen > 0);
+            goto finish;
+        }
+
+        ALenum state;
+        if(mBufferDataSize > 0)
+        {
+            alGetSourcei(mSource, AL_SOURCE_STATE, &state);
+
+            /* If mQuit is not set, don't quit even if there's no more audio,
+             * so what's buffered has a chance to play to the real end.
+             */
+            readAudio(getSync());
+        }
+        else
+        {
+            ALint processed, queued;
+
+            /* First remove any processed buffers. */
+            alGetSourcei(mSource, AL_BUFFERS_PROCESSED, &processed);
+            while(processed > 0)
+            {
+                ALuint bid;
+                alSourceUnqueueBuffers(mSource, 1, &bid);
+                --processed;
+            }
+
+            /* Refill the buffer queue. */
+            int sync_skip{getSync()};
+            alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued);
+            while(static_cast<ALuint>(queued) < mBuffers.size())
+            {
+                /* Read the next chunk of data, filling the buffer, and queue
+                 * it on the source.
+                 */
+                if(!readAudio(samples.get(), static_cast<ALuint>(buffer_len), sync_skip))
+                    break;
+
+                const ALuint bufid{mBuffers[mBufferIdx]};
+                mBufferIdx = static_cast<ALuint>((mBufferIdx+1) % mBuffers.size());
+
+                alBufferData(bufid, mFormat, samples.get(), buffer_len, mCodecCtx->sample_rate);
+                alSourceQueueBuffers(mSource, 1, &bufid);
+                ++queued;
+            }
+
+            /* Check that the source is playing. */
+            alGetSourcei(mSource, AL_SOURCE_STATE, &state);
+            if(state == AL_STOPPED)
+            {
+                /* AL_STOPPED means there was an underrun. Clear the buffer
+                 * queue since this likely means we're late, and rewind the
+                 * source to get it back into an AL_INITIAL state.
+                 */
+                alSourceRewind(mSource);
+                alSourcei(mSource, AL_BUFFER, 0);
+                if(alcGetInteger64vSOFT)
+                {
+                    /* Also update the device start time with the current
+                     * device clock, so the decoder knows we're running behind.
+                     */
+                    int64_t devtime{};
+                    alcGetInteger64vSOFT(alcGetContextsDevice(alcGetCurrentContext()),
+                        ALC_DEVICE_CLOCK_SOFT, 1, &devtime);
+                    mDeviceStartTime = nanoseconds{devtime} - mCurrentPts;
+                }
+                continue;
+            }
+        }
+
+        /* (re)start the source if needed, and wait for a buffer to finish */
+        if(state != AL_PLAYING && state != AL_PAUSED)
+        {
+            if(!startPlayback())
+                break;
+        }
+        if(ALenum err{alGetError()})
+            std::cerr<< "Got AL error: 0x"<<std::hex<<err<<std::dec
+                << " ("<<alGetString(err)<<")" <<std::endl;
+
+        mSrcCond.wait_for(srclock, sleep_time);
+    }
+finish:
+
+    alSourceRewind(mSource);
+    alSourcei(mSource, AL_BUFFER, 0);
+    srclock.unlock();
+
+    return 0;
+}
+
+
+nanoseconds VideoState::getClock()
+{
+    /* NOTE: This returns incorrect times while not playing. */
+    std::lock_guard<std::mutex> _{mDispPtsMutex};
+    if(mDisplayPtsTime == microseconds::min())
+        return nanoseconds::zero();
+    auto delta = get_avtime() - mDisplayPtsTime;
+    return mDisplayPts + delta;
+}
+
+/* Called by VideoState::updateVideo to display the next video frame. */
+void VideoState::display(SDL_Window *screen, SDL_Renderer *renderer, AVFrame *frame)
+{
+    if(!mImage)
+        return;
+
+    double aspect_ratio;
+    int win_w, win_h;
+    int w, h, x, y;
+
+    int frame_width{frame->width - static_cast<int>(frame->crop_left + frame->crop_right)};
+    int frame_height{frame->height - static_cast<int>(frame->crop_top + frame->crop_bottom)};
+    if(frame->sample_aspect_ratio.num == 0)
+        aspect_ratio = 0.0;
+    else
+    {
+        aspect_ratio = av_q2d(frame->sample_aspect_ratio) * frame_width /
+            frame_height;
+    }
+    if(aspect_ratio <= 0.0)
+        aspect_ratio = static_cast<double>(frame_width) / frame_height;
+
+    SDL_GetWindowSize(screen, &win_w, &win_h);
+    h = win_h;
+    w = (static_cast<int>(std::rint(h * aspect_ratio)) + 3) & ~3;
+    if(w > win_w)
+    {
+        w = win_w;
+        h = (static_cast<int>(std::rint(w / aspect_ratio)) + 3) & ~3;
+    }
+    x = (win_w - w) / 2;
+    y = (win_h - h) / 2;
+
+    SDL_Rect src_rect{ static_cast<int>(frame->crop_left), static_cast<int>(frame->crop_top),
+        frame_width, frame_height };
+    SDL_Rect dst_rect{ x, y, w, h };
+    SDL_RenderCopy(renderer, mImage, &src_rect, &dst_rect);
+    SDL_RenderPresent(renderer);
+}
+
+/* Called regularly on the main thread where the SDL_Renderer was created. It
+ * handles updating the textures of decoded frames and displaying the latest
+ * frame.
+ */
+void VideoState::updateVideo(SDL_Window *screen, SDL_Renderer *renderer, bool redraw)
+{
+    size_t read_idx{mPictQRead.load(std::memory_order_relaxed)};
+    Picture *vp{&mPictQ[read_idx]};
+
+    auto clocktime = mMovie.getMasterClock();
+    bool updated{false};
+    while(1)
+    {
+        size_t next_idx{(read_idx+1)%mPictQ.size()};
+        if(next_idx == mPictQWrite.load(std::memory_order_acquire))
+            break;
+        Picture *nextvp{&mPictQ[next_idx]};
+        if(clocktime < nextvp->mPts && !mMovie.mQuit.load(std::memory_order_relaxed))
+        {
+            /* For the first update, ensure the first frame gets shown.  */
+            if(!mFirstUpdate || updated)
+                break;
+        }
+
+        vp = nextvp;
+        updated = true;
+        read_idx = next_idx;
+    }
+    if(mMovie.mQuit.load(std::memory_order_relaxed))
+    {
+        if(mEOS)
+            mFinalUpdate = true;
+        mPictQRead.store(read_idx, std::memory_order_release);
+        std::unique_lock<std::mutex>{mPictQMutex}.unlock();
+        mPictQCond.notify_one();
+        return;
+    }
+
+    AVFrame *frame{vp->mFrame.get()};
+    if(updated)
+    {
+        mPictQRead.store(read_idx, std::memory_order_release);
+        std::unique_lock<std::mutex>{mPictQMutex}.unlock();
+        mPictQCond.notify_one();
+
+        /* allocate or resize the buffer! */
+        bool fmt_updated{false};
+        if(!mImage || mWidth != frame->width || mHeight != frame->height)
+        {
+            fmt_updated = true;
+            if(mImage)
+                SDL_DestroyTexture(mImage);
+            mImage = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
+                frame->width, frame->height);
+            if(!mImage)
+                std::cerr<< "Failed to create YV12 texture!" <<std::endl;
+            mWidth = frame->width;
+            mHeight = frame->height;
+        }
+
+        int frame_width{frame->width - static_cast<int>(frame->crop_left + frame->crop_right)};
+        int frame_height{frame->height - static_cast<int>(frame->crop_top + frame->crop_bottom)};
+        if(mFirstUpdate && frame_width > 0 && frame_height > 0)
+        {
+            /* For the first update, set the window size to the video size. */
+            mFirstUpdate = false;
+
+            if(frame->sample_aspect_ratio.den != 0)
+            {
+                double aspect_ratio = av_q2d(frame->sample_aspect_ratio);
+                if(aspect_ratio >= 1.0)
+                    frame_width = static_cast<int>(frame_width*aspect_ratio + 0.5);
+                else if(aspect_ratio > 0.0)
+                    frame_height = static_cast<int>(frame_height/aspect_ratio + 0.5);
+            }
+            SDL_SetWindowSize(screen, frame_width, frame_height);
+        }
+
+        if(mImage)
+        {
+            void *pixels{nullptr};
+            int pitch{0};
+
+            if(mCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P)
+                SDL_UpdateYUVTexture(mImage, nullptr,
+                    frame->data[0], frame->linesize[0],
+                    frame->data[1], frame->linesize[1],
+                    frame->data[2], frame->linesize[2]
+                );
+            else if(SDL_LockTexture(mImage, nullptr, &pixels, &pitch) != 0)
+                std::cerr<< "Failed to lock texture" <<std::endl;
+            else
+            {
+                // Convert the image into YUV format that SDL uses
+                int w{frame->width};
+                int h{frame->height};
+                if(!mSwscaleCtx || fmt_updated)
+                {
+                    mSwscaleCtx.reset(sws_getContext(
+                        w, h, mCodecCtx->pix_fmt,
+                        w, h, AV_PIX_FMT_YUV420P, 0,
+                        nullptr, nullptr, nullptr
+                    ));
+                }
+
+                /* point pict at the queue */
+                uint8_t *pict_data[3];
+                pict_data[0] = static_cast<uint8_t*>(pixels);
+                pict_data[1] = pict_data[0] + w*h;
+                pict_data[2] = pict_data[1] + w*h/4;
+
+                int pict_linesize[3];
+                pict_linesize[0] = pitch;
+                pict_linesize[1] = pitch / 2;
+                pict_linesize[2] = pitch / 2;
+
+                sws_scale(mSwscaleCtx.get(), reinterpret_cast<uint8_t**>(frame->data), frame->linesize,
+                    0, h, pict_data, pict_linesize);
+                SDL_UnlockTexture(mImage);
+            }
+
+            redraw = true;
+        }
+    }
+
+    if(redraw)
+    {
+        /* Show the picture! */
+        display(screen, renderer, frame);
+    }
+
+    if(updated)
+    {
+        auto disp_time = get_avtime();
+
+        std::lock_guard<std::mutex> _{mDispPtsMutex};
+        mDisplayPts = vp->mPts;
+        mDisplayPtsTime = disp_time;
+    }
+    if(mEOS.load(std::memory_order_acquire))
+    {
+        if((read_idx+1)%mPictQ.size() == mPictQWrite.load(std::memory_order_acquire))
+        {
+            mFinalUpdate = true;
+            std::unique_lock<std::mutex>{mPictQMutex}.unlock();
+            mPictQCond.notify_one();
+        }
+    }
+}
+
+int VideoState::handler()
+{
+    std::for_each(mPictQ.begin(), mPictQ.end(),
+        [](Picture &pict) -> void
+        { pict.mFrame = AVFramePtr{av_frame_alloc()}; });
+
+    /* Prefill the codec buffer. */
+    auto packet_sender = [this]()
+    {
+        while(1)
+        {
+            const int ret{mQueue.sendPacket(mCodecCtx.get())};
+            if(ret == AVErrorEOF) break;
+        }
+    };
+    auto sender = std::async(std::launch::async, packet_sender);
+
+    {
+        std::lock_guard<std::mutex> _{mDispPtsMutex};
+        mDisplayPtsTime = get_avtime();
+    }
+
+    auto current_pts = nanoseconds::zero();
+    while(1)
+    {
+        size_t write_idx{mPictQWrite.load(std::memory_order_relaxed)};
+        Picture *vp{&mPictQ[write_idx]};
+
+        /* Retrieve video frame. */
+        AVFrame *decoded_frame{vp->mFrame.get()};
+        while(int ret{mQueue.receiveFrame(mCodecCtx.get(), decoded_frame)})
+        {
+            if(ret == AVErrorEOF) goto finish;
+            std::cerr<< "Failed to receive frame: "<<ret <<std::endl;
+        }
+
+        /* Get the PTS for this frame. */
+        if(decoded_frame->best_effort_timestamp != AVNoPtsValue)
+            current_pts = duration_cast<nanoseconds>(seconds_d64{av_q2d(mStream->time_base) *
+                static_cast<double>(decoded_frame->best_effort_timestamp)});
+        vp->mPts = current_pts;
+
+        /* Update the video clock to the next expected PTS. */
+        auto frame_delay = av_q2d(mCodecCtx->time_base);
+        frame_delay += decoded_frame->repeat_pict * (frame_delay * 0.5);
+        current_pts += duration_cast<nanoseconds>(seconds_d64{frame_delay});
+
+        /* Put the frame in the queue to be loaded into a texture and displayed
+         * by the rendering thread.
+         */
+        write_idx = (write_idx+1)%mPictQ.size();
+        mPictQWrite.store(write_idx, std::memory_order_release);
+
+        if(write_idx == mPictQRead.load(std::memory_order_acquire))
+        {
+            /* Wait until we have space for a new pic */
+            std::unique_lock<std::mutex> lock{mPictQMutex};
+            while(write_idx == mPictQRead.load(std::memory_order_acquire))
+                mPictQCond.wait(lock);
+        }
+    }
+finish:
+    mEOS = true;
+
+    std::unique_lock<std::mutex> lock{mPictQMutex};
+    while(!mFinalUpdate) mPictQCond.wait(lock);
+
+    return 0;
+}
+
+
+int MovieState::decode_interrupt_cb(void *ctx)
+{
+    return static_cast<MovieState*>(ctx)->mQuit.load(std::memory_order_relaxed);
+}
+
+bool MovieState::prepare()
+{
+    AVIOContext *avioctx{nullptr};
+    AVIOInterruptCB intcb{decode_interrupt_cb, this};
+    if(avio_open2(&avioctx, mFilename.c_str(), AVIO_FLAG_READ, &intcb, nullptr))
+    {
+        std::cerr<< "Failed to open "<<mFilename <<std::endl;
+        return false;
+    }
+    mIOContext.reset(avioctx);
+
+    /* Open movie file. If avformat_open_input fails it will automatically free
+     * this context, so don't set it onto a smart pointer yet.
+     */
+    AVFormatContext *fmtctx{avformat_alloc_context()};
+    fmtctx->pb = mIOContext.get();
+    fmtctx->interrupt_callback = intcb;
+    if(avformat_open_input(&fmtctx, mFilename.c_str(), nullptr, nullptr) != 0)
+    {
+        std::cerr<< "Failed to open "<<mFilename <<std::endl;
+        return false;
+    }
+    mFormatCtx.reset(fmtctx);
+
+    /* Retrieve stream information */
+    if(avformat_find_stream_info(mFormatCtx.get(), nullptr) < 0)
+    {
+        std::cerr<< mFilename<<": failed to find stream info" <<std::endl;
+        return false;
+    }
+
+    /* Dump information about file onto standard error */
+    av_dump_format(mFormatCtx.get(), 0, mFilename.c_str(), 0);
+
+    mParseThread = std::thread{std::mem_fn(&MovieState::parse_handler), this};
+
+    std::unique_lock<std::mutex> slock{mStartupMutex};
+    while(!mStartupDone) mStartupCond.wait(slock);
+    return true;
+}
+
+void MovieState::setTitle(SDL_Window *window)
+{
+    auto pos1 = mFilename.rfind('/');
+    auto pos2 = mFilename.rfind('\\');
+    auto fpos = ((pos1 == std::string::npos) ? pos2 :
+                 (pos2 == std::string::npos) ? pos1 :
+                 std::max(pos1, pos2)) + 1;
+    SDL_SetWindowTitle(window, (mFilename.substr(fpos)+" - "+AppName).c_str());
+}
+
+nanoseconds MovieState::getClock()
+{
+    if(mClockBase == microseconds::min())
+        return nanoseconds::zero();
+    return get_avtime() - mClockBase;
+}
+
+nanoseconds MovieState::getMasterClock()
+{
+    if(mAVSyncType == SyncMaster::Video && mVideo.mStream)
+        return mVideo.getClock();
+    if(mAVSyncType == SyncMaster::Audio && mAudio.mStream)
+        return mAudio.getClock();
+    return getClock();
+}
+
+nanoseconds MovieState::getDuration()
+{ return std::chrono::duration<int64_t,std::ratio<1,AV_TIME_BASE>>(mFormatCtx->duration); }
+
+int MovieState::streamComponentOpen(unsigned int stream_index)
+{
+    if(stream_index >= mFormatCtx->nb_streams)
+        return -1;
+
+    /* Get a pointer to the codec context for the stream, and open the
+     * associated codec.
+     */
+    AVCodecCtxPtr avctx{avcodec_alloc_context3(nullptr)};
+    if(!avctx) return -1;
+
+    if(avcodec_parameters_to_context(avctx.get(), mFormatCtx->streams[stream_index]->codecpar))
+        return -1;
+
+    const AVCodec *codec{avcodec_find_decoder(avctx->codec_id)};
+    if(!codec || avcodec_open2(avctx.get(), codec, nullptr) < 0)
+    {
+        std::cerr<< "Unsupported codec: "<<avcodec_get_name(avctx->codec_id)
+            << " (0x"<<std::hex<<avctx->codec_id<<std::dec<<")" <<std::endl;
+        return -1;
+    }
+
+    /* Initialize and start the media type handler */
+    switch(avctx->codec_type)
+    {
+        case AVMEDIA_TYPE_AUDIO:
+            mAudio.mStream = mFormatCtx->streams[stream_index];
+            mAudio.mCodecCtx = std::move(avctx);
+            break;
+
+        case AVMEDIA_TYPE_VIDEO:
+            mVideo.mStream = mFormatCtx->streams[stream_index];
+            mVideo.mCodecCtx = std::move(avctx);
+            break;
+
+        default:
+            return -1;
+    }
+
+    return static_cast<int>(stream_index);
+}
+
+int MovieState::parse_handler()
+{
+    auto &audio_queue = mAudio.mQueue;
+    auto &video_queue = mVideo.mQueue;
+
+    int video_index{-1};
+    int audio_index{-1};
+
+    /* Find the first video and audio streams */
+    for(unsigned int i{0u};i < mFormatCtx->nb_streams;i++)
+    {
+        auto codecpar = mFormatCtx->streams[i]->codecpar;
+        if(codecpar->codec_type == AVMEDIA_TYPE_VIDEO && !DisableVideo && video_index < 0)
+            video_index = streamComponentOpen(i);
+        else if(codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0)
+            audio_index = streamComponentOpen(i);
+    }
+
+    {
+        std::unique_lock<std::mutex> slock{mStartupMutex};
+        mStartupDone = true;
+    }
+    mStartupCond.notify_all();
+
+    if(video_index < 0 && audio_index < 0)
+    {
+        std::cerr<< mFilename<<": could not open codecs" <<std::endl;
+        mQuit = true;
+    }
+
+    /* Set the base time 750ms ahead of the current av time. */
+    mClockBase = get_avtime() + milliseconds{750};
+
+    if(audio_index >= 0)
+        mAudioThread = std::thread{std::mem_fn(&AudioState::handler), &mAudio};
+    if(video_index >= 0)
+        mVideoThread = std::thread{std::mem_fn(&VideoState::handler), &mVideo};
+
+    /* Main packet reading/dispatching loop */
+    AVPacketPtr packet{av_packet_alloc()};
+    while(!mQuit.load(std::memory_order_relaxed))
+    {
+        if(av_read_frame(mFormatCtx.get(), packet.get()) < 0)
+            break;
+
+        /* Copy the packet into the queue it's meant for. */
+        if(packet->stream_index == video_index)
+        {
+            while(!mQuit.load(std::memory_order_acquire) && !video_queue.put(packet.get()))
+                std::this_thread::sleep_for(milliseconds{100});
+        }
+        else if(packet->stream_index == audio_index)
+        {
+            while(!mQuit.load(std::memory_order_acquire) && !audio_queue.put(packet.get()))
+                std::this_thread::sleep_for(milliseconds{100});
+        }
+
+        av_packet_unref(packet.get());
+    }
+    /* Finish the queues so the receivers know nothing more is coming. */
+    video_queue.setFinished();
+    audio_queue.setFinished();
+
+    /* all done - wait for it */
+    if(mVideoThread.joinable())
+        mVideoThread.join();
+    if(mAudioThread.joinable())
+        mAudioThread.join();
+
+    mVideo.mEOS = true;
+    std::unique_lock<std::mutex> lock{mVideo.mPictQMutex};
+    while(!mVideo.mFinalUpdate)
+        mVideo.mPictQCond.wait(lock);
+    lock.unlock();
+
+    SDL_Event evt{};
+    evt.user.type = FF_MOVIE_DONE_EVENT;
+    SDL_PushEvent(&evt);
+
+    return 0;
+}
+
+void MovieState::stop()
+{
+    mQuit = true;
+    mAudio.mQueue.flush();
+    mVideo.mQueue.flush();
+}
+
+
+// Helper class+method to print the time with human-readable formatting.
+struct PrettyTime {
+    seconds mTime;
+};
+std::ostream &operator<<(std::ostream &os, const PrettyTime &rhs)
+{
+    using hours = std::chrono::hours;
+    using minutes = std::chrono::minutes;
+
+    seconds t{rhs.mTime};
+    if(t.count() < 0)
+    {
+        os << '-';
+        t *= -1;
+    }
+
+    // Only handle up to hour formatting
+    if(t >= hours{1})
+        os << duration_cast<hours>(t).count() << 'h' << std::setfill('0') << std::setw(2)
+           << (duration_cast<minutes>(t).count() % 60) << 'm';
+    else
+        os << duration_cast<minutes>(t).count() << 'm' << std::setfill('0');
+    os << std::setw(2) << (duration_cast<seconds>(t).count() % 60) << 's' << std::setw(0)
+       << std::setfill(' ');
+    return os;
+}
+
+} // namespace
+
+
+int main(int argc, char *argv[])
+{
+    SDL_SetMainReady();
+
+    std::unique_ptr<MovieState> movState;
+
+    if(argc < 2)
+    {
+        std::cerr<< "Usage: "<<argv[0]<<" [-device <device name>] [-direct] <files...>" <<std::endl;
+        return 1;
+    }
+    /* Register all formats and codecs */
+#if !(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100))
+    av_register_all();
+#endif
+    /* Initialize networking protocols */
+    avformat_network_init();
+
+    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
+    {
+        std::cerr<< "Could not initialize SDL - <<"<<SDL_GetError() <<std::endl;
+        return 1;
+    }
+
+    /* Make a window to put our video */
+    SDL_Window *screen{SDL_CreateWindow(AppName.c_str(), 0, 0, 640, 480, SDL_WINDOW_RESIZABLE)};
+    if(!screen)
+    {
+        std::cerr<< "SDL: could not set video mode - exiting" <<std::endl;
+        return 1;
+    }
+    /* Make a renderer to handle the texture image surface and rendering. */
+    Uint32 render_flags{SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC};
+    SDL_Renderer *renderer{SDL_CreateRenderer(screen, -1, render_flags)};
+    if(renderer)
+    {
+        SDL_RendererInfo rinf{};
+        bool ok{false};
+
+        /* Make sure the renderer supports IYUV textures. If not, fallback to a
+         * software renderer. */
+        if(SDL_GetRendererInfo(renderer, &rinf) == 0)
+        {
+            for(Uint32 i{0u};!ok && i < rinf.num_texture_formats;i++)
+                ok = (rinf.texture_formats[i] == SDL_PIXELFORMAT_IYUV);
+        }
+        if(!ok)
+        {
+            std::cerr<< "IYUV pixelformat textures not supported on renderer "<<rinf.name <<std::endl;
+            SDL_DestroyRenderer(renderer);
+            renderer = nullptr;
+        }
+    }
+    if(!renderer)
+    {
+        render_flags = SDL_RENDERER_SOFTWARE | SDL_RENDERER_PRESENTVSYNC;
+        renderer = SDL_CreateRenderer(screen, -1, render_flags);
+    }
+    if(!renderer)
+    {
+        std::cerr<< "SDL: could not create renderer - exiting" <<std::endl;
+        return 1;
+    }
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+    SDL_RenderFillRect(renderer, nullptr);
+    SDL_RenderPresent(renderer);
+
+    /* Open an audio device */
+    ++argv; --argc;
+    if(InitAL(&argv, &argc))
+    {
+        std::cerr<< "Failed to set up audio device" <<std::endl;
+        return 1;
+    }
+
+    {
+        auto device = alcGetContextsDevice(alcGetCurrentContext());
+        if(alcIsExtensionPresent(device, "ALC_SOFT_device_clock"))
+        {
+            std::cout<< "Found ALC_SOFT_device_clock" <<std::endl;
+            alcGetInteger64vSOFT = reinterpret_cast<LPALCGETINTEGER64VSOFT>(
+                alcGetProcAddress(device, "alcGetInteger64vSOFT")
+            );
+        }
+    }
+
+    if(alIsExtensionPresent("AL_SOFT_source_latency"))
+    {
+        std::cout<< "Found AL_SOFT_source_latency" <<std::endl;
+        alGetSourcei64vSOFT = reinterpret_cast<LPALGETSOURCEI64VSOFT>(
+            alGetProcAddress("alGetSourcei64vSOFT")
+        );
+    }
+    if(alIsExtensionPresent("AL_SOFT_events"))
+    {
+        std::cout<< "Found AL_SOFT_events" <<std::endl;
+        alEventControlSOFT = reinterpret_cast<LPALEVENTCONTROLSOFT>(
+            alGetProcAddress("alEventControlSOFT"));
+        alEventCallbackSOFT = reinterpret_cast<LPALEVENTCALLBACKSOFT>(
+            alGetProcAddress("alEventCallbackSOFT"));
+    }
+    if(alIsExtensionPresent("AL_SOFT_callback_buffer"))
+    {
+        std::cout<< "Found AL_SOFT_callback_buffer" <<std::endl;
+        alBufferCallbackSOFT = reinterpret_cast<LPALBUFFERCALLBACKSOFT>(
+            alGetProcAddress("alBufferCallbackSOFT"));
+    }
+
+    int fileidx{0};
+    for(;fileidx < argc;++fileidx)
+    {
+        if(strcmp(argv[fileidx], "-direct") == 0)
+        {
+            if(alIsExtensionPresent("AL_SOFT_direct_channels_remix"))
+            {
+                std::cout<< "Found AL_SOFT_direct_channels_remix" <<std::endl;
+                DirectOutMode = AL_REMIX_UNMATCHED_SOFT;
+            }
+            else if(alIsExtensionPresent("AL_SOFT_direct_channels"))
+            {
+                std::cout<< "Found AL_SOFT_direct_channels" <<std::endl;
+                DirectOutMode = AL_DROP_UNMATCHED_SOFT;
+            }
+            else
+                std::cerr<< "AL_SOFT_direct_channels not supported for direct output" <<std::endl;
+        }
+        else if(strcmp(argv[fileidx], "-wide") == 0)
+        {
+            if(!alIsExtensionPresent("AL_EXT_STEREO_ANGLES"))
+                std::cerr<< "AL_EXT_STEREO_ANGLES not supported for wide stereo" <<std::endl;
+            else
+            {
+                std::cout<< "Found AL_EXT_STEREO_ANGLES" <<std::endl;
+                EnableWideStereo = true;
+            }
+        }
+        else if(strcmp(argv[fileidx], "-uhj") == 0)
+        {
+            if(!alIsExtensionPresent("AL_SOFT_UHJ"))
+                std::cerr<< "AL_SOFT_UHJ not supported for UHJ decoding" <<std::endl;
+            else
+            {
+                std::cout<< "Found AL_SOFT_UHJ" <<std::endl;
+                EnableUhj = true;
+            }
+        }
+        else if(strcmp(argv[fileidx], "-superstereo") == 0)
+        {
+            if(!alIsExtensionPresent("AL_SOFT_UHJ"))
+                std::cerr<< "AL_SOFT_UHJ not supported for Super Stereo decoding" <<std::endl;
+            else
+            {
+                std::cout<< "Found AL_SOFT_UHJ (Super Stereo)" <<std::endl;
+                EnableSuperStereo = true;
+            }
+        }
+        else if(strcmp(argv[fileidx], "-novideo") == 0)
+            DisableVideo = true;
+        else
+            break;
+    }
+
+    while(fileidx < argc && !movState)
+    {
+        movState = std::unique_ptr<MovieState>{new MovieState{argv[fileidx++]}};
+        if(!movState->prepare()) movState = nullptr;
+    }
+    if(!movState)
+    {
+        std::cerr<< "Could not start a video" <<std::endl;
+        return 1;
+    }
+    movState->setTitle(screen);
+
+    /* Default to going to the next movie at the end of one. */
+    enum class EomAction {
+        Next, Quit
+    } eom_action{EomAction::Next};
+    seconds last_time{seconds::min()};
+    while(1)
+    {
+        /* SDL_WaitEventTimeout is broken, just force a 10ms sleep. */
+        std::this_thread::sleep_for(milliseconds{10});
+
+        auto cur_time = std::chrono::duration_cast<seconds>(movState->getMasterClock());
+        if(cur_time != last_time)
+        {
+            auto end_time = std::chrono::duration_cast<seconds>(movState->getDuration());
+            std::cout<< "    \r "<<PrettyTime{cur_time}<<" / "<<PrettyTime{end_time} <<std::flush;
+            last_time = cur_time;
+        }
+
+        bool force_redraw{false};
+        SDL_Event event{};
+        while(SDL_PollEvent(&event) != 0)
+        {
+            switch(event.type)
+            {
+            case SDL_KEYDOWN:
+                switch(event.key.keysym.sym)
+                {
+                case SDLK_ESCAPE:
+                    movState->stop();
+                    eom_action = EomAction::Quit;
+                    break;
+
+                case SDLK_n:
+                    movState->stop();
+                    eom_action = EomAction::Next;
+                    break;
+
+                default:
+                    break;
+                }
+                break;
+
+            case SDL_WINDOWEVENT:
+                switch(event.window.event)
+                {
+                case SDL_WINDOWEVENT_RESIZED:
+                    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+                    SDL_RenderFillRect(renderer, nullptr);
+                    force_redraw = true;
+                    break;
+
+                case SDL_WINDOWEVENT_EXPOSED:
+                    force_redraw = true;
+                    break;
+
+                default:
+                    break;
+                }
+                break;
+
+            case SDL_QUIT:
+                movState->stop();
+                eom_action = EomAction::Quit;
+                break;
+
+            case FF_MOVIE_DONE_EVENT:
+                std::cout<<'\n';
+                last_time = seconds::min();
+                if(eom_action != EomAction::Quit)
+                {
+                    movState = nullptr;
+                    while(fileidx < argc && !movState)
+                    {
+                        movState = std::unique_ptr<MovieState>{new MovieState{argv[fileidx++]}};
+                        if(!movState->prepare()) movState = nullptr;
+                    }
+                    if(movState)
+                    {
+                        movState->setTitle(screen);
+                        break;
+                    }
+                }
+
+                /* Nothing more to play. Shut everything down and quit. */
+                movState = nullptr;
+
+                CloseAL();
+
+                SDL_DestroyRenderer(renderer);
+                renderer = nullptr;
+                SDL_DestroyWindow(screen);
+                screen = nullptr;
+
+                SDL_Quit();
+                exit(0);
+
+            default:
+                break;
+            }
+        }
+
+        movState->mVideo.updateVideo(screen, renderer, force_redraw);
+    }
+
+    std::cerr<< "SDL_WaitEvent error - "<<SDL_GetError() <<std::endl;
+    return 1;
+}
diff --git a/examples/alhrtf.c b/examples/alhrtf.c
new file mode 100644 (file)
index 0000000..d878870
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * OpenAL HRTF Example
+ *
+ * Copyright (c) 2015 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains an example for selecting an HRTF. */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+
+#ifndef M_PI
+#define M_PI                         (3.14159265358979323846)
+#endif
+
+static LPALCGETSTRINGISOFT alcGetStringiSOFT;
+static LPALCRESETDEVICESOFT alcResetDeviceSOFT;
+
+/* LoadBuffer loads the named audio file into an OpenAL buffer object, and
+ * returns the new buffer ID.
+ */
+static ALuint LoadSound(const char *filename)
+{
+    ALenum err, format;
+    ALuint buffer;
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    short *membuf;
+    sf_count_t num_frames;
+    ALsizei num_bytes;
+
+    /* Open the audio file and check that it's usable. */
+    sndfile = sf_open(filename, SFM_READ, &sfinfo);
+    if(!sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(sndfile));
+        return 0;
+    }
+    if(sfinfo.frames < 1 || sfinfo.frames > (sf_count_t)(INT_MAX/sizeof(short))/sfinfo.channels)
+    {
+        fprintf(stderr, "Bad sample count in %s (%" PRId64 ")\n", filename, sfinfo.frames);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Get the sound format, and figure out the OpenAL format */
+    format = AL_NONE;
+    if(sfinfo.channels == 1)
+        format = AL_FORMAT_MONO16;
+    else if(sfinfo.channels == 2)
+        format = AL_FORMAT_STEREO16;
+    else if(sfinfo.channels == 3)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT2D_16;
+    }
+    else if(sfinfo.channels == 4)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT3D_16;
+    }
+    if(!format)
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", sfinfo.channels);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Decode the whole audio file to a buffer. */
+    membuf = malloc((size_t)(sfinfo.frames * sfinfo.channels) * sizeof(short));
+
+    num_frames = sf_readf_short(sndfile, membuf, sfinfo.frames);
+    if(num_frames < 1)
+    {
+        free(membuf);
+        sf_close(sndfile);
+        fprintf(stderr, "Failed to read samples in %s (%" PRId64 ")\n", filename, num_frames);
+        return 0;
+    }
+    num_bytes = (ALsizei)(num_frames * sfinfo.channels) * (ALsizei)sizeof(short);
+
+    /* Buffer the audio data into a new buffer object, then free the data and
+     * close the file.
+     */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    alBufferData(buffer, format, membuf, num_bytes, sfinfo.samplerate);
+
+    free(membuf);
+    sf_close(sndfile);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(buffer && alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+int main(int argc, char **argv)
+{
+    ALCdevice *device;
+    ALCcontext *context;
+    ALboolean has_angle_ext;
+    ALuint source, buffer;
+    const char *soundname;
+    const char *hrtfname;
+    ALCint hrtf_state;
+    ALCint num_hrtf;
+    ALdouble angle;
+    ALenum state;
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name>] [-hrtf <name>] <soundfile>\n", argv[0]);
+        return 1;
+    }
+
+    /* Initialize OpenAL, and check for HRTF support. */
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    context = alcGetCurrentContext();
+    device = alcGetContextsDevice(context);
+    if(!alcIsExtensionPresent(device, "ALC_SOFT_HRTF"))
+    {
+        fprintf(stderr, "Error: ALC_SOFT_HRTF not supported\n");
+        CloseAL();
+        return 1;
+    }
+
+    /* Define a macro to help load the function pointers. */
+#define LOAD_PROC(d, T, x)  ((x) = FUNCTION_CAST(T, alcGetProcAddress((d), #x)))
+    LOAD_PROC(device, LPALCGETSTRINGISOFT, alcGetStringiSOFT);
+    LOAD_PROC(device, LPALCRESETDEVICESOFT, alcResetDeviceSOFT);
+#undef LOAD_PROC
+
+    /* Check for the AL_EXT_STEREO_ANGLES extension to be able to also rotate
+     * stereo sources.
+     */
+    has_angle_ext = alIsExtensionPresent("AL_EXT_STEREO_ANGLES");
+    printf("AL_EXT_STEREO_ANGLES %sfound\n", has_angle_ext?"":"not ");
+
+    /* Check for user-preferred HRTF */
+    if(strcmp(argv[0], "-hrtf") == 0)
+    {
+        hrtfname = argv[1];
+        soundname = argv[2];
+    }
+    else
+    {
+        hrtfname = NULL;
+        soundname = argv[0];
+    }
+
+    /* Enumerate available HRTFs, and reset the device using one. */
+    alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf);
+    if(!num_hrtf)
+        printf("No HRTFs found\n");
+    else
+    {
+        ALCint attr[5];
+        ALCint index = -1;
+        ALCint i;
+
+        printf("Available HRTFs:\n");
+        for(i = 0;i < num_hrtf;i++)
+        {
+            const ALCchar *name = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i);
+            printf("    %d: %s\n", i, name);
+
+            /* Check if this is the HRTF the user requested. */
+            if(hrtfname && strcmp(name, hrtfname) == 0)
+                index = i;
+        }
+
+        i = 0;
+        attr[i++] = ALC_HRTF_SOFT;
+        attr[i++] = ALC_TRUE;
+        if(index == -1)
+        {
+            if(hrtfname)
+                printf("HRTF \"%s\" not found\n", hrtfname);
+            printf("Using default HRTF...\n");
+        }
+        else
+        {
+            printf("Selecting HRTF %d...\n", index);
+            attr[i++] = ALC_HRTF_ID_SOFT;
+            attr[i++] = index;
+        }
+        attr[i] = 0;
+
+        if(!alcResetDeviceSOFT(device, attr))
+            printf("Failed to reset device: %s\n", alcGetString(device, alcGetError(device)));
+    }
+
+    /* Check if HRTF is enabled, and show which is being used. */
+    alcGetIntegerv(device, ALC_HRTF_SOFT, 1, &hrtf_state);
+    if(!hrtf_state)
+        printf("HRTF not enabled!\n");
+    else
+    {
+        const ALchar *name = alcGetString(device, ALC_HRTF_SPECIFIER_SOFT);
+        printf("HRTF enabled, using %s\n", name);
+    }
+    fflush(stdout);
+
+    /* Load the sound into a buffer. */
+    buffer = LoadSound(soundname);
+    if(!buffer)
+    {
+        CloseAL();
+        return 1;
+    }
+
+    /* Create the source to play the sound with. */
+    source = 0;
+    alGenSources(1, &source);
+    alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE);
+    alSource3f(source, AL_POSITION, 0.0f, 0.0f, -1.0f);
+    alSourcei(source, AL_BUFFER, (ALint)buffer);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Play the sound until it finishes. */
+    angle = 0.0;
+    alSourcePlay(source);
+    do {
+        al_nssleep(10000000);
+
+        alcSuspendContext(context);
+
+        /* Rotate the source around the listener by about 1/4 cycle per second,
+         * and keep it within -pi...+pi.
+         */
+        angle += 0.01 * M_PI * 0.5;
+        if(angle > M_PI)
+            angle -= M_PI*2.0;
+
+        /* This only rotates mono sounds. */
+        alSource3f(source, AL_POSITION, (ALfloat)sin(angle), 0.0f, -(ALfloat)cos(angle));
+
+        if(has_angle_ext)
+        {
+            /* This rotates stereo sounds with the AL_EXT_STEREO_ANGLES
+             * extension. Angles are specified counter-clockwise in radians.
+             */
+            ALfloat angles[2] = { (ALfloat)(M_PI/6.0 - angle), (ALfloat)(-M_PI/6.0 - angle) };
+            alSourcefv(source, AL_STEREO_ANGLES, angles);
+        }
+        alcProcessContext(context);
+
+        alGetSourcei(source, AL_SOURCE_STATE, &state);
+    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);
+
+    /* All done. Delete resources, and close down OpenAL. */
+    alDeleteSources(1, &source);
+    alDeleteBuffers(1, &buffer);
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/allatency.c b/examples/allatency.c
new file mode 100644 (file)
index 0000000..ab4a4eb
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * OpenAL Source Latency Example
+ *
+ * Copyright (c) 2012 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains an example for checking the latency of a sound. */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+
+static LPALSOURCEDSOFT alSourcedSOFT;
+static LPALSOURCE3DSOFT alSource3dSOFT;
+static LPALSOURCEDVSOFT alSourcedvSOFT;
+static LPALGETSOURCEDSOFT alGetSourcedSOFT;
+static LPALGETSOURCE3DSOFT alGetSource3dSOFT;
+static LPALGETSOURCEDVSOFT alGetSourcedvSOFT;
+static LPALSOURCEI64SOFT alSourcei64SOFT;
+static LPALSOURCE3I64SOFT alSource3i64SOFT;
+static LPALSOURCEI64VSOFT alSourcei64vSOFT;
+static LPALGETSOURCEI64SOFT alGetSourcei64SOFT;
+static LPALGETSOURCE3I64SOFT alGetSource3i64SOFT;
+static LPALGETSOURCEI64VSOFT alGetSourcei64vSOFT;
+
+/* LoadBuffer loads the named audio file into an OpenAL buffer object, and
+ * returns the new buffer ID.
+ */
+static ALuint LoadSound(const char *filename)
+{
+    ALenum err, format;
+    ALuint buffer;
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    short *membuf;
+    sf_count_t num_frames;
+    ALsizei num_bytes;
+
+    /* Open the audio file and check that it's usable. */
+    sndfile = sf_open(filename, SFM_READ, &sfinfo);
+    if(!sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(sndfile));
+        return 0;
+    }
+    if(sfinfo.frames < 1 || sfinfo.frames > (sf_count_t)(INT_MAX/sizeof(short))/sfinfo.channels)
+    {
+        fprintf(stderr, "Bad sample count in %s (%" PRId64 ")\n", filename, sfinfo.frames);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Get the sound format, and figure out the OpenAL format */
+    format = AL_NONE;
+    if(sfinfo.channels == 1)
+        format = AL_FORMAT_MONO16;
+    else if(sfinfo.channels == 2)
+        format = AL_FORMAT_STEREO16;
+    else if(sfinfo.channels == 3)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT2D_16;
+    }
+    else if(sfinfo.channels == 4)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT3D_16;
+    }
+    if(!format)
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", sfinfo.channels);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Decode the whole audio file to a buffer. */
+    membuf = malloc((size_t)(sfinfo.frames * sfinfo.channels) * sizeof(short));
+
+    num_frames = sf_readf_short(sndfile, membuf, sfinfo.frames);
+    if(num_frames < 1)
+    {
+        free(membuf);
+        sf_close(sndfile);
+        fprintf(stderr, "Failed to read samples in %s (%" PRId64 ")\n", filename, num_frames);
+        return 0;
+    }
+    num_bytes = (ALsizei)(num_frames * sfinfo.channels) * (ALsizei)sizeof(short);
+
+    /* Buffer the audio data into a new buffer object, then free the data and
+     * close the file.
+     */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    alBufferData(buffer, format, membuf, num_bytes, sfinfo.samplerate);
+
+    free(membuf);
+    sf_close(sndfile);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(buffer && alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+int main(int argc, char **argv)
+{
+    ALuint source, buffer;
+    ALdouble offsets[2];
+    ALenum state;
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name>] <filename>\n", argv[0]);
+        return 1;
+    }
+
+    /* Initialize OpenAL, and check for source_latency support. */
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    if(!alIsExtensionPresent("AL_SOFT_source_latency"))
+    {
+        fprintf(stderr, "Error: AL_SOFT_source_latency not supported\n");
+        CloseAL();
+        return 1;
+    }
+
+    /* Define a macro to help load the function pointers. */
+#define LOAD_PROC(T, x)  ((x) = FUNCTION_CAST(T, alGetProcAddress(#x)))
+    LOAD_PROC(LPALSOURCEDSOFT, alSourcedSOFT);
+    LOAD_PROC(LPALSOURCE3DSOFT, alSource3dSOFT);
+    LOAD_PROC(LPALSOURCEDVSOFT, alSourcedvSOFT);
+    LOAD_PROC(LPALGETSOURCEDSOFT, alGetSourcedSOFT);
+    LOAD_PROC(LPALGETSOURCE3DSOFT, alGetSource3dSOFT);
+    LOAD_PROC(LPALGETSOURCEDVSOFT, alGetSourcedvSOFT);
+    LOAD_PROC(LPALSOURCEI64SOFT, alSourcei64SOFT);
+    LOAD_PROC(LPALSOURCE3I64SOFT, alSource3i64SOFT);
+    LOAD_PROC(LPALSOURCEI64VSOFT, alSourcei64vSOFT);
+    LOAD_PROC(LPALGETSOURCEI64SOFT, alGetSourcei64SOFT);
+    LOAD_PROC(LPALGETSOURCE3I64SOFT, alGetSource3i64SOFT);
+    LOAD_PROC(LPALGETSOURCEI64VSOFT, alGetSourcei64vSOFT);
+#undef LOAD_PROC
+
+    /* Load the sound into a buffer. */
+    buffer = LoadSound(argv[0]);
+    if(!buffer)
+    {
+        CloseAL();
+        return 1;
+    }
+
+    /* Create the source to play the sound with. */
+    source = 0;
+    alGenSources(1, &source);
+    alSourcei(source, AL_BUFFER, (ALint)buffer);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Play the sound until it finishes. */
+    alSourcePlay(source);
+    do {
+        al_nssleep(10000000);
+        alGetSourcei(source, AL_SOURCE_STATE, &state);
+
+        /* Get the source offset and latency. AL_SEC_OFFSET_LATENCY_SOFT will
+         * place the offset (in seconds) in offsets[0], and the time until that
+         * offset will be heard (in seconds) in offsets[1]. */
+        alGetSourcedvSOFT(source, AL_SEC_OFFSET_LATENCY_SOFT, offsets);
+        printf("\rOffset: %f - Latency:%3u ms  ", offsets[0], (ALuint)(offsets[1]*1000));
+        fflush(stdout);
+    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);
+    printf("\n");
+
+    /* All done. Delete resources, and close down OpenAL. */
+    alDeleteSources(1, &source);
+    alDeleteBuffers(1, &buffer);
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/alloopback.c b/examples/alloopback.c
new file mode 100644 (file)
index 0000000..56cd420
--- /dev/null
@@ -0,0 +1,293 @@
+/*
+ * OpenAL Loopback Example
+ *
+ * Copyright (c) 2013 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains an example for using the loopback device for custom
+ * output handling.
+ */
+
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+
+#define SDL_MAIN_HANDLED
+#include "SDL.h"
+#include "SDL_audio.h"
+#include "SDL_error.h"
+#include "SDL_stdinc.h"
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+#ifndef SDL_AUDIO_MASK_BITSIZE
+#define SDL_AUDIO_MASK_BITSIZE (0xFF)
+#endif
+#ifndef SDL_AUDIO_BITSIZE
+#define SDL_AUDIO_BITSIZE(x) (x & SDL_AUDIO_MASK_BITSIZE)
+#endif
+
+#ifndef M_PI
+#define M_PI    (3.14159265358979323846)
+#endif
+
+typedef struct {
+    ALCdevice *Device;
+    ALCcontext *Context;
+
+    ALCsizei FrameSize;
+} PlaybackInfo;
+
+static LPALCLOOPBACKOPENDEVICESOFT alcLoopbackOpenDeviceSOFT;
+static LPALCISRENDERFORMATSUPPORTEDSOFT alcIsRenderFormatSupportedSOFT;
+static LPALCRENDERSAMPLESSOFT alcRenderSamplesSOFT;
+
+
+void SDLCALL RenderSDLSamples(void *userdata, Uint8 *stream, int len)
+{
+    PlaybackInfo *playback = (PlaybackInfo*)userdata;
+    alcRenderSamplesSOFT(playback->Device, stream, len/playback->FrameSize);
+}
+
+
+static const char *ChannelsName(ALCenum chans)
+{
+    switch(chans)
+    {
+    case ALC_MONO_SOFT: return "Mono";
+    case ALC_STEREO_SOFT: return "Stereo";
+    case ALC_QUAD_SOFT: return "Quadraphonic";
+    case ALC_5POINT1_SOFT: return "5.1 Surround";
+    case ALC_6POINT1_SOFT: return "6.1 Surround";
+    case ALC_7POINT1_SOFT: return "7.1 Surround";
+    }
+    return "Unknown Channels";
+}
+
+static const char *TypeName(ALCenum type)
+{
+    switch(type)
+    {
+    case ALC_BYTE_SOFT: return "S8";
+    case ALC_UNSIGNED_BYTE_SOFT: return "U8";
+    case ALC_SHORT_SOFT: return "S16";
+    case ALC_UNSIGNED_SHORT_SOFT: return "U16";
+    case ALC_INT_SOFT: return "S32";
+    case ALC_UNSIGNED_INT_SOFT: return "U32";
+    case ALC_FLOAT_SOFT: return "Float32";
+    }
+    return "Unknown Type";
+}
+
+/* Creates a one second buffer containing a sine wave, and returns the new
+ * buffer ID. */
+static ALuint CreateSineWave(void)
+{
+    ALshort data[44100*4];
+    ALuint buffer;
+    ALenum err;
+    ALuint i;
+
+    for(i = 0;i < 44100*4;i++)
+        data[i] = (ALshort)(sin(i/44100.0 * 1000.0 * 2.0*M_PI) * 32767.0);
+
+    /* Buffer the audio data into a new buffer object. */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    alBufferData(buffer, AL_FORMAT_MONO16, data, sizeof(data), 44100);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+int main(int argc, char *argv[])
+{
+    PlaybackInfo playback = { NULL, NULL, 0 };
+    SDL_AudioSpec desired, obtained;
+    ALuint source, buffer;
+    ALCint attrs[16];
+    ALenum state;
+    (void)argc;
+    (void)argv;
+
+    SDL_SetMainReady();
+
+    /* Print out error if extension is missing. */
+    if(!alcIsExtensionPresent(NULL, "ALC_SOFT_loopback"))
+    {
+        fprintf(stderr, "Error: ALC_SOFT_loopback not supported!\n");
+        return 1;
+    }
+
+    /* Define a macro to help load the function pointers. */
+#define LOAD_PROC(T, x)  ((x) = FUNCTION_CAST(T, alcGetProcAddress(NULL, #x)))
+    LOAD_PROC(LPALCLOOPBACKOPENDEVICESOFT, alcLoopbackOpenDeviceSOFT);
+    LOAD_PROC(LPALCISRENDERFORMATSUPPORTEDSOFT, alcIsRenderFormatSupportedSOFT);
+    LOAD_PROC(LPALCRENDERSAMPLESSOFT, alcRenderSamplesSOFT);
+#undef LOAD_PROC
+
+    if(SDL_Init(SDL_INIT_AUDIO) == -1)
+    {
+        fprintf(stderr, "Failed to init SDL audio: %s\n", SDL_GetError());
+        return 1;
+    }
+
+    /* Set up SDL audio with our requested format and callback. */
+    desired.channels = 2;
+    desired.format = AUDIO_S16SYS;
+    desired.freq = 44100;
+    desired.padding = 0;
+    desired.samples = 4096;
+    desired.callback = RenderSDLSamples;
+    desired.userdata = &playback;
+    if(SDL_OpenAudio(&desired, &obtained) != 0)
+    {
+        SDL_Quit();
+        fprintf(stderr, "Failed to open SDL audio: %s\n", SDL_GetError());
+        return 1;
+    }
+
+    /* Set up our OpenAL attributes based on what we got from SDL. */
+    attrs[0] = ALC_FORMAT_CHANNELS_SOFT;
+    if(obtained.channels == 1)
+        attrs[1] = ALC_MONO_SOFT;
+    else if(obtained.channels == 2)
+        attrs[1] = ALC_STEREO_SOFT;
+    else
+    {
+        fprintf(stderr, "Unhandled SDL channel count: %d\n", obtained.channels);
+        goto error;
+    }
+
+    attrs[2] = ALC_FORMAT_TYPE_SOFT;
+    if(obtained.format == AUDIO_U8)
+        attrs[3] = ALC_UNSIGNED_BYTE_SOFT;
+    else if(obtained.format == AUDIO_S8)
+        attrs[3] = ALC_BYTE_SOFT;
+    else if(obtained.format == AUDIO_U16SYS)
+        attrs[3] = ALC_UNSIGNED_SHORT_SOFT;
+    else if(obtained.format == AUDIO_S16SYS)
+        attrs[3] = ALC_SHORT_SOFT;
+    else if(obtained.format == AUDIO_S32SYS)
+        attrs[3] = ALC_INT_SOFT;
+    else if(obtained.format == AUDIO_F32SYS)
+        attrs[3] = ALC_FLOAT_SOFT;
+    else
+    {
+        fprintf(stderr, "Unhandled SDL format: 0x%04x\n", obtained.format);
+        goto error;
+    }
+
+    attrs[4] = ALC_FREQUENCY;
+    attrs[5] = obtained.freq;
+
+    attrs[6] = 0; /* end of list */
+
+    playback.FrameSize = obtained.channels * SDL_AUDIO_BITSIZE(obtained.format) / 8;
+
+    /* Initialize OpenAL loopback device, using our format attributes. */
+    playback.Device = alcLoopbackOpenDeviceSOFT(NULL);
+    if(!playback.Device)
+    {
+        fprintf(stderr, "Failed to open loopback device!\n");
+        goto error;
+    }
+    /* Make sure the format is supported before setting them on the device. */
+    if(alcIsRenderFormatSupportedSOFT(playback.Device, attrs[5], attrs[1], attrs[3]) == ALC_FALSE)
+    {
+        fprintf(stderr, "Render format not supported: %s, %s, %dhz\n",
+                        ChannelsName(attrs[1]), TypeName(attrs[3]), attrs[5]);
+        goto error;
+    }
+    playback.Context = alcCreateContext(playback.Device, attrs);
+    if(!playback.Context || alcMakeContextCurrent(playback.Context) == ALC_FALSE)
+    {
+        fprintf(stderr, "Failed to set an OpenAL audio context\n");
+        goto error;
+    }
+
+    /* Start SDL playing. Our callback (thus alcRenderSamplesSOFT) will now
+     * start being called regularly to update the AL playback state. */
+    SDL_PauseAudio(0);
+
+    /* Load the sound into a buffer. */
+    buffer = CreateSineWave();
+    if(!buffer)
+    {
+        SDL_CloseAudio();
+        alcDestroyContext(playback.Context);
+        alcCloseDevice(playback.Device);
+        SDL_Quit();
+        return 1;
+    }
+
+    /* Create the source to play the sound with. */
+    source = 0;
+    alGenSources(1, &source);
+    alSourcei(source, AL_BUFFER, (ALint)buffer);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Play the sound until it finishes. */
+    alSourcePlay(source);
+    do {
+        al_nssleep(10000000);
+        alGetSourcei(source, AL_SOURCE_STATE, &state);
+    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);
+
+    /* All done. Delete resources, and close OpenAL. */
+    alDeleteSources(1, &source);
+    alDeleteBuffers(1, &buffer);
+
+    /* Stop SDL playing. */
+    SDL_PauseAudio(1);
+
+    /* Close up OpenAL and SDL. */
+    SDL_CloseAudio();
+    alcDestroyContext(playback.Context);
+    alcCloseDevice(playback.Device);
+    SDL_Quit();
+
+    return 0;
+
+error:
+    SDL_CloseAudio();
+    if(playback.Context)
+        alcDestroyContext(playback.Context);
+    if(playback.Device)
+        alcCloseDevice(playback.Device);
+    SDL_Quit();
+
+    return 1;
+}
diff --git a/examples/almultireverb.c b/examples/almultireverb.c
new file mode 100644 (file)
index 0000000..a77cc59
--- /dev/null
@@ -0,0 +1,688 @@
+/*
+ * OpenAL Multi-Zone Reverb Example
+ *
+ * Copyright (c) 2018 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains an example for controlling multiple reverb zones to
+ * smoothly transition between reverb environments. The general concept is to
+ * extend single-reverb by also tracking the closest adjacent environment, and
+ * utilize EAX Reverb's panning vectors to position them relative to the
+ * listener.
+ */
+
+
+#include <assert.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/efx.h"
+#include "AL/efx-presets.h"
+
+#include "common/alhelpers.h"
+
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+
+/* Filter object functions */
+static LPALGENFILTERS alGenFilters;
+static LPALDELETEFILTERS alDeleteFilters;
+static LPALISFILTER alIsFilter;
+static LPALFILTERI alFilteri;
+static LPALFILTERIV alFilteriv;
+static LPALFILTERF alFilterf;
+static LPALFILTERFV alFilterfv;
+static LPALGETFILTERI alGetFilteri;
+static LPALGETFILTERIV alGetFilteriv;
+static LPALGETFILTERF alGetFilterf;
+static LPALGETFILTERFV alGetFilterfv;
+
+/* Effect object functions */
+static LPALGENEFFECTS alGenEffects;
+static LPALDELETEEFFECTS alDeleteEffects;
+static LPALISEFFECT alIsEffect;
+static LPALEFFECTI alEffecti;
+static LPALEFFECTIV alEffectiv;
+static LPALEFFECTF alEffectf;
+static LPALEFFECTFV alEffectfv;
+static LPALGETEFFECTI alGetEffecti;
+static LPALGETEFFECTIV alGetEffectiv;
+static LPALGETEFFECTF alGetEffectf;
+static LPALGETEFFECTFV alGetEffectfv;
+
+/* Auxiliary Effect Slot object functions */
+static LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots;
+static LPALDELETEAUXILIARYEFFECTSLOTS alDeleteAuxiliaryEffectSlots;
+static LPALISAUXILIARYEFFECTSLOT alIsAuxiliaryEffectSlot;
+static LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti;
+static LPALAUXILIARYEFFECTSLOTIV alAuxiliaryEffectSlotiv;
+static LPALAUXILIARYEFFECTSLOTF alAuxiliaryEffectSlotf;
+static LPALAUXILIARYEFFECTSLOTFV alAuxiliaryEffectSlotfv;
+static LPALGETAUXILIARYEFFECTSLOTI alGetAuxiliaryEffectSloti;
+static LPALGETAUXILIARYEFFECTSLOTIV alGetAuxiliaryEffectSlotiv;
+static LPALGETAUXILIARYEFFECTSLOTF alGetAuxiliaryEffectSlotf;
+static LPALGETAUXILIARYEFFECTSLOTFV alGetAuxiliaryEffectSlotfv;
+
+
+/* LoadEffect loads the given initial reverb properties into the given OpenAL
+ * effect object, and returns non-zero on success.
+ */
+static int LoadEffect(ALuint effect, const EFXEAXREVERBPROPERTIES *reverb)
+{
+    ALenum err;
+
+    alGetError();
+
+    /* Prepare the effect for EAX Reverb (standard reverb doesn't contain
+     * the needed panning vectors).
+     */
+    alEffecti(effect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB);
+    if((err=alGetError()) != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Failed to set EAX Reverb: %s (0x%04x)\n", alGetString(err), err);
+        return 0;
+    }
+
+    /* Load the reverb properties. */
+    alEffectf(effect, AL_EAXREVERB_DENSITY, reverb->flDensity);
+    alEffectf(effect, AL_EAXREVERB_DIFFUSION, reverb->flDiffusion);
+    alEffectf(effect, AL_EAXREVERB_GAIN, reverb->flGain);
+    alEffectf(effect, AL_EAXREVERB_GAINHF, reverb->flGainHF);
+    alEffectf(effect, AL_EAXREVERB_GAINLF, reverb->flGainLF);
+    alEffectf(effect, AL_EAXREVERB_DECAY_TIME, reverb->flDecayTime);
+    alEffectf(effect, AL_EAXREVERB_DECAY_HFRATIO, reverb->flDecayHFRatio);
+    alEffectf(effect, AL_EAXREVERB_DECAY_LFRATIO, reverb->flDecayLFRatio);
+    alEffectf(effect, AL_EAXREVERB_REFLECTIONS_GAIN, reverb->flReflectionsGain);
+    alEffectf(effect, AL_EAXREVERB_REFLECTIONS_DELAY, reverb->flReflectionsDelay);
+    alEffectfv(effect, AL_EAXREVERB_REFLECTIONS_PAN, reverb->flReflectionsPan);
+    alEffectf(effect, AL_EAXREVERB_LATE_REVERB_GAIN, reverb->flLateReverbGain);
+    alEffectf(effect, AL_EAXREVERB_LATE_REVERB_DELAY, reverb->flLateReverbDelay);
+    alEffectfv(effect, AL_EAXREVERB_LATE_REVERB_PAN, reverb->flLateReverbPan);
+    alEffectf(effect, AL_EAXREVERB_ECHO_TIME, reverb->flEchoTime);
+    alEffectf(effect, AL_EAXREVERB_ECHO_DEPTH, reverb->flEchoDepth);
+    alEffectf(effect, AL_EAXREVERB_MODULATION_TIME, reverb->flModulationTime);
+    alEffectf(effect, AL_EAXREVERB_MODULATION_DEPTH, reverb->flModulationDepth);
+    alEffectf(effect, AL_EAXREVERB_AIR_ABSORPTION_GAINHF, reverb->flAirAbsorptionGainHF);
+    alEffectf(effect, AL_EAXREVERB_HFREFERENCE, reverb->flHFReference);
+    alEffectf(effect, AL_EAXREVERB_LFREFERENCE, reverb->flLFReference);
+    alEffectf(effect, AL_EAXREVERB_ROOM_ROLLOFF_FACTOR, reverb->flRoomRolloffFactor);
+    alEffecti(effect, AL_EAXREVERB_DECAY_HFLIMIT, reverb->iDecayHFLimit);
+
+    /* Check if an error occured, and return failure if so. */
+    if((err=alGetError()) != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Error setting up reverb: %s\n", alGetString(err));
+        return 0;
+    }
+
+    return 1;
+}
+
+
+/* LoadBuffer loads the named audio file into an OpenAL buffer object, and
+ * returns the new buffer ID.
+ */
+static ALuint LoadSound(const char *filename)
+{
+    ALenum err, format;
+    ALuint buffer;
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    short *membuf;
+    sf_count_t num_frames;
+    ALsizei num_bytes;
+
+    /* Open the audio file and check that it's usable. */
+    sndfile = sf_open(filename, SFM_READ, &sfinfo);
+    if(!sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(sndfile));
+        return 0;
+    }
+    if(sfinfo.frames < 1 || sfinfo.frames > (sf_count_t)(INT_MAX/sizeof(short))/sfinfo.channels)
+    {
+        fprintf(stderr, "Bad sample count in %s (%" PRId64 ")\n", filename, sfinfo.frames);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Get the sound format, and figure out the OpenAL format */
+    if(sfinfo.channels == 1)
+        format = AL_FORMAT_MONO16;
+    else if(sfinfo.channels == 2)
+        format = AL_FORMAT_STEREO16;
+    else
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", sfinfo.channels);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Decode the whole audio file to a buffer. */
+    membuf = malloc((size_t)(sfinfo.frames * sfinfo.channels) * sizeof(short));
+
+    num_frames = sf_readf_short(sndfile, membuf, sfinfo.frames);
+    if(num_frames < 1)
+    {
+        free(membuf);
+        sf_close(sndfile);
+        fprintf(stderr, "Failed to read samples in %s (%" PRId64 ")\n", filename, num_frames);
+        return 0;
+    }
+    num_bytes = (ALsizei)(num_frames * sfinfo.channels) * (ALsizei)sizeof(short);
+
+    /* Buffer the audio data into a new buffer object, then free the data and
+     * close the file.
+     */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    alBufferData(buffer, format, membuf, num_bytes, sfinfo.samplerate);
+
+    free(membuf);
+    sf_close(sndfile);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(buffer && alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+/* Helper to calculate the dot-product of the two given vectors. */
+static ALfloat dot_product(const ALfloat vec0[3], const ALfloat vec1[3])
+{
+    return vec0[0]*vec1[0] + vec0[1]*vec1[1] + vec0[2]*vec1[2];
+}
+
+/* Helper to normalize a given vector. */
+static void normalize(ALfloat vec[3])
+{
+    ALfloat mag = sqrtf(dot_product(vec, vec));
+    if(mag > 0.00001f)
+    {
+        vec[0] /= mag;
+        vec[1] /= mag;
+        vec[2] /= mag;
+    }
+    else
+    {
+        vec[0] = 0.0f;
+        vec[1] = 0.0f;
+        vec[2] = 0.0f;
+    }
+}
+
+
+/* The main update function to update the listener and environment effects. */
+static void UpdateListenerAndEffects(float timediff, const ALuint slots[2], const ALuint effects[2], const EFXEAXREVERBPROPERTIES reverbs[2])
+{
+    static const ALfloat listener_move_scale = 10.0f;
+    /* Individual reverb zones are connected via "portals". Each portal has a
+     * position (center point of the connecting area), a normal (facing
+     * direction), and a radius (approximate size of the connecting area).
+     */
+    const ALfloat portal_pos[3] = { 0.0f, 0.0f, 0.0f };
+    const ALfloat portal_norm[3] = { sqrtf(0.5f), 0.0f, -sqrtf(0.5f) };
+    const ALfloat portal_radius = 2.5f;
+    ALfloat other_dir[3], this_dir[3];
+    ALfloat listener_pos[3];
+    ALfloat local_norm[3];
+    ALfloat local_dir[3];
+    ALfloat near_edge[3];
+    ALfloat far_edge[3];
+    ALfloat dist, edist;
+
+    /* Update the listener position for the amount of time passed. This uses a
+     * simple triangular LFO to offset the position (moves along the X axis
+     * between -listener_move_scale and +listener_move_scale for each
+     * transition).
+     */
+    listener_pos[0] = (fabsf(2.0f - timediff/2.0f) - 1.0f) * listener_move_scale;
+    listener_pos[1] = 0.0f;
+    listener_pos[2] = 0.0f;
+    alListenerfv(AL_POSITION, listener_pos);
+
+    /* Calculate local_dir, which represents the listener-relative point to the
+     * adjacent zone (should also include orientation). Because EAX Reverb uses
+     * left-handed coordinates instead of right-handed like the rest of OpenAL,
+     * negate Z for the local values.
+     */
+    local_dir[0] = portal_pos[0] - listener_pos[0];
+    local_dir[1] = portal_pos[1] - listener_pos[1];
+    local_dir[2] = -(portal_pos[2] - listener_pos[2]);
+    /* A normal application would also rotate the portal's normal given the
+     * listener orientation, to get the listener-relative normal.
+     */
+    local_norm[0] = portal_norm[0];
+    local_norm[1] = portal_norm[1];
+    local_norm[2] = -portal_norm[2];
+
+    /* Calculate the distance from the listener to the portal, and ensure it's
+     * far enough away to not suffer severe floating-point precision issues.
+     */
+    dist = sqrtf(dot_product(local_dir, local_dir));
+    if(dist > 0.00001f)
+    {
+        const EFXEAXREVERBPROPERTIES *other_reverb, *this_reverb;
+        ALuint other_effect, this_effect;
+        ALfloat magnitude, dir_dot_norm;
+
+        /* Normalize the direction to the portal. */
+        local_dir[0] /= dist;
+        local_dir[1] /= dist;
+        local_dir[2] /= dist;
+
+        /* Calculate the dot product of the portal's local direction and local
+         * normal, which is used for angular and side checks later on.
+         */
+        dir_dot_norm = dot_product(local_dir, local_norm);
+
+        /* Figure out which zone we're in. */
+        if(dir_dot_norm <= 0.0f)
+        {
+            /* We're in front of the portal, so we're in Zone 0. */
+            this_effect = effects[0];
+            other_effect = effects[1];
+            this_reverb = &reverbs[0];
+            other_reverb = &reverbs[1];
+        }
+        else
+        {
+            /* We're behind the portal, so we're in Zone 1. */
+            this_effect = effects[1];
+            other_effect = effects[0];
+            this_reverb = &reverbs[1];
+            other_reverb = &reverbs[0];
+        }
+
+        /* Calculate the listener-relative extents of the portal. */
+        /* First, project the listener-to-portal vector onto the portal's plane
+         * to get the portal-relative direction along the plane that goes away
+         * from the listener (toward the farthest edge of the portal).
+         */
+        far_edge[0] = local_dir[0] - local_norm[0]*dir_dot_norm;
+        far_edge[1] = local_dir[1] - local_norm[1]*dir_dot_norm;
+        far_edge[2] = local_dir[2] - local_norm[2]*dir_dot_norm;
+
+        edist = sqrtf(dot_product(far_edge, far_edge));
+        if(edist > 0.0001f)
+        {
+            /* Rescale the portal-relative vector to be at the radius edge. */
+            ALfloat mag = portal_radius / edist;
+            far_edge[0] *= mag;
+            far_edge[1] *= mag;
+            far_edge[2] *= mag;
+
+            /* Calculate the closest edge of the portal by negating the
+             * farthest, and add an offset to make them both relative to the
+             * listener.
+             */
+            near_edge[0] = local_dir[0]*dist - far_edge[0];
+            near_edge[1] = local_dir[1]*dist - far_edge[1];
+            near_edge[2] = local_dir[2]*dist - far_edge[2];
+            far_edge[0] += local_dir[0]*dist;
+            far_edge[1] += local_dir[1]*dist;
+            far_edge[2] += local_dir[2]*dist;
+
+            /* Normalize the listener-relative extents of the portal, then
+             * calculate the panning magnitude for the other zone given the
+             * apparent size of the opening. The panning magnitude affects the
+             * envelopment of the environment, with 1 being a point, 0.5 being
+             * half coverage around the listener, and 0 being full coverage.
+             */
+            normalize(far_edge);
+            normalize(near_edge);
+            magnitude = 1.0f - acosf(dot_product(far_edge, near_edge))/(float)(M_PI*2.0);
+
+            /* Recalculate the panning direction, to be directly between the
+             * direction of the two extents.
+             */
+            local_dir[0] = far_edge[0] + near_edge[0];
+            local_dir[1] = far_edge[1] + near_edge[1];
+            local_dir[2] = far_edge[2] + near_edge[2];
+            normalize(local_dir);
+        }
+        else
+        {
+            /* If we get here, the listener is directly in front of or behind
+             * the center of the portal, making all aperture edges effectively
+             * equidistant. Calculating the panning magnitude is simplified,
+             * using the arctangent of the radius and distance.
+             */
+            magnitude = 1.0f - (atan2f(portal_radius, dist) / (float)M_PI);
+        }
+
+        /* Scale the other zone's panning vector. */
+        other_dir[0] = local_dir[0] * magnitude;
+        other_dir[1] = local_dir[1] * magnitude;
+        other_dir[2] = local_dir[2] * magnitude;
+        /* Pan the current zone to the opposite direction of the portal, and
+         * take the remaining percentage of the portal's magnitude.
+         */
+        this_dir[0] = local_dir[0] * (magnitude-1.0f);
+        this_dir[1] = local_dir[1] * (magnitude-1.0f);
+        this_dir[2] = local_dir[2] * (magnitude-1.0f);
+
+        /* Now set the effects' panning vectors and gain. Energy is shared
+         * between environments, so attenuate according to each zone's
+         * contribution (note: gain^2 = energy).
+         */
+        alEffectf(this_effect, AL_EAXREVERB_REFLECTIONS_GAIN, this_reverb->flReflectionsGain * sqrtf(magnitude));
+        alEffectf(this_effect, AL_EAXREVERB_LATE_REVERB_GAIN, this_reverb->flLateReverbGain * sqrtf(magnitude));
+        alEffectfv(this_effect, AL_EAXREVERB_REFLECTIONS_PAN, this_dir);
+        alEffectfv(this_effect, AL_EAXREVERB_LATE_REVERB_PAN, this_dir);
+
+        alEffectf(other_effect, AL_EAXREVERB_REFLECTIONS_GAIN, other_reverb->flReflectionsGain * sqrtf(1.0f-magnitude));
+        alEffectf(other_effect, AL_EAXREVERB_LATE_REVERB_GAIN, other_reverb->flLateReverbGain * sqrtf(1.0f-magnitude));
+        alEffectfv(other_effect, AL_EAXREVERB_REFLECTIONS_PAN, other_dir);
+        alEffectfv(other_effect, AL_EAXREVERB_LATE_REVERB_PAN, other_dir);
+    }
+    else
+    {
+        /* We're practically in the center of the portal. Give the panning
+         * vectors a 50/50 split, with Zone 0 covering the half in front of
+         * the normal, and Zone 1 covering the half behind.
+         */
+        this_dir[0] = local_norm[0] / 2.0f;
+        this_dir[1] = local_norm[1] / 2.0f;
+        this_dir[2] = local_norm[2] / 2.0f;
+
+        other_dir[0] = local_norm[0] / -2.0f;
+        other_dir[1] = local_norm[1] / -2.0f;
+        other_dir[2] = local_norm[2] / -2.0f;
+
+        alEffectf(effects[0], AL_EAXREVERB_REFLECTIONS_GAIN, reverbs[0].flReflectionsGain * sqrtf(0.5f));
+        alEffectf(effects[0], AL_EAXREVERB_LATE_REVERB_GAIN, reverbs[0].flLateReverbGain * sqrtf(0.5f));
+        alEffectfv(effects[0], AL_EAXREVERB_REFLECTIONS_PAN, this_dir);
+        alEffectfv(effects[0], AL_EAXREVERB_LATE_REVERB_PAN, this_dir);
+
+        alEffectf(effects[1], AL_EAXREVERB_REFLECTIONS_GAIN, reverbs[1].flReflectionsGain * sqrtf(0.5f));
+        alEffectf(effects[1], AL_EAXREVERB_LATE_REVERB_GAIN, reverbs[1].flLateReverbGain * sqrtf(0.5f));
+        alEffectfv(effects[1], AL_EAXREVERB_REFLECTIONS_PAN, other_dir);
+        alEffectfv(effects[1], AL_EAXREVERB_LATE_REVERB_PAN, other_dir);
+    }
+
+    /* Finally, update the effect slots with the updated effect parameters. */
+    alAuxiliaryEffectSloti(slots[0], AL_EFFECTSLOT_EFFECT, (ALint)effects[0]);
+    alAuxiliaryEffectSloti(slots[1], AL_EFFECTSLOT_EFFECT, (ALint)effects[1]);
+}
+
+
+int main(int argc, char **argv)
+{
+    static const int MaxTransitions = 8;
+    EFXEAXREVERBPROPERTIES reverbs[2] = {
+        EFX_REVERB_PRESET_CARPETEDHALLWAY,
+        EFX_REVERB_PRESET_BATHROOM
+    };
+    ALCdevice *device = NULL;
+    ALCcontext *context = NULL;
+    ALuint effects[2] = { 0, 0 };
+    ALuint slots[2] = { 0, 0 };
+    ALuint direct_filter = 0;
+    ALuint buffer = 0;
+    ALuint source = 0;
+    ALCint num_sends = 0;
+    ALenum state = AL_INITIAL;
+    ALfloat direct_gain = 1.0f;
+    int basetime = 0;
+    int loops = 0;
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name>] [options] <filename>\n\n"
+        "Options:\n"
+        "\t-nodirect\tSilence direct path output (easier to hear reverb)\n\n",
+        argv[0]);
+        return 1;
+    }
+
+    /* Initialize OpenAL, and check for EFX support with at least 2 auxiliary
+     * sends (if multiple sends are supported, 2 are provided by default; if
+     * you want more, you have to request it through alcCreateContext).
+     */
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    while(argc > 0)
+    {
+        if(strcmp(argv[0], "-nodirect") == 0)
+            direct_gain = 0.0f;
+        else
+            break;
+        argv++;
+        argc--;
+    }
+    if(argc < 1)
+    {
+        fprintf(stderr, "No filename spacified.\n");
+        CloseAL();
+        return 1;
+    }
+
+    context = alcGetCurrentContext();
+    device = alcGetContextsDevice(context);
+
+    if(!alcIsExtensionPresent(device, "ALC_EXT_EFX"))
+    {
+        fprintf(stderr, "Error: EFX not supported\n");
+        CloseAL();
+        return 1;
+    }
+
+    num_sends = 0;
+    alcGetIntegerv(device, ALC_MAX_AUXILIARY_SENDS, 1, &num_sends);
+    if(alcGetError(device) != ALC_NO_ERROR || num_sends < 2)
+    {
+        fprintf(stderr, "Error: Device does not support multiple sends (got %d, need 2)\n",
+                num_sends);
+        CloseAL();
+        return 1;
+    }
+
+    /* Define a macro to help load the function pointers. */
+#define LOAD_PROC(T, x)  ((x) = FUNCTION_CAST(T, alGetProcAddress(#x)))
+    LOAD_PROC(LPALGENFILTERS, alGenFilters);
+    LOAD_PROC(LPALDELETEFILTERS, alDeleteFilters);
+    LOAD_PROC(LPALISFILTER, alIsFilter);
+    LOAD_PROC(LPALFILTERI, alFilteri);
+    LOAD_PROC(LPALFILTERIV, alFilteriv);
+    LOAD_PROC(LPALFILTERF, alFilterf);
+    LOAD_PROC(LPALFILTERFV, alFilterfv);
+    LOAD_PROC(LPALGETFILTERI, alGetFilteri);
+    LOAD_PROC(LPALGETFILTERIV, alGetFilteriv);
+    LOAD_PROC(LPALGETFILTERF, alGetFilterf);
+    LOAD_PROC(LPALGETFILTERFV, alGetFilterfv);
+
+    LOAD_PROC(LPALGENEFFECTS, alGenEffects);
+    LOAD_PROC(LPALDELETEEFFECTS, alDeleteEffects);
+    LOAD_PROC(LPALISEFFECT, alIsEffect);
+    LOAD_PROC(LPALEFFECTI, alEffecti);
+    LOAD_PROC(LPALEFFECTIV, alEffectiv);
+    LOAD_PROC(LPALEFFECTF, alEffectf);
+    LOAD_PROC(LPALEFFECTFV, alEffectfv);
+    LOAD_PROC(LPALGETEFFECTI, alGetEffecti);
+    LOAD_PROC(LPALGETEFFECTIV, alGetEffectiv);
+    LOAD_PROC(LPALGETEFFECTF, alGetEffectf);
+    LOAD_PROC(LPALGETEFFECTFV, alGetEffectfv);
+
+    LOAD_PROC(LPALGENAUXILIARYEFFECTSLOTS, alGenAuxiliaryEffectSlots);
+    LOAD_PROC(LPALDELETEAUXILIARYEFFECTSLOTS, alDeleteAuxiliaryEffectSlots);
+    LOAD_PROC(LPALISAUXILIARYEFFECTSLOT, alIsAuxiliaryEffectSlot);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTI, alAuxiliaryEffectSloti);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTIV, alAuxiliaryEffectSlotiv);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTF, alAuxiliaryEffectSlotf);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTFV, alAuxiliaryEffectSlotfv);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTI, alGetAuxiliaryEffectSloti);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTIV, alGetAuxiliaryEffectSlotiv);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTF, alGetAuxiliaryEffectSlotf);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTFV, alGetAuxiliaryEffectSlotfv);
+#undef LOAD_PROC
+
+    /* Load the sound into a buffer. */
+    buffer = LoadSound(argv[0]);
+    if(!buffer)
+    {
+        CloseAL();
+        return 1;
+    }
+
+    /* Generate two effects for two "zones", and load a reverb into each one.
+     * Note that unlike single-zone reverb, where you can store one effect per
+     * preset, for multi-zone reverb you should have one effect per environment
+     * instance, or one per audible zone. This is because we'll be changing the
+     * effects' properties in real-time based on the environment instance
+     * relative to the listener.
+     */
+    alGenEffects(2, effects);
+    if(!LoadEffect(effects[0], &reverbs[0]) || !LoadEffect(effects[1], &reverbs[1]))
+    {
+        alDeleteEffects(2, effects);
+        alDeleteBuffers(1, &buffer);
+        CloseAL();
+        return 1;
+    }
+
+    /* Create the effect slot objects, one for each "active" effect. */
+    alGenAuxiliaryEffectSlots(2, slots);
+
+    /* Tell the effect slots to use the loaded effect objects, with slot 0 for
+     * Zone 0 and slot 1 for Zone 1. Note that this effectively copies the
+     * effect properties. Modifying or deleting the effect object afterward
+     * won't directly affect the effect slot until they're reapplied like this.
+     */
+    alAuxiliaryEffectSloti(slots[0], AL_EFFECTSLOT_EFFECT, (ALint)effects[0]);
+    alAuxiliaryEffectSloti(slots[1], AL_EFFECTSLOT_EFFECT, (ALint)effects[1]);
+    assert(alGetError()==AL_NO_ERROR && "Failed to set effect slot");
+
+    /* For the purposes of this example, prepare a filter that optionally
+     * silences the direct path which allows us to hear just the reverberation.
+     * A filter like this is normally used for obstruction, where the path
+     * directly between the listener and source is blocked (the exact
+     * properties depending on the type and thickness of the obstructing
+     * material).
+     */
+    alGenFilters(1, &direct_filter);
+    alFilteri(direct_filter, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
+    alFilterf(direct_filter, AL_LOWPASS_GAIN, direct_gain);
+    assert(alGetError()==AL_NO_ERROR && "Failed to set direct filter");
+
+    /* Create the source to play the sound with, place it in front of the
+     * listener's path in the left zone.
+     */
+    source = 0;
+    alGenSources(1, &source);
+    alSourcei(source, AL_LOOPING, AL_TRUE);
+    alSource3f(source, AL_POSITION, -5.0f, 0.0f, -2.0f);
+    alSourcei(source, AL_DIRECT_FILTER, (ALint)direct_filter);
+    alSourcei(source, AL_BUFFER, (ALint)buffer);
+
+    /* Connect the source to the effect slots. Here, we connect source send 0
+     * to Zone 0's slot, and send 1 to Zone 1's slot. Filters can be specified
+     * to occlude the source from each zone by varying amounts; for example, a
+     * source within a particular zone would be unfiltered, while a source that
+     * can only see a zone through a window or thin wall may be attenuated for
+     * that zone.
+     */
+    alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)slots[0], 0, AL_FILTER_NULL);
+    alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)slots[1], 1, AL_FILTER_NULL);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Get the current time as the base for timing in the main loop. */
+    basetime = altime_get();
+    loops = 0;
+    printf("Transition %d of %d...\n", loops+1, MaxTransitions);
+
+    /* Play the sound for a while. */
+    alSourcePlay(source);
+    do {
+        int curtime;
+        ALfloat timediff;
+
+        /* Start a batch update, to ensure all changes apply simultaneously. */
+        alcSuspendContext(context);
+
+        /* Get the current time to track the amount of time that passed.
+         * Convert the difference to seconds.
+         */
+        curtime = altime_get();
+        timediff = (float)(curtime - basetime) / 1000.0f;
+
+        /* Avoid negative time deltas, in case of non-monotonic clocks. */
+        if(timediff < 0.0f)
+            timediff = 0.0f;
+        else while(timediff >= 4.0f*(float)((loops&1)+1))
+        {
+            /* For this example, each transition occurs over 4 seconds, and
+             * there's 2 transitions per cycle.
+             */
+            if(++loops < MaxTransitions)
+                printf("Transition %d of %d...\n", loops+1, MaxTransitions);
+            if(!(loops&1))
+            {
+                /* Cycle completed. Decrease the delta and increase the base
+                 * time to start a new cycle.
+                 */
+                timediff -= 8.0f;
+                basetime += 8000;
+            }
+        }
+
+        /* Update the listener and effects, and finish the batch. */
+        UpdateListenerAndEffects(timediff, slots, effects, reverbs);
+        alcProcessContext(context);
+
+        al_nssleep(10000000);
+
+        alGetSourcei(source, AL_SOURCE_STATE, &state);
+    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING && loops < MaxTransitions);
+
+    /* All done. Delete resources, and close down OpenAL. */
+    alDeleteSources(1, &source);
+    alDeleteAuxiliaryEffectSlots(2, slots);
+    alDeleteEffects(2, effects);
+    alDeleteFilters(1, &direct_filter);
+    alDeleteBuffers(1, &buffer);
+
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/alplay.c b/examples/alplay.c
new file mode 100644 (file)
index 0000000..4291cb4
--- /dev/null
@@ -0,0 +1,335 @@
+/*
+ * OpenAL Source Play Example
+ *
+ * Copyright (c) 2017 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains an example for playing a sound buffer. */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+
+enum FormatType {
+    Int16,
+    Float,
+    IMA4,
+    MSADPCM
+};
+
+/* LoadBuffer loads the named audio file into an OpenAL buffer object, and
+ * returns the new buffer ID.
+ */
+static ALuint LoadSound(const char *filename)
+{
+    enum FormatType sample_format = Int16;
+    ALint byteblockalign = 0;
+    ALint splblockalign = 0;
+    sf_count_t num_frames;
+    ALenum err, format;
+    ALsizei num_bytes;
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    ALuint buffer;
+    void *membuf;
+
+    /* Open the audio file and check that it's usable. */
+    sndfile = sf_open(filename, SFM_READ, &sfinfo);
+    if(!sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(sndfile));
+        return 0;
+    }
+    if(sfinfo.frames < 1)
+    {
+        fprintf(stderr, "Bad sample count in %s (%" PRId64 ")\n", filename, sfinfo.frames);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Detect a suitable format to load. Formats like Vorbis and Opus use float
+     * natively, so load as float to avoid clipping when possible. Formats
+     * larger than 16-bit can also use float to preserve a bit more precision.
+     */
+    switch((sfinfo.format&SF_FORMAT_SUBMASK))
+    {
+    case SF_FORMAT_PCM_24:
+    case SF_FORMAT_PCM_32:
+    case SF_FORMAT_FLOAT:
+    case SF_FORMAT_DOUBLE:
+    case SF_FORMAT_VORBIS:
+    case SF_FORMAT_OPUS:
+    case SF_FORMAT_ALAC_20:
+    case SF_FORMAT_ALAC_24:
+    case SF_FORMAT_ALAC_32:
+    case 0x0080/*SF_FORMAT_MPEG_LAYER_I*/:
+    case 0x0081/*SF_FORMAT_MPEG_LAYER_II*/:
+    case 0x0082/*SF_FORMAT_MPEG_LAYER_III*/:
+        if(alIsExtensionPresent("AL_EXT_FLOAT32"))
+            sample_format = Float;
+        break;
+    case SF_FORMAT_IMA_ADPCM:
+        /* ADPCM formats require setting a block alignment as specified in the
+         * file, which needs to be read from the wave 'fmt ' chunk manually
+         * since libsndfile doesn't provide it in a format-agnostic way.
+         */
+        if(sfinfo.channels <= 2 && (sfinfo.format&SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV
+            && alIsExtensionPresent("AL_EXT_IMA4")
+            && alIsExtensionPresent("AL_SOFT_block_alignment"))
+            sample_format = IMA4;
+        break;
+    case SF_FORMAT_MS_ADPCM:
+        if(sfinfo.channels <= 2 && (sfinfo.format&SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV
+            && alIsExtensionPresent("AL_SOFT_MSADPCM")
+            && alIsExtensionPresent("AL_SOFT_block_alignment"))
+            sample_format = MSADPCM;
+        break;
+    }
+
+    if(sample_format == IMA4 || sample_format == MSADPCM)
+    {
+        /* For ADPCM, lookup the wave file's "fmt " chunk, which is a
+         * WAVEFORMATEX-based structure for the audio format.
+         */
+        SF_CHUNK_INFO inf = { "fmt ", 4, 0, NULL };
+        SF_CHUNK_ITERATOR *iter = sf_get_chunk_iterator(sndfile, &inf);
+
+        /* If there's an issue getting the chunk or block alignment, load as
+         * 16-bit and have libsndfile do the conversion.
+         */
+        if(!iter || sf_get_chunk_size(iter, &inf) != SF_ERR_NO_ERROR || inf.datalen < 14)
+            sample_format = Int16;
+        else
+        {
+            ALubyte *fmtbuf = calloc(inf.datalen, 1);
+            inf.data = fmtbuf;
+            if(sf_get_chunk_data(iter, &inf) != SF_ERR_NO_ERROR)
+                sample_format = Int16;
+            else
+            {
+                /* Read the nBlockAlign field, and convert from bytes- to
+                 * samples-per-block (verifying it's valid by converting back
+                 * and comparing to the original value).
+                 */
+                byteblockalign = fmtbuf[12] | (fmtbuf[13]<<8);
+                if(sample_format == IMA4)
+                {
+                    splblockalign = (byteblockalign/sfinfo.channels - 4)/4*8 + 1;
+                    if(splblockalign < 1
+                        || ((splblockalign-1)/2 + 4)*sfinfo.channels != byteblockalign)
+                        sample_format = Int16;
+                }
+                else
+                {
+                    splblockalign = (byteblockalign/sfinfo.channels - 7)*2 + 2;
+                    if(splblockalign < 2
+                        || ((splblockalign-2)/2 + 7)*sfinfo.channels != byteblockalign)
+                        sample_format = Int16;
+                }
+            }
+            free(fmtbuf);
+        }
+    }
+
+    if(sample_format == Int16)
+    {
+        splblockalign = 1;
+        byteblockalign = sfinfo.channels * 2;
+    }
+    else if(sample_format == Float)
+    {
+        splblockalign = 1;
+        byteblockalign = sfinfo.channels * 4;
+    }
+
+    /* Figure out the OpenAL format from the file and desired sample type. */
+    format = AL_NONE;
+    if(sfinfo.channels == 1)
+    {
+        if(sample_format == Int16)
+            format = AL_FORMAT_MONO16;
+        else if(sample_format == Float)
+            format = AL_FORMAT_MONO_FLOAT32;
+        else if(sample_format == IMA4)
+            format = AL_FORMAT_MONO_IMA4;
+        else if(sample_format == MSADPCM)
+            format = AL_FORMAT_MONO_MSADPCM_SOFT;
+    }
+    else if(sfinfo.channels == 2)
+    {
+        if(sample_format == Int16)
+            format = AL_FORMAT_STEREO16;
+        else if(sample_format == Float)
+            format = AL_FORMAT_STEREO_FLOAT32;
+        else if(sample_format == IMA4)
+            format = AL_FORMAT_STEREO_IMA4;
+        else if(sample_format == MSADPCM)
+            format = AL_FORMAT_STEREO_MSADPCM_SOFT;
+    }
+    else if(sfinfo.channels == 3)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+        {
+            if(sample_format == Int16)
+                format = AL_FORMAT_BFORMAT2D_16;
+            else if(sample_format == Float)
+                format = AL_FORMAT_BFORMAT2D_FLOAT32;
+        }
+    }
+    else if(sfinfo.channels == 4)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+        {
+            if(sample_format == Int16)
+                format = AL_FORMAT_BFORMAT3D_16;
+            else if(sample_format == Float)
+                format = AL_FORMAT_BFORMAT3D_FLOAT32;
+        }
+    }
+    if(!format)
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", sfinfo.channels);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    if(sfinfo.frames/splblockalign > (sf_count_t)(INT_MAX/byteblockalign))
+    {
+        fprintf(stderr, "Too many samples in %s (%" PRId64 ")\n", filename, sfinfo.frames);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Decode the whole audio file to a buffer. */
+    membuf = malloc((size_t)(sfinfo.frames / splblockalign * byteblockalign));
+
+    if(sample_format == Int16)
+        num_frames = sf_readf_short(sndfile, membuf, sfinfo.frames);
+    else if(sample_format == Float)
+        num_frames = sf_readf_float(sndfile, membuf, sfinfo.frames);
+    else
+    {
+        sf_count_t count = sfinfo.frames / splblockalign * byteblockalign;
+        num_frames = sf_read_raw(sndfile, membuf, count);
+        if(num_frames > 0)
+            num_frames = num_frames / byteblockalign * splblockalign;
+    }
+    if(num_frames < 1)
+    {
+        free(membuf);
+        sf_close(sndfile);
+        fprintf(stderr, "Failed to read samples in %s (%" PRId64 ")\n", filename, num_frames);
+        return 0;
+    }
+    num_bytes = (ALsizei)(num_frames / splblockalign * byteblockalign);
+
+    printf("Loading: %s (%s, %dhz)\n", filename, FormatName(format), sfinfo.samplerate);
+    fflush(stdout);
+
+    /* Buffer the audio data into a new buffer object, then free the data and
+     * close the file.
+     */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    if(splblockalign > 1)
+        alBufferi(buffer, AL_UNPACK_BLOCK_ALIGNMENT_SOFT, splblockalign);
+    alBufferData(buffer, format, membuf, num_bytes, sfinfo.samplerate);
+
+    free(membuf);
+    sf_close(sndfile);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(buffer && alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+int main(int argc, char **argv)
+{
+    ALuint source, buffer;
+    ALfloat offset;
+    ALenum state;
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name>] <filename>\n", argv[0]);
+        return 1;
+    }
+
+    /* Initialize OpenAL. */
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    /* Load the sound into a buffer. */
+    buffer = LoadSound(argv[0]);
+    if(!buffer)
+    {
+        CloseAL();
+        return 1;
+    }
+
+    /* Create the source to play the sound with. */
+    source = 0;
+    alGenSources(1, &source);
+    alSourcei(source, AL_BUFFER, (ALint)buffer);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Play the sound until it finishes. */
+    alSourcePlay(source);
+    do {
+        al_nssleep(10000000);
+        alGetSourcei(source, AL_SOURCE_STATE, &state);
+
+        /* Get the source offset. */
+        alGetSourcef(source, AL_SEC_OFFSET, &offset);
+        printf("\rOffset: %f  ", offset);
+        fflush(stdout);
+    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);
+    printf("\n");
+
+    /* All done. Delete resources, and close down OpenAL. */
+    alDeleteSources(1, &source);
+    alDeleteBuffers(1, &buffer);
+
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/alrecord.c b/examples/alrecord.c
new file mode 100644 (file)
index 0000000..0389449
--- /dev/null
@@ -0,0 +1,403 @@
+/*
+ * OpenAL Recording Example
+ *
+ * Copyright (c) 2017 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains a relatively simple recorder. */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+#include "win_main_utf8.h"
+
+
+#if defined(_WIN64)
+#define SZFMT "%I64u"
+#elif defined(_WIN32)
+#define SZFMT "%u"
+#else
+#define SZFMT "%zu"
+#endif
+
+
+#if defined(_MSC_VER) && (_MSC_VER < 1900)
+static float msvc_strtof(const char *str, char **end)
+{ return (float)strtod(str, end); }
+#define strtof msvc_strtof
+#endif
+
+
+static void fwrite16le(ALushort val, FILE *f)
+{
+    ALubyte data[2];
+    data[0] = (ALubyte)(val&0xff);
+    data[1] = (ALubyte)(val>>8);
+    fwrite(data, 1, 2, f);
+}
+
+static void fwrite32le(ALuint val, FILE *f)
+{
+    ALubyte data[4];
+    data[0] = (ALubyte)(val&0xff);
+    data[1] = (ALubyte)((val>>8)&0xff);
+    data[2] = (ALubyte)((val>>16)&0xff);
+    data[3] = (ALubyte)(val>>24);
+    fwrite(data, 1, 4, f);
+}
+
+
+typedef struct Recorder {
+    ALCdevice *mDevice;
+
+    FILE *mFile;
+    long mDataSizeOffset;
+    ALuint mDataSize;
+    float mRecTime;
+
+    ALuint mChannels;
+    ALuint mBits;
+    ALuint mSampleRate;
+    ALuint mFrameSize;
+    ALbyte *mBuffer;
+    ALsizei mBufferSize;
+} Recorder;
+
+int main(int argc, char **argv)
+{
+    static const char optlist[] =
+"    --channels/-c <channels>  Set channel count (1 or 2)\n"
+"    --bits/-b <bits>          Set channel count (8, 16, or 32)\n"
+"    --rate/-r <rate>          Set sample rate (8000 to 96000)\n"
+"    --time/-t <time>          Time in seconds to record (1 to 10)\n"
+"    --outfile/-o <filename>   Output filename (default: record.wav)";
+    const char *fname = "record.wav";
+    const char *devname = NULL;
+    const char *progname;
+    Recorder recorder;
+    long total_size;
+    ALenum format;
+    ALCenum err;
+
+    progname = argv[0];
+    if(argc < 2)
+    {
+        fprintf(stderr, "Record from a device to a wav file.\n\n"
+                "Usage: %s [-device <name>] [options...]\n\n"
+                "Available options:\n%s\n", progname, optlist);
+        return 0;
+    }
+
+    recorder.mDevice = NULL;
+    recorder.mFile = NULL;
+    recorder.mDataSizeOffset = 0;
+    recorder.mDataSize = 0;
+    recorder.mRecTime = 4.0f;
+    recorder.mChannels = 1;
+    recorder.mBits = 16;
+    recorder.mSampleRate = 44100;
+    recorder.mFrameSize = recorder.mChannels * recorder.mBits / 8;
+    recorder.mBuffer = NULL;
+    recorder.mBufferSize = 0;
+
+    argv++; argc--;
+    if(argc > 1 && strcmp(argv[0], "-device") == 0)
+    {
+        devname = argv[1];
+        argv += 2;
+        argc -= 2;
+    }
+
+    while(argc > 0)
+    {
+        char *end;
+        if(strcmp(argv[0], "--") == 0)
+            break;
+        else if(strcmp(argv[0], "--channels") == 0 || strcmp(argv[0], "-c") == 0)
+        {
+            if(argc < 2)
+            {
+                fprintf(stderr, "Missing argument for option: %s\n", argv[0]);
+                return 1;
+            }
+
+            recorder.mChannels = (ALuint)strtoul(argv[1], &end, 0);
+            if((recorder.mChannels != 1 && recorder.mChannels != 2) || (end && *end != '\0'))
+            {
+                fprintf(stderr, "Invalid channels: %s\n", argv[1]);
+                return 1;
+            }
+            argv += 2;
+            argc -= 2;
+        }
+        else if(strcmp(argv[0], "--bits") == 0 || strcmp(argv[0], "-b") == 0)
+        {
+            if(argc < 2)
+            {
+                fprintf(stderr, "Missing argument for option: %s\n", argv[0]);
+                return 1;
+            }
+
+            recorder.mBits = (ALuint)strtoul(argv[1], &end, 0);
+            if((recorder.mBits != 8 && recorder.mBits != 16 && recorder.mBits != 32) ||
+               (end && *end != '\0'))
+            {
+                fprintf(stderr, "Invalid bit count: %s\n", argv[1]);
+                return 1;
+            }
+            argv += 2;
+            argc -= 2;
+        }
+        else if(strcmp(argv[0], "--rate") == 0 || strcmp(argv[0], "-r") == 0)
+        {
+            if(argc < 2)
+            {
+                fprintf(stderr, "Missing argument for option: %s\n", argv[0]);
+                return 1;
+            }
+
+            recorder.mSampleRate = (ALuint)strtoul(argv[1], &end, 0);
+            if(!(recorder.mSampleRate >= 8000 && recorder.mSampleRate <= 96000) || (end && *end != '\0'))
+            {
+                fprintf(stderr, "Invalid sample rate: %s\n", argv[1]);
+                return 1;
+            }
+            argv += 2;
+            argc -= 2;
+        }
+        else if(strcmp(argv[0], "--time") == 0 || strcmp(argv[0], "-t") == 0)
+        {
+            if(argc < 2)
+            {
+                fprintf(stderr, "Missing argument for option: %s\n", argv[0]);
+                return 1;
+            }
+
+            recorder.mRecTime = strtof(argv[1], &end);
+            if(!(recorder.mRecTime >= 1.0f && recorder.mRecTime <= 10.0f) || (end && *end != '\0'))
+            {
+                fprintf(stderr, "Invalid record time: %s\n", argv[1]);
+                return 1;
+            }
+            argv += 2;
+            argc -= 2;
+        }
+        else if(strcmp(argv[0], "--outfile") == 0 || strcmp(argv[0], "-o") == 0)
+        {
+            if(argc < 2)
+            {
+                fprintf(stderr, "Missing argument for option: %s\n", argv[0]);
+                return 1;
+            }
+
+            fname = argv[1];
+            argv += 2;
+            argc -= 2;
+        }
+        else if(strcmp(argv[0], "--help") == 0 || strcmp(argv[0], "-h") == 0)
+        {
+            fprintf(stderr, "Record from a device to a wav file.\n\n"
+                    "Usage: %s [-device <name>] [options...]\n\n"
+                    "Available options:\n%s\n", progname, optlist);
+            return 0;
+        }
+        else
+        {
+            fprintf(stderr, "Invalid option '%s'.\n\n"
+                    "Usage: %s [-device <name>] [options...]\n\n"
+                    "Available options:\n%s\n", argv[0], progname, optlist);
+            return 0;
+        }
+    }
+
+    recorder.mFrameSize = recorder.mChannels * recorder.mBits / 8;
+
+    format = AL_NONE;
+    if(recorder.mChannels == 1)
+    {
+        if(recorder.mBits == 8)
+            format = AL_FORMAT_MONO8;
+        else if(recorder.mBits == 16)
+            format = AL_FORMAT_MONO16;
+        else if(recorder.mBits == 32)
+            format = AL_FORMAT_MONO_FLOAT32;
+    }
+    else if(recorder.mChannels == 2)
+    {
+        if(recorder.mBits == 8)
+            format = AL_FORMAT_STEREO8;
+        else if(recorder.mBits == 16)
+            format = AL_FORMAT_STEREO16;
+        else if(recorder.mBits == 32)
+            format = AL_FORMAT_STEREO_FLOAT32;
+    }
+
+    recorder.mDevice = alcCaptureOpenDevice(devname, recorder.mSampleRate, format, 32768);
+    if(!recorder.mDevice)
+    {
+        fprintf(stderr, "Failed to open %s, %s %d-bit, %s, %dhz (%d samples)\n",
+            devname ? devname : "default device",
+            (recorder.mBits == 32) ? "Float" :
+            (recorder.mBits !=  8) ? "Signed" : "Unsigned", recorder.mBits,
+            (recorder.mChannels == 1) ? "Mono" : "Stereo", recorder.mSampleRate,
+            32768
+        );
+        return 1;
+    }
+    fprintf(stderr, "Opened \"%s\"\n", alcGetString(
+        recorder.mDevice, ALC_CAPTURE_DEVICE_SPECIFIER
+    ));
+
+    recorder.mFile = fopen(fname, "wb");
+    if(!recorder.mFile)
+    {
+        fprintf(stderr, "Failed to open '%s' for writing\n", fname);
+        alcCaptureCloseDevice(recorder.mDevice);
+        return 1;
+    }
+
+    fputs("RIFF", recorder.mFile);
+    fwrite32le(0xFFFFFFFF, recorder.mFile); // 'RIFF' header len; filled in at close
+
+    fputs("WAVE", recorder.mFile);
+
+    fputs("fmt ", recorder.mFile);
+    fwrite32le(18, recorder.mFile); // 'fmt ' header len
+
+    // 16-bit val, format type id (1 = integer PCM, 3 = float PCM)
+    fwrite16le((recorder.mBits == 32) ? 0x0003 : 0x0001, recorder.mFile);
+    // 16-bit val, channel count
+    fwrite16le((ALushort)recorder.mChannels, recorder.mFile);
+    // 32-bit val, frequency
+    fwrite32le(recorder.mSampleRate, recorder.mFile);
+    // 32-bit val, bytes per second
+    fwrite32le(recorder.mSampleRate * recorder.mFrameSize, recorder.mFile);
+    // 16-bit val, frame size
+    fwrite16le((ALushort)recorder.mFrameSize, recorder.mFile);
+    // 16-bit val, bits per sample
+    fwrite16le((ALushort)recorder.mBits, recorder.mFile);
+    // 16-bit val, extra byte count
+    fwrite16le(0, recorder.mFile);
+
+    fputs("data", recorder.mFile);
+    fwrite32le(0xFFFFFFFF, recorder.mFile); // 'data' header len; filled in at close
+
+    recorder.mDataSizeOffset = ftell(recorder.mFile) - 4;
+    if(ferror(recorder.mFile) || recorder.mDataSizeOffset < 0)
+    {
+        fprintf(stderr, "Error writing header: %s\n", strerror(errno));
+        fclose(recorder.mFile);
+        alcCaptureCloseDevice(recorder.mDevice);
+        return 1;
+    }
+
+    fprintf(stderr, "Recording '%s', %s %d-bit, %s, %dhz (%g second%s)\n", fname,
+        (recorder.mBits == 32) ? "Float" :
+        (recorder.mBits !=  8) ? "Signed" : "Unsigned", recorder.mBits,
+        (recorder.mChannels == 1) ? "Mono" : "Stereo", recorder.mSampleRate,
+        recorder.mRecTime, (recorder.mRecTime != 1.0f) ? "s" : ""
+    );
+
+    err = ALC_NO_ERROR;
+    alcCaptureStart(recorder.mDevice);
+    while((double)recorder.mDataSize/(double)recorder.mSampleRate < recorder.mRecTime &&
+          (err=alcGetError(recorder.mDevice)) == ALC_NO_ERROR && !ferror(recorder.mFile))
+    {
+        ALCint count = 0;
+        fprintf(stderr, "\rCaptured %u samples", recorder.mDataSize);
+        alcGetIntegerv(recorder.mDevice, ALC_CAPTURE_SAMPLES, 1, &count);
+        if(count < 1)
+        {
+            al_nssleep(10000000);
+            continue;
+        }
+        if(count > recorder.mBufferSize)
+        {
+            ALbyte *data = calloc(recorder.mFrameSize, (ALuint)count);
+            free(recorder.mBuffer);
+            recorder.mBuffer = data;
+            recorder.mBufferSize = count;
+        }
+        alcCaptureSamples(recorder.mDevice, recorder.mBuffer, count);
+#if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN
+        /* Byteswap multibyte samples on big-endian systems (wav needs little-
+         * endian, and OpenAL gives the system's native-endian).
+         */
+        if(recorder.mBits == 16)
+        {
+            ALCint i;
+            for(i = 0;i < count*recorder.mChannels;i++)
+            {
+                ALbyte b = recorder.mBuffer[i*2 + 0];
+                recorder.mBuffer[i*2 + 0] = recorder.mBuffer[i*2 + 1];
+                recorder.mBuffer[i*2 + 1] = b;
+            }
+        }
+        else if(recorder.mBits == 32)
+        {
+            ALCint i;
+            for(i = 0;i < count*recorder.mChannels;i++)
+            {
+                ALbyte b0 = recorder.mBuffer[i*4 + 0];
+                ALbyte b1 = recorder.mBuffer[i*4 + 1];
+                recorder.mBuffer[i*4 + 0] = recorder.mBuffer[i*4 + 3];
+                recorder.mBuffer[i*4 + 1] = recorder.mBuffer[i*4 + 2];
+                recorder.mBuffer[i*4 + 2] = b1;
+                recorder.mBuffer[i*4 + 3] = b0;
+            }
+        }
+#endif
+        recorder.mDataSize += (ALuint)fwrite(recorder.mBuffer, recorder.mFrameSize, (ALuint)count,
+                                             recorder.mFile);
+    }
+    alcCaptureStop(recorder.mDevice);
+    fprintf(stderr, "\rCaptured %u samples\n", recorder.mDataSize);
+    if(err != ALC_NO_ERROR)
+        fprintf(stderr, "Got device error 0x%04x: %s\n", err, alcGetString(recorder.mDevice, err));
+
+    alcCaptureCloseDevice(recorder.mDevice);
+    recorder.mDevice = NULL;
+
+    free(recorder.mBuffer);
+    recorder.mBuffer = NULL;
+    recorder.mBufferSize = 0;
+
+    total_size = ftell(recorder.mFile);
+    if(fseek(recorder.mFile, recorder.mDataSizeOffset, SEEK_SET) == 0)
+    {
+        fwrite32le(recorder.mDataSize*recorder.mFrameSize, recorder.mFile);
+        if(fseek(recorder.mFile, 4, SEEK_SET) == 0)
+            fwrite32le((ALuint)total_size - 8, recorder.mFile);
+    }
+
+    fclose(recorder.mFile);
+    recorder.mFile = NULL;
+
+    return 0;
+}
diff --git a/examples/alreverb.c b/examples/alreverb.c
new file mode 100644 (file)
index 0000000..11a3ac6
--- /dev/null
@@ -0,0 +1,344 @@
+/*
+ * OpenAL Reverb Example
+ *
+ * Copyright (c) 2012 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains an example for applying reverb to a sound. */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+#include "AL/efx.h"
+#include "AL/efx-presets.h"
+
+#include "common/alhelpers.h"
+
+
+/* Effect object functions */
+static LPALGENEFFECTS alGenEffects;
+static LPALDELETEEFFECTS alDeleteEffects;
+static LPALISEFFECT alIsEffect;
+static LPALEFFECTI alEffecti;
+static LPALEFFECTIV alEffectiv;
+static LPALEFFECTF alEffectf;
+static LPALEFFECTFV alEffectfv;
+static LPALGETEFFECTI alGetEffecti;
+static LPALGETEFFECTIV alGetEffectiv;
+static LPALGETEFFECTF alGetEffectf;
+static LPALGETEFFECTFV alGetEffectfv;
+
+/* Auxiliary Effect Slot object functions */
+static LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots;
+static LPALDELETEAUXILIARYEFFECTSLOTS alDeleteAuxiliaryEffectSlots;
+static LPALISAUXILIARYEFFECTSLOT alIsAuxiliaryEffectSlot;
+static LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti;
+static LPALAUXILIARYEFFECTSLOTIV alAuxiliaryEffectSlotiv;
+static LPALAUXILIARYEFFECTSLOTF alAuxiliaryEffectSlotf;
+static LPALAUXILIARYEFFECTSLOTFV alAuxiliaryEffectSlotfv;
+static LPALGETAUXILIARYEFFECTSLOTI alGetAuxiliaryEffectSloti;
+static LPALGETAUXILIARYEFFECTSLOTIV alGetAuxiliaryEffectSlotiv;
+static LPALGETAUXILIARYEFFECTSLOTF alGetAuxiliaryEffectSlotf;
+static LPALGETAUXILIARYEFFECTSLOTFV alGetAuxiliaryEffectSlotfv;
+
+
+/* LoadEffect loads the given reverb properties into a new OpenAL effect
+ * object, and returns the new effect ID. */
+static ALuint LoadEffect(const EFXEAXREVERBPROPERTIES *reverb)
+{
+    ALuint effect = 0;
+    ALenum err;
+
+    /* Create the effect object and check if we can do EAX reverb. */
+    alGenEffects(1, &effect);
+    if(alGetEnumValue("AL_EFFECT_EAXREVERB") != 0)
+    {
+        printf("Using EAX Reverb\n");
+
+        /* EAX Reverb is available. Set the EAX effect type then load the
+         * reverb properties. */
+        alEffecti(effect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB);
+
+        alEffectf(effect, AL_EAXREVERB_DENSITY, reverb->flDensity);
+        alEffectf(effect, AL_EAXREVERB_DIFFUSION, reverb->flDiffusion);
+        alEffectf(effect, AL_EAXREVERB_GAIN, reverb->flGain);
+        alEffectf(effect, AL_EAXREVERB_GAINHF, reverb->flGainHF);
+        alEffectf(effect, AL_EAXREVERB_GAINLF, reverb->flGainLF);
+        alEffectf(effect, AL_EAXREVERB_DECAY_TIME, reverb->flDecayTime);
+        alEffectf(effect, AL_EAXREVERB_DECAY_HFRATIO, reverb->flDecayHFRatio);
+        alEffectf(effect, AL_EAXREVERB_DECAY_LFRATIO, reverb->flDecayLFRatio);
+        alEffectf(effect, AL_EAXREVERB_REFLECTIONS_GAIN, reverb->flReflectionsGain);
+        alEffectf(effect, AL_EAXREVERB_REFLECTIONS_DELAY, reverb->flReflectionsDelay);
+        alEffectfv(effect, AL_EAXREVERB_REFLECTIONS_PAN, reverb->flReflectionsPan);
+        alEffectf(effect, AL_EAXREVERB_LATE_REVERB_GAIN, reverb->flLateReverbGain);
+        alEffectf(effect, AL_EAXREVERB_LATE_REVERB_DELAY, reverb->flLateReverbDelay);
+        alEffectfv(effect, AL_EAXREVERB_LATE_REVERB_PAN, reverb->flLateReverbPan);
+        alEffectf(effect, AL_EAXREVERB_ECHO_TIME, reverb->flEchoTime);
+        alEffectf(effect, AL_EAXREVERB_ECHO_DEPTH, reverb->flEchoDepth);
+        alEffectf(effect, AL_EAXREVERB_MODULATION_TIME, reverb->flModulationTime);
+        alEffectf(effect, AL_EAXREVERB_MODULATION_DEPTH, reverb->flModulationDepth);
+        alEffectf(effect, AL_EAXREVERB_AIR_ABSORPTION_GAINHF, reverb->flAirAbsorptionGainHF);
+        alEffectf(effect, AL_EAXREVERB_HFREFERENCE, reverb->flHFReference);
+        alEffectf(effect, AL_EAXREVERB_LFREFERENCE, reverb->flLFReference);
+        alEffectf(effect, AL_EAXREVERB_ROOM_ROLLOFF_FACTOR, reverb->flRoomRolloffFactor);
+        alEffecti(effect, AL_EAXREVERB_DECAY_HFLIMIT, reverb->iDecayHFLimit);
+    }
+    else
+    {
+        printf("Using Standard Reverb\n");
+
+        /* No EAX Reverb. Set the standard reverb effect type then load the
+         * available reverb properties. */
+        alEffecti(effect, AL_EFFECT_TYPE, AL_EFFECT_REVERB);
+
+        alEffectf(effect, AL_REVERB_DENSITY, reverb->flDensity);
+        alEffectf(effect, AL_REVERB_DIFFUSION, reverb->flDiffusion);
+        alEffectf(effect, AL_REVERB_GAIN, reverb->flGain);
+        alEffectf(effect, AL_REVERB_GAINHF, reverb->flGainHF);
+        alEffectf(effect, AL_REVERB_DECAY_TIME, reverb->flDecayTime);
+        alEffectf(effect, AL_REVERB_DECAY_HFRATIO, reverb->flDecayHFRatio);
+        alEffectf(effect, AL_REVERB_REFLECTIONS_GAIN, reverb->flReflectionsGain);
+        alEffectf(effect, AL_REVERB_REFLECTIONS_DELAY, reverb->flReflectionsDelay);
+        alEffectf(effect, AL_REVERB_LATE_REVERB_GAIN, reverb->flLateReverbGain);
+        alEffectf(effect, AL_REVERB_LATE_REVERB_DELAY, reverb->flLateReverbDelay);
+        alEffectf(effect, AL_REVERB_AIR_ABSORPTION_GAINHF, reverb->flAirAbsorptionGainHF);
+        alEffectf(effect, AL_REVERB_ROOM_ROLLOFF_FACTOR, reverb->flRoomRolloffFactor);
+        alEffecti(effect, AL_REVERB_DECAY_HFLIMIT, reverb->iDecayHFLimit);
+    }
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL error: %s\n", alGetString(err));
+        if(alIsEffect(effect))
+            alDeleteEffects(1, &effect);
+        return 0;
+    }
+
+    return effect;
+}
+
+
+/* LoadBuffer loads the named audio file into an OpenAL buffer object, and
+ * returns the new buffer ID.
+ */
+static ALuint LoadSound(const char *filename)
+{
+    ALenum err, format;
+    ALuint buffer;
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    short *membuf;
+    sf_count_t num_frames;
+    ALsizei num_bytes;
+
+    /* Open the audio file and check that it's usable. */
+    sndfile = sf_open(filename, SFM_READ, &sfinfo);
+    if(!sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(sndfile));
+        return 0;
+    }
+    if(sfinfo.frames < 1 || sfinfo.frames > (sf_count_t)(INT_MAX/sizeof(short))/sfinfo.channels)
+    {
+        fprintf(stderr, "Bad sample count in %s (%" PRId64 ")\n", filename, sfinfo.frames);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Get the sound format, and figure out the OpenAL format */
+    format = AL_NONE;
+    if(sfinfo.channels == 1)
+        format = AL_FORMAT_MONO16;
+    else if(sfinfo.channels == 2)
+        format = AL_FORMAT_STEREO16;
+    else if(sfinfo.channels == 3)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT2D_16;
+    }
+    else if(sfinfo.channels == 4)
+    {
+        if(sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            format = AL_FORMAT_BFORMAT3D_16;
+    }
+    if(!format)
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", sfinfo.channels);
+        sf_close(sndfile);
+        return 0;
+    }
+
+    /* Decode the whole audio file to a buffer. */
+    membuf = malloc((size_t)(sfinfo.frames * sfinfo.channels) * sizeof(short));
+
+    num_frames = sf_readf_short(sndfile, membuf, sfinfo.frames);
+    if(num_frames < 1)
+    {
+        free(membuf);
+        sf_close(sndfile);
+        fprintf(stderr, "Failed to read samples in %s (%" PRId64 ")\n", filename, num_frames);
+        return 0;
+    }
+    num_bytes = (ALsizei)(num_frames * sfinfo.channels) * (ALsizei)sizeof(short);
+
+    /* Buffer the audio data into a new buffer object, then free the data and
+     * close the file.
+     */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    alBufferData(buffer, format, membuf, num_bytes, sfinfo.samplerate);
+
+    free(membuf);
+    sf_close(sndfile);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(buffer && alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+int main(int argc, char **argv)
+{
+    EFXEAXREVERBPROPERTIES reverb = EFX_REVERB_PRESET_GENERIC;
+    ALuint source, buffer, effect, slot;
+    ALenum state;
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name] <filename>\n", argv[0]);
+        return 1;
+    }
+
+    /* Initialize OpenAL, and check for EFX support. */
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    if(!alcIsExtensionPresent(alcGetContextsDevice(alcGetCurrentContext()), "ALC_EXT_EFX"))
+    {
+        fprintf(stderr, "Error: EFX not supported\n");
+        CloseAL();
+        return 1;
+    }
+
+    /* Define a macro to help load the function pointers. */
+#define LOAD_PROC(T, x)  ((x) = FUNCTION_CAST(T, alGetProcAddress(#x)))
+    LOAD_PROC(LPALGENEFFECTS, alGenEffects);
+    LOAD_PROC(LPALDELETEEFFECTS, alDeleteEffects);
+    LOAD_PROC(LPALISEFFECT, alIsEffect);
+    LOAD_PROC(LPALEFFECTI, alEffecti);
+    LOAD_PROC(LPALEFFECTIV, alEffectiv);
+    LOAD_PROC(LPALEFFECTF, alEffectf);
+    LOAD_PROC(LPALEFFECTFV, alEffectfv);
+    LOAD_PROC(LPALGETEFFECTI, alGetEffecti);
+    LOAD_PROC(LPALGETEFFECTIV, alGetEffectiv);
+    LOAD_PROC(LPALGETEFFECTF, alGetEffectf);
+    LOAD_PROC(LPALGETEFFECTFV, alGetEffectfv);
+
+    LOAD_PROC(LPALGENAUXILIARYEFFECTSLOTS, alGenAuxiliaryEffectSlots);
+    LOAD_PROC(LPALDELETEAUXILIARYEFFECTSLOTS, alDeleteAuxiliaryEffectSlots);
+    LOAD_PROC(LPALISAUXILIARYEFFECTSLOT, alIsAuxiliaryEffectSlot);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTI, alAuxiliaryEffectSloti);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTIV, alAuxiliaryEffectSlotiv);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTF, alAuxiliaryEffectSlotf);
+    LOAD_PROC(LPALAUXILIARYEFFECTSLOTFV, alAuxiliaryEffectSlotfv);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTI, alGetAuxiliaryEffectSloti);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTIV, alGetAuxiliaryEffectSlotiv);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTF, alGetAuxiliaryEffectSlotf);
+    LOAD_PROC(LPALGETAUXILIARYEFFECTSLOTFV, alGetAuxiliaryEffectSlotfv);
+#undef LOAD_PROC
+
+    /* Load the sound into a buffer. */
+    buffer = LoadSound(argv[0]);
+    if(!buffer)
+    {
+        CloseAL();
+        return 1;
+    }
+
+    /* Load the reverb into an effect. */
+    effect = LoadEffect(&reverb);
+    if(!effect)
+    {
+        alDeleteBuffers(1, &buffer);
+        CloseAL();
+        return 1;
+    }
+
+    /* Create the effect slot object. This is what "plays" an effect on sources
+     * that connect to it. */
+    slot = 0;
+    alGenAuxiliaryEffectSlots(1, &slot);
+
+    /* Tell the effect slot to use the loaded effect object. Note that the this
+     * effectively copies the effect properties. You can modify or delete the
+     * effect object afterward without affecting the effect slot.
+     */
+    alAuxiliaryEffectSloti(slot, AL_EFFECTSLOT_EFFECT, (ALint)effect);
+    assert(alGetError()==AL_NO_ERROR && "Failed to set effect slot");
+
+    /* Create the source to play the sound with. */
+    source = 0;
+    alGenSources(1, &source);
+    alSourcei(source, AL_BUFFER, (ALint)buffer);
+
+    /* Connect the source to the effect slot. This tells the source to use the
+     * effect slot 'slot', on send #0 with the AL_FILTER_NULL filter object.
+     */
+    alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)slot, 0, AL_FILTER_NULL);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Play the sound until it finishes. */
+    alSourcePlay(source);
+    do {
+        al_nssleep(10000000);
+        alGetSourcei(source, AL_SOURCE_STATE, &state);
+    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);
+
+    /* All done. Delete resources, and close down OpenAL. */
+    alDeleteSources(1, &source);
+    alDeleteAuxiliaryEffectSlots(1, &slot);
+    alDeleteEffects(1, &effect);
+    alDeleteBuffers(1, &buffer);
+
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/alstream.c b/examples/alstream.c
new file mode 100644 (file)
index 0000000..a61680d
--- /dev/null
@@ -0,0 +1,519 @@
+/*
+ * OpenAL Audio Stream Example
+ *
+ * Copyright (c) 2011 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains a relatively simple streaming audio player. */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+
+/* Define the number of buffers and buffer size (in milliseconds) to use. 4
+ * buffers at 200ms each gives a nice per-chunk size, and lets the queue last
+ * for almost one second.
+ */
+#define NUM_BUFFERS 4
+#define BUFFER_MILLISEC 200
+
+typedef enum SampleType {
+    Int16, Float, IMA4, MSADPCM
+} SampleType;
+
+typedef struct StreamPlayer {
+    /* These are the buffers and source to play out through OpenAL with. */
+    ALuint buffers[NUM_BUFFERS];
+    ALuint source;
+
+    /* Handle for the audio file */
+    SNDFILE *sndfile;
+    SF_INFO sfinfo;
+    void *membuf;
+
+    /* The sample type and block/frame size being read for the buffer. */
+    SampleType sample_type;
+    int byteblockalign;
+    int sampleblockalign;
+    int block_count;
+
+    /* The format of the output stream (sample rate is in sfinfo) */
+    ALenum format;
+} StreamPlayer;
+
+static StreamPlayer *NewPlayer(void);
+static void DeletePlayer(StreamPlayer *player);
+static int OpenPlayerFile(StreamPlayer *player, const char *filename);
+static void ClosePlayerFile(StreamPlayer *player);
+static int StartPlayer(StreamPlayer *player);
+static int UpdatePlayer(StreamPlayer *player);
+
+/* Creates a new player object, and allocates the needed OpenAL source and
+ * buffer objects. Error checking is simplified for the purposes of this
+ * example, and will cause an abort if needed.
+ */
+static StreamPlayer *NewPlayer(void)
+{
+    StreamPlayer *player;
+
+    player = calloc(1, sizeof(*player));
+    assert(player != NULL);
+
+    /* Generate the buffers and source */
+    alGenBuffers(NUM_BUFFERS, player->buffers);
+    assert(alGetError() == AL_NO_ERROR && "Could not create buffers");
+
+    alGenSources(1, &player->source);
+    assert(alGetError() == AL_NO_ERROR && "Could not create source");
+
+    /* Set parameters so mono sources play out the front-center speaker and
+     * won't distance attenuate. */
+    alSource3i(player->source, AL_POSITION, 0, 0, -1);
+    alSourcei(player->source, AL_SOURCE_RELATIVE, AL_TRUE);
+    alSourcei(player->source, AL_ROLLOFF_FACTOR, 0);
+    assert(alGetError() == AL_NO_ERROR && "Could not set source parameters");
+
+    return player;
+}
+
+/* Destroys a player object, deleting the source and buffers. No error handling
+ * since these calls shouldn't fail with a properly-made player object. */
+static void DeletePlayer(StreamPlayer *player)
+{
+    ClosePlayerFile(player);
+
+    alDeleteSources(1, &player->source);
+    alDeleteBuffers(NUM_BUFFERS, player->buffers);
+    if(alGetError() != AL_NO_ERROR)
+        fprintf(stderr, "Failed to delete object IDs\n");
+
+    memset(player, 0, sizeof(*player));
+    free(player);
+}
+
+
+/* Opens the first audio stream of the named file. If a file is already open,
+ * it will be closed first. */
+static int OpenPlayerFile(StreamPlayer *player, const char *filename)
+{
+    int byteblockalign=0, splblockalign=0;
+
+    ClosePlayerFile(player);
+
+    /* Open the audio file and check that it's usable. */
+    player->sndfile = sf_open(filename, SFM_READ, &player->sfinfo);
+    if(!player->sndfile)
+    {
+        fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(NULL));
+        return 0;
+    }
+
+    /* Detect a suitable format to load. Formats like Vorbis and Opus use float
+     * natively, so load as float to avoid clipping when possible. Formats
+     * larger than 16-bit can also use float to preserve a bit more precision.
+     */
+    switch((player->sfinfo.format&SF_FORMAT_SUBMASK))
+    {
+    case SF_FORMAT_PCM_24:
+    case SF_FORMAT_PCM_32:
+    case SF_FORMAT_FLOAT:
+    case SF_FORMAT_DOUBLE:
+    case SF_FORMAT_VORBIS:
+    case SF_FORMAT_OPUS:
+    case SF_FORMAT_ALAC_20:
+    case SF_FORMAT_ALAC_24:
+    case SF_FORMAT_ALAC_32:
+    case 0x0080/*SF_FORMAT_MPEG_LAYER_I*/:
+    case 0x0081/*SF_FORMAT_MPEG_LAYER_II*/:
+    case 0x0082/*SF_FORMAT_MPEG_LAYER_III*/:
+        if(alIsExtensionPresent("AL_EXT_FLOAT32"))
+            player->sample_type = Float;
+        break;
+    case SF_FORMAT_IMA_ADPCM:
+        /* ADPCM formats require setting a block alignment as specified in the
+         * file, which needs to be read from the wave 'fmt ' chunk manually
+         * since libsndfile doesn't provide it in a format-agnostic way.
+         */
+        if(player->sfinfo.channels <= 2
+            && (player->sfinfo.format&SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV
+            && alIsExtensionPresent("AL_EXT_IMA4")
+            && alIsExtensionPresent("AL_SOFT_block_alignment"))
+            player->sample_type = IMA4;
+        break;
+    case SF_FORMAT_MS_ADPCM:
+        if(player->sfinfo.channels <= 2
+            && (player->sfinfo.format&SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV
+            && alIsExtensionPresent("AL_SOFT_MSADPCM")
+            && alIsExtensionPresent("AL_SOFT_block_alignment"))
+            player->sample_type = MSADPCM;
+        break;
+    }
+
+    if(player->sample_type == IMA4 || player->sample_type == MSADPCM)
+    {
+        /* For ADPCM, lookup the wave file's "fmt " chunk, which is a
+         * WAVEFORMATEX-based structure for the audio format.
+         */
+        SF_CHUNK_INFO inf = { "fmt ", 4, 0, NULL };
+        SF_CHUNK_ITERATOR *iter = sf_get_chunk_iterator(player->sndfile, &inf);
+
+        /* If there's an issue getting the chunk or block alignment, load as
+         * 16-bit and have libsndfile do the conversion.
+         */
+        if(!iter || sf_get_chunk_size(iter, &inf) != SF_ERR_NO_ERROR || inf.datalen < 14)
+            player->sample_type = Int16;
+        else
+        {
+            ALubyte *fmtbuf = calloc(inf.datalen, 1);
+            inf.data = fmtbuf;
+            if(sf_get_chunk_data(iter, &inf) != SF_ERR_NO_ERROR)
+                player->sample_type = Int16;
+            else
+            {
+                /* Read the nBlockAlign field, and convert from bytes- to
+                 * samples-per-block (verifying it's valid by converting back
+                 * and comparing to the original value).
+                 */
+                byteblockalign = fmtbuf[12] | (fmtbuf[13]<<8);
+                if(player->sample_type == IMA4)
+                {
+                    splblockalign = (byteblockalign/player->sfinfo.channels - 4)/4*8 + 1;
+                    if(splblockalign < 1
+                        || ((splblockalign-1)/2 + 4)*player->sfinfo.channels != byteblockalign)
+                        player->sample_type = Int16;
+                }
+                else
+                {
+                    splblockalign = (byteblockalign/player->sfinfo.channels - 7)*2 + 2;
+                    if(splblockalign < 2
+                        || ((splblockalign-2)/2 + 7)*player->sfinfo.channels != byteblockalign)
+                        player->sample_type = Int16;
+                }
+            }
+            free(fmtbuf);
+        }
+    }
+
+    if(player->sample_type == Int16)
+    {
+        player->sampleblockalign = 1;
+        player->byteblockalign = player->sfinfo.channels * 2;
+    }
+    else if(player->sample_type == Float)
+    {
+        player->sampleblockalign = 1;
+        player->byteblockalign = player->sfinfo.channels * 4;
+    }
+    else
+    {
+        player->sampleblockalign = splblockalign;
+        player->byteblockalign = byteblockalign;
+    }
+
+    /* Figure out the OpenAL format from the file and desired sample type. */
+    player->format = AL_NONE;
+    if(player->sfinfo.channels == 1)
+    {
+        if(player->sample_type == Int16)
+            player->format = AL_FORMAT_MONO16;
+        else if(player->sample_type == Float)
+            player->format = AL_FORMAT_MONO_FLOAT32;
+        else if(player->sample_type == IMA4)
+            player->format = AL_FORMAT_MONO_IMA4;
+        else if(player->sample_type == MSADPCM)
+            player->format = AL_FORMAT_MONO_MSADPCM_SOFT;
+    }
+    else if(player->sfinfo.channels == 2)
+    {
+        if(player->sample_type == Int16)
+            player->format = AL_FORMAT_STEREO16;
+        else if(player->sample_type == Float)
+            player->format = AL_FORMAT_STEREO_FLOAT32;
+        else if(player->sample_type == IMA4)
+            player->format = AL_FORMAT_STEREO_IMA4;
+        else if(player->sample_type == MSADPCM)
+            player->format = AL_FORMAT_STEREO_MSADPCM_SOFT;
+    }
+    else if(player->sfinfo.channels == 3)
+    {
+        if(sf_command(player->sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+        {
+            if(player->sample_type == Int16)
+                player->format = AL_FORMAT_BFORMAT2D_16;
+            else if(player->sample_type == Float)
+                player->format = AL_FORMAT_BFORMAT2D_FLOAT32;
+        }
+    }
+    else if(player->sfinfo.channels == 4)
+    {
+        if(sf_command(player->sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+        {
+            if(player->sample_type == Int16)
+                player->format = AL_FORMAT_BFORMAT3D_16;
+            else if(player->sample_type == Float)
+                player->format = AL_FORMAT_BFORMAT3D_FLOAT32;
+        }
+    }
+    if(!player->format)
+    {
+        fprintf(stderr, "Unsupported channel count: %d\n", player->sfinfo.channels);
+        sf_close(player->sndfile);
+        player->sndfile = NULL;
+        return 0;
+    }
+
+    player->block_count = player->sfinfo.samplerate / player->sampleblockalign;
+    player->block_count = player->block_count * BUFFER_MILLISEC / 1000;
+    player->membuf = malloc((size_t)(player->block_count * player->byteblockalign));
+
+    return 1;
+}
+
+/* Closes the audio file stream */
+static void ClosePlayerFile(StreamPlayer *player)
+{
+    if(player->sndfile)
+        sf_close(player->sndfile);
+    player->sndfile = NULL;
+
+    free(player->membuf);
+    player->membuf = NULL;
+
+    if(player->sampleblockalign > 1)
+    {
+        ALsizei i;
+        for(i = 0;i < NUM_BUFFERS;i++)
+            alBufferi(player->buffers[i], AL_UNPACK_BLOCK_ALIGNMENT_SOFT, 0);
+        player->sampleblockalign = 0;
+        player->byteblockalign = 0;
+    }
+}
+
+
+/* Prebuffers some audio from the file, and starts playing the source */
+static int StartPlayer(StreamPlayer *player)
+{
+    ALsizei i;
+
+    /* Rewind the source position and clear the buffer queue */
+    alSourceRewind(player->source);
+    alSourcei(player->source, AL_BUFFER, 0);
+
+    /* Fill the buffer queue */
+    for(i = 0;i < NUM_BUFFERS;i++)
+    {
+        sf_count_t slen;
+
+        /* Get some data to give it to the buffer */
+        if(player->sample_type == Int16)
+        {
+            slen = sf_readf_short(player->sndfile, player->membuf,
+                player->block_count * player->sampleblockalign);
+            if(slen < 1) break;
+            slen *= player->byteblockalign;
+        }
+        else if(player->sample_type == Float)
+        {
+            slen = sf_readf_float(player->sndfile, player->membuf,
+                player->block_count * player->sampleblockalign);
+            if(slen < 1) break;
+            slen *= player->byteblockalign;
+        }
+        else
+        {
+            slen = sf_read_raw(player->sndfile, player->membuf,
+                player->block_count * player->byteblockalign);
+            if(slen > 0) slen -= slen%player->byteblockalign;
+            if(slen < 1) break;
+        }
+
+        if(player->sampleblockalign > 1)
+            alBufferi(player->buffers[i], AL_UNPACK_BLOCK_ALIGNMENT_SOFT,
+                player->sampleblockalign);
+
+        alBufferData(player->buffers[i], player->format, player->membuf, (ALsizei)slen,
+            player->sfinfo.samplerate);
+    }
+    if(alGetError() != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Error buffering for playback\n");
+        return 0;
+    }
+
+    /* Now queue and start playback! */
+    alSourceQueueBuffers(player->source, i, player->buffers);
+    alSourcePlay(player->source);
+    if(alGetError() != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Error starting playback\n");
+        return 0;
+    }
+
+    return 1;
+}
+
+static int UpdatePlayer(StreamPlayer *player)
+{
+    ALint processed, state;
+
+    /* Get relevant source info */
+    alGetSourcei(player->source, AL_SOURCE_STATE, &state);
+    alGetSourcei(player->source, AL_BUFFERS_PROCESSED, &processed);
+    if(alGetError() != AL_NO_ERROR)
+    {
+        fprintf(stderr, "Error checking source state\n");
+        return 0;
+    }
+
+    /* Unqueue and handle each processed buffer */
+    while(processed > 0)
+    {
+        ALuint bufid;
+        sf_count_t slen;
+
+        alSourceUnqueueBuffers(player->source, 1, &bufid);
+        processed--;
+
+        /* Read the next chunk of data, refill the buffer, and queue it
+         * back on the source */
+        if(player->sample_type == Int16)
+        {
+            slen = sf_readf_short(player->sndfile, player->membuf,
+                player->block_count * player->sampleblockalign);
+            if(slen > 0) slen *= player->byteblockalign;
+        }
+        else if(player->sample_type == Float)
+        {
+            slen = sf_readf_float(player->sndfile, player->membuf,
+                player->block_count * player->sampleblockalign);
+            if(slen > 0) slen *= player->byteblockalign;
+        }
+        else
+        {
+            slen = sf_read_raw(player->sndfile, player->membuf,
+                player->block_count * player->byteblockalign);
+            if(slen > 0) slen -= slen%player->byteblockalign;
+        }
+
+        if(slen > 0)
+        {
+            alBufferData(bufid, player->format, player->membuf, (ALsizei)slen,
+                player->sfinfo.samplerate);
+            alSourceQueueBuffers(player->source, 1, &bufid);
+        }
+        if(alGetError() != AL_NO_ERROR)
+        {
+            fprintf(stderr, "Error buffering data\n");
+            return 0;
+        }
+    }
+
+    /* Make sure the source hasn't underrun */
+    if(state != AL_PLAYING && state != AL_PAUSED)
+    {
+        ALint queued;
+
+        /* If no buffers are queued, playback is finished */
+        alGetSourcei(player->source, AL_BUFFERS_QUEUED, &queued);
+        if(queued == 0)
+            return 0;
+
+        alSourcePlay(player->source);
+        if(alGetError() != AL_NO_ERROR)
+        {
+            fprintf(stderr, "Error restarting playback\n");
+            return 0;
+        }
+    }
+
+    return 1;
+}
+
+
+int main(int argc, char **argv)
+{
+    StreamPlayer *player;
+    int i;
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name>] <filenames...>\n", argv[0]);
+        return 1;
+    }
+
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    player = NewPlayer();
+
+    /* Play each file listed on the command line */
+    for(i = 0;i < argc;i++)
+    {
+        const char *namepart;
+
+        if(!OpenPlayerFile(player, argv[i]))
+            continue;
+
+        /* Get the name portion, without the path, for display. */
+        namepart = strrchr(argv[i], '/');
+        if(namepart || (namepart=strrchr(argv[i], '\\')))
+            namepart++;
+        else
+            namepart = argv[i];
+
+        printf("Playing: %s (%s, %dhz)\n", namepart, FormatName(player->format),
+            player->sfinfo.samplerate);
+        fflush(stdout);
+
+        if(!StartPlayer(player))
+        {
+            ClosePlayerFile(player);
+            continue;
+        }
+
+        while(UpdatePlayer(player))
+            al_nssleep(10000000);
+
+        /* All done with this file. Close it and go to the next */
+        ClosePlayerFile(player);
+    }
+    printf("Done.\n");
+
+    /* All files done. Delete the player, and close down OpenAL */
+    DeletePlayer(player);
+    player = NULL;
+
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/alstreamcb.cpp b/examples/alstreamcb.cpp
new file mode 100644 (file)
index 0000000..a2e7b65
--- /dev/null
@@ -0,0 +1,551 @@
+/*
+ * OpenAL Callback-based Stream Example
+ *
+ * Copyright (c) 2020 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains a streaming audio player using a callback buffer. */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <atomic>
+#include <chrono>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "sndfile.h"
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+
+namespace {
+
+using std::chrono::seconds;
+using std::chrono::nanoseconds;
+
+LPALBUFFERCALLBACKSOFT alBufferCallbackSOFT;
+
+struct StreamPlayer {
+    /* A lockless ring-buffer (supports single-provider, single-consumer
+     * operation).
+     */
+    std::unique_ptr<ALbyte[]> mBufferData;
+    size_t mBufferDataSize{0};
+    std::atomic<size_t> mReadPos{0};
+    std::atomic<size_t> mWritePos{0};
+    size_t mSamplesPerBlock{1};
+    size_t mBytesPerBlock{1};
+
+    enum class SampleType {
+        Int16, Float, IMA4, MSADPCM
+    };
+    SampleType mSampleFormat{SampleType::Int16};
+
+    /* The buffer to get the callback, and source to play with. */
+    ALuint mBuffer{0}, mSource{0};
+    size_t mStartOffset{0};
+
+    /* Handle for the audio file to decode. */
+    SNDFILE *mSndfile{nullptr};
+    SF_INFO mSfInfo{};
+    size_t mDecoderOffset{0};
+
+    /* The format of the callback samples. */
+    ALenum mFormat;
+
+    StreamPlayer()
+    {
+        alGenBuffers(1, &mBuffer);
+        if(alGetError() != AL_NO_ERROR)
+            throw std::runtime_error{"alGenBuffers failed"};
+        alGenSources(1, &mSource);
+        if(alGetError() != AL_NO_ERROR)
+        {
+            alDeleteBuffers(1, &mBuffer);
+            throw std::runtime_error{"alGenSources failed"};
+        }
+    }
+    ~StreamPlayer()
+    {
+        alDeleteSources(1, &mSource);
+        alDeleteBuffers(1, &mBuffer);
+        if(mSndfile)
+            sf_close(mSndfile);
+    }
+
+    void close()
+    {
+        if(mSamplesPerBlock > 1)
+            alBufferi(mBuffer, AL_UNPACK_BLOCK_ALIGNMENT_SOFT, 0);
+
+        if(mSndfile)
+        {
+            alSourceRewind(mSource);
+            alSourcei(mSource, AL_BUFFER, 0);
+            sf_close(mSndfile);
+            mSndfile = nullptr;
+        }
+    }
+
+    bool open(const char *filename)
+    {
+        close();
+
+        /* Open the file and figure out the OpenAL format. */
+        mSndfile = sf_open(filename, SFM_READ, &mSfInfo);
+        if(!mSndfile)
+        {
+            fprintf(stderr, "Could not open audio in %s: %s\n", filename, sf_strerror(mSndfile));
+            return false;
+        }
+
+        switch((mSfInfo.format&SF_FORMAT_SUBMASK))
+        {
+        case SF_FORMAT_PCM_24:
+        case SF_FORMAT_PCM_32:
+        case SF_FORMAT_FLOAT:
+        case SF_FORMAT_DOUBLE:
+        case SF_FORMAT_VORBIS:
+        case SF_FORMAT_OPUS:
+        case SF_FORMAT_ALAC_20:
+        case SF_FORMAT_ALAC_24:
+        case SF_FORMAT_ALAC_32:
+        case 0x0080/*SF_FORMAT_MPEG_LAYER_I*/:
+        case 0x0081/*SF_FORMAT_MPEG_LAYER_II*/:
+        case 0x0082/*SF_FORMAT_MPEG_LAYER_III*/:
+            if(alIsExtensionPresent("AL_EXT_FLOAT32"))
+                mSampleFormat = SampleType::Float;
+            break;
+        case SF_FORMAT_IMA_ADPCM:
+            if(mSfInfo.channels <= 2 && (mSfInfo.format&SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV
+                && alIsExtensionPresent("AL_EXT_IMA4")
+                && alIsExtensionPresent("AL_SOFT_block_alignment"))
+                mSampleFormat = SampleType::IMA4;
+            break;
+        case SF_FORMAT_MS_ADPCM:
+            if(mSfInfo.channels <= 2 && (mSfInfo.format&SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV
+                && alIsExtensionPresent("AL_SOFT_MSADPCM")
+                && alIsExtensionPresent("AL_SOFT_block_alignment"))
+                mSampleFormat = SampleType::MSADPCM;
+            break;
+        }
+
+        int splblocksize{}, byteblocksize{};
+        if(mSampleFormat == SampleType::IMA4 || mSampleFormat == SampleType::MSADPCM)
+        {
+            SF_CHUNK_INFO inf{ "fmt ", 4, 0, nullptr };
+            SF_CHUNK_ITERATOR *iter = sf_get_chunk_iterator(mSndfile, &inf);
+            if(!iter || sf_get_chunk_size(iter, &inf) != SF_ERR_NO_ERROR || inf.datalen < 14)
+                mSampleFormat = SampleType::Int16;
+            else
+            {
+                auto fmtbuf = std::make_unique<ALubyte[]>(inf.datalen);
+                inf.data = fmtbuf.get();
+                if(sf_get_chunk_data(iter, &inf) != SF_ERR_NO_ERROR)
+                    mSampleFormat = SampleType::Int16;
+                else
+                {
+                    byteblocksize = fmtbuf[12] | (fmtbuf[13]<<8u);
+                    if(mSampleFormat == SampleType::IMA4)
+                    {
+                        splblocksize = (byteblocksize/mSfInfo.channels - 4)/4*8 + 1;
+                        if(splblocksize < 1
+                            || ((splblocksize-1)/2 + 4)*mSfInfo.channels != byteblocksize)
+                            mSampleFormat = SampleType::Int16;
+                    }
+                    else
+                    {
+                        splblocksize = (byteblocksize/mSfInfo.channels - 7)*2 + 2;
+                        if(splblocksize < 2
+                            || ((splblocksize-2)/2 + 7)*mSfInfo.channels != byteblocksize)
+                            mSampleFormat = SampleType::Int16;
+                    }
+                }
+            }
+        }
+
+        if(mSampleFormat == SampleType::Int16)
+        {
+            mSamplesPerBlock = 1;
+            mBytesPerBlock = static_cast<size_t>(mSfInfo.channels * 2);
+        }
+        else if(mSampleFormat == SampleType::Float)
+        {
+            mSamplesPerBlock = 1;
+            mBytesPerBlock = static_cast<size_t>(mSfInfo.channels * 4);
+        }
+        else
+        {
+            mSamplesPerBlock = static_cast<size_t>(splblocksize);
+            mBytesPerBlock = static_cast<size_t>(byteblocksize);
+        }
+
+        mFormat = AL_NONE;
+        if(mSfInfo.channels == 1)
+        {
+            if(mSampleFormat == SampleType::Int16)
+                mFormat = AL_FORMAT_MONO16;
+            else if(mSampleFormat == SampleType::Float)
+                mFormat = AL_FORMAT_MONO_FLOAT32;
+            else if(mSampleFormat == SampleType::IMA4)
+                mFormat = AL_FORMAT_MONO_IMA4;
+            else if(mSampleFormat == SampleType::MSADPCM)
+                mFormat = AL_FORMAT_MONO_MSADPCM_SOFT;
+        }
+        else if(mSfInfo.channels == 2)
+        {
+            if(mSampleFormat == SampleType::Int16)
+                mFormat = AL_FORMAT_STEREO16;
+            else if(mSampleFormat == SampleType::Float)
+                mFormat = AL_FORMAT_STEREO_FLOAT32;
+            else if(mSampleFormat == SampleType::IMA4)
+                mFormat = AL_FORMAT_STEREO_IMA4;
+            else if(mSampleFormat == SampleType::MSADPCM)
+                mFormat = AL_FORMAT_STEREO_MSADPCM_SOFT;
+        }
+        else if(mSfInfo.channels == 3)
+        {
+            if(sf_command(mSndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            {
+                if(mSampleFormat == SampleType::Int16)
+                    mFormat = AL_FORMAT_BFORMAT2D_16;
+                else if(mSampleFormat == SampleType::Float)
+                    mFormat = AL_FORMAT_BFORMAT2D_FLOAT32;
+            }
+        }
+        else if(mSfInfo.channels == 4)
+        {
+            if(sf_command(mSndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+            {
+                if(mSampleFormat == SampleType::Int16)
+                    mFormat = AL_FORMAT_BFORMAT3D_16;
+                else if(mSampleFormat == SampleType::Float)
+                    mFormat = AL_FORMAT_BFORMAT3D_FLOAT32;
+            }
+        }
+        if(!mFormat)
+        {
+            fprintf(stderr, "Unsupported channel count: %d\n", mSfInfo.channels);
+            sf_close(mSndfile);
+            mSndfile = nullptr;
+
+            return false;
+        }
+
+        /* Set a 1s ring buffer size. */
+        size_t numblocks{(static_cast<ALuint>(mSfInfo.samplerate) + mSamplesPerBlock-1)
+            / mSamplesPerBlock};
+        mBufferDataSize = static_cast<ALuint>(numblocks * mBytesPerBlock);
+        mBufferData.reset(new ALbyte[mBufferDataSize]);
+        mReadPos.store(0, std::memory_order_relaxed);
+        mWritePos.store(0, std::memory_order_relaxed);
+        mDecoderOffset = 0;
+
+        return true;
+    }
+
+    /* The actual C-style callback just forwards to the non-static method. Not
+     * strictly needed and the compiler will optimize it to a normal function,
+     * but it allows the callback implementation to have a nice 'this' pointer
+     * with normal member access.
+     */
+    static ALsizei AL_APIENTRY bufferCallbackC(void *userptr, void *data, ALsizei size)
+    { return static_cast<StreamPlayer*>(userptr)->bufferCallback(data, size); }
+    ALsizei bufferCallback(void *data, ALsizei size)
+    {
+        /* NOTE: The callback *MUST* be real-time safe! That means no blocking,
+         * no allocations or deallocations, no I/O, no page faults, or calls to
+         * functions that could do these things (this includes calling to
+         * libraries like SDL_sound, libsndfile, ffmpeg, etc). Nothing should
+         * unexpectedly stall this call since the audio has to get to the
+         * device on time.
+         */
+        ALsizei got{0};
+
+        size_t roffset{mReadPos.load(std::memory_order_acquire)};
+        while(got < size)
+        {
+            /* If the write offset == read offset, there's nothing left in the
+             * ring-buffer. Break from the loop and give what has been written.
+             */
+            const size_t woffset{mWritePos.load(std::memory_order_relaxed)};
+            if(woffset == roffset) break;
+
+            /* If the write offset is behind the read offset, the readable
+             * portion wrapped around. Just read up to the end of the buffer in
+             * that case, otherwise read up to the write offset. Also limit the
+             * amount to copy given how much is remaining to write.
+             */
+            size_t todo{((woffset < roffset) ? mBufferDataSize : woffset) - roffset};
+            todo = std::min<size_t>(todo, static_cast<ALuint>(size-got));
+
+            /* Copy from the ring buffer to the provided output buffer. Wrap
+             * the resulting read offset if it reached the end of the ring-
+             * buffer.
+             */
+            memcpy(data, &mBufferData[roffset], todo);
+            data = static_cast<ALbyte*>(data) + todo;
+            got += static_cast<ALsizei>(todo);
+
+            roffset += todo;
+            if(roffset == mBufferDataSize)
+                roffset = 0;
+        }
+        /* Finally, store the updated read offset, and return how many bytes
+         * have been written.
+         */
+        mReadPos.store(roffset, std::memory_order_release);
+
+        return got;
+    }
+
+    bool prepare()
+    {
+        if(mSamplesPerBlock > 1)
+            alBufferi(mBuffer, AL_UNPACK_BLOCK_ALIGNMENT_SOFT, static_cast<int>(mSamplesPerBlock));
+        alBufferCallbackSOFT(mBuffer, mFormat, mSfInfo.samplerate, bufferCallbackC, this);
+        alSourcei(mSource, AL_BUFFER, static_cast<ALint>(mBuffer));
+        if(ALenum err{alGetError()})
+        {
+            fprintf(stderr, "Failed to set callback: %s (0x%04x)\n", alGetString(err), err);
+            return false;
+        }
+        return true;
+    }
+
+    bool update()
+    {
+        ALenum state;
+        ALint pos;
+        alGetSourcei(mSource, AL_SAMPLE_OFFSET, &pos);
+        alGetSourcei(mSource, AL_SOURCE_STATE, &state);
+
+        size_t woffset{mWritePos.load(std::memory_order_acquire)};
+        if(state != AL_INITIAL)
+        {
+            const size_t roffset{mReadPos.load(std::memory_order_relaxed)};
+            const size_t readable{((woffset >= roffset) ? woffset : (mBufferDataSize+woffset)) -
+                roffset};
+            /* For a stopped (underrun) source, the current playback offset is
+             * the current decoder offset excluding the readable buffered data.
+             * For a playing/paused source, it's the source's offset including
+             * the playback offset the source was started with.
+             */
+            const size_t curtime{((state == AL_STOPPED)
+                ? (mDecoderOffset-readable) / mBytesPerBlock * mSamplesPerBlock
+                : (static_cast<ALuint>(pos) + mStartOffset/mBytesPerBlock*mSamplesPerBlock))
+                / static_cast<ALuint>(mSfInfo.samplerate)};
+            printf("\r%3zus (%3zu%% full)", curtime, readable * 100 / mBufferDataSize);
+        }
+        else
+            fputs("Starting...", stdout);
+        fflush(stdout);
+
+        while(!sf_error(mSndfile))
+        {
+            size_t read_bytes;
+            const size_t roffset{mReadPos.load(std::memory_order_relaxed)};
+            if(roffset > woffset)
+            {
+                /* Note that the ring buffer's writable space is one byte less
+                 * than the available area because the write offset ending up
+                 * at the read offset would be interpreted as being empty
+                 * instead of full.
+                 */
+                const size_t writable{(roffset-woffset-1) / mBytesPerBlock};
+                if(!writable) break;
+
+                if(mSampleFormat == SampleType::Int16)
+                {
+                    sf_count_t num_frames{sf_readf_short(mSndfile,
+                        reinterpret_cast<short*>(&mBufferData[woffset]),
+                        static_cast<sf_count_t>(writable*mSamplesPerBlock))};
+                    if(num_frames < 1) break;
+                    read_bytes = static_cast<size_t>(num_frames) * mBytesPerBlock;
+                }
+                else if(mSampleFormat == SampleType::Float)
+                {
+                    sf_count_t num_frames{sf_readf_float(mSndfile,
+                        reinterpret_cast<float*>(&mBufferData[woffset]),
+                        static_cast<sf_count_t>(writable*mSamplesPerBlock))};
+                    if(num_frames < 1) break;
+                    read_bytes = static_cast<size_t>(num_frames) * mBytesPerBlock;
+                }
+                else
+                {
+                    sf_count_t numbytes{sf_read_raw(mSndfile, &mBufferData[woffset],
+                        static_cast<sf_count_t>(writable*mBytesPerBlock))};
+                    if(numbytes < 1) break;
+                    read_bytes = static_cast<size_t>(numbytes);
+                }
+
+                woffset += read_bytes;
+            }
+            else
+            {
+                /* If the read offset is at or behind the write offset, the
+                 * writeable area (might) wrap around. Make sure the sample
+                 * data can fit, and calculate how much can go in front before
+                 * wrapping.
+                 */
+                const size_t writable{(!roffset ? mBufferDataSize-woffset-1 :
+                    (mBufferDataSize-woffset)) / mBytesPerBlock};
+                if(!writable) break;
+
+                if(mSampleFormat == SampleType::Int16)
+                {
+                    sf_count_t num_frames{sf_readf_short(mSndfile,
+                        reinterpret_cast<short*>(&mBufferData[woffset]),
+                        static_cast<sf_count_t>(writable*mSamplesPerBlock))};
+                    if(num_frames < 1) break;
+                    read_bytes = static_cast<size_t>(num_frames) * mBytesPerBlock;
+                }
+                else if(mSampleFormat == SampleType::Float)
+                {
+                    sf_count_t num_frames{sf_readf_float(mSndfile,
+                        reinterpret_cast<float*>(&mBufferData[woffset]),
+                        static_cast<sf_count_t>(writable*mSamplesPerBlock))};
+                    if(num_frames < 1) break;
+                    read_bytes = static_cast<size_t>(num_frames) * mBytesPerBlock;
+                }
+                else
+                {
+                    sf_count_t numbytes{sf_read_raw(mSndfile, &mBufferData[woffset],
+                        static_cast<sf_count_t>(writable*mBytesPerBlock))};
+                    if(numbytes < 1) break;
+                    read_bytes = static_cast<size_t>(numbytes);
+                }
+
+                woffset += read_bytes;
+                if(woffset == mBufferDataSize)
+                    woffset = 0;
+            }
+            mWritePos.store(woffset, std::memory_order_release);
+            mDecoderOffset += read_bytes;
+        }
+
+        if(state != AL_PLAYING && state != AL_PAUSED)
+        {
+            /* If the source is not playing or paused, it either underrun
+             * (AL_STOPPED) or is just getting started (AL_INITIAL). If the
+             * ring buffer is empty, it's done, otherwise play the source with
+             * what's available.
+             */
+            const size_t roffset{mReadPos.load(std::memory_order_relaxed)};
+            const size_t readable{((woffset >= roffset) ? woffset : (mBufferDataSize+woffset)) -
+                roffset};
+            if(readable == 0)
+                return false;
+
+            /* Store the playback offset that the source will start reading
+             * from, so it can be tracked during playback.
+             */
+            mStartOffset = mDecoderOffset - readable;
+            alSourcePlay(mSource);
+            if(alGetError() != AL_NO_ERROR)
+                return false;
+        }
+        return true;
+    }
+};
+
+} // namespace
+
+int main(int argc, char **argv)
+{
+    /* A simple RAII container for OpenAL startup and shutdown. */
+    struct AudioManager {
+        AudioManager(char ***argv_, int *argc_)
+        {
+            if(InitAL(argv_, argc_) != 0)
+                throw std::runtime_error{"Failed to initialize OpenAL"};
+        }
+        ~AudioManager() { CloseAL(); }
+    };
+
+    /* Print out usage if no arguments were specified */
+    if(argc < 2)
+    {
+        fprintf(stderr, "Usage: %s [-device <name>] <filenames...>\n", argv[0]);
+        return 1;
+    }
+
+    argv++; argc--;
+    AudioManager almgr{&argv, &argc};
+
+    if(!alIsExtensionPresent("AL_SOFT_callback_buffer"))
+    {
+        fprintf(stderr, "AL_SOFT_callback_buffer extension not available\n");
+        return 1;
+    }
+
+    alBufferCallbackSOFT = reinterpret_cast<LPALBUFFERCALLBACKSOFT>(
+        alGetProcAddress("alBufferCallbackSOFT"));
+
+    ALCint refresh{25};
+    alcGetIntegerv(alcGetContextsDevice(alcGetCurrentContext()), ALC_REFRESH, 1, &refresh);
+
+    std::unique_ptr<StreamPlayer> player{new StreamPlayer{}};
+
+    /* Play each file listed on the command line */
+    for(int i{0};i < argc;++i)
+    {
+        if(!player->open(argv[i]))
+            continue;
+
+        /* Get the name portion, without the path, for display. */
+        const char *namepart{strrchr(argv[i], '/')};
+        if(namepart || (namepart=strrchr(argv[i], '\\')))
+            ++namepart;
+        else
+            namepart = argv[i];
+
+        printf("Playing: %s (%s, %dhz)\n", namepart, FormatName(player->mFormat),
+            player->mSfInfo.samplerate);
+        fflush(stdout);
+
+        if(!player->prepare())
+        {
+            player->close();
+            continue;
+        }
+
+        while(player->update())
+            std::this_thread::sleep_for(nanoseconds{seconds{1}} / refresh);
+        putc('\n', stdout);
+
+        /* All done with this file. Close it and go to the next */
+        player->close();
+    }
+    /* All done. */
+    printf("Done.\n");
+
+    return 0;
+}
diff --git a/examples/altonegen.c b/examples/altonegen.c
new file mode 100644 (file)
index 0000000..75db2d6
--- /dev/null
@@ -0,0 +1,330 @@
+/*
+ * OpenAL Tone Generator Test
+ *
+ * Copyright (c) 2015 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains a test for generating waveforms and plays them for a
+ * given length of time. Intended to inspect the behavior of the mixer by
+ * checking the output with a spectrum analyzer and oscilloscope.
+ *
+ * TODO: This would actually be nicer as a GUI app with buttons to start and
+ * stop individual waveforms, include additional whitenoise and pinknoise
+ * generators, and have the ability to hook up EFX filters and effects.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <limits.h>
+#include <math.h>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+#include "common/alhelpers.h"
+
+#include "win_main_utf8.h"
+
+#ifndef M_PI
+#define M_PI    (3.14159265358979323846)
+#endif
+
+enum WaveType {
+    WT_Sine,
+    WT_Square,
+    WT_Sawtooth,
+    WT_Triangle,
+    WT_Impulse,
+    WT_WhiteNoise,
+};
+
+static const char *GetWaveTypeName(enum WaveType type)
+{
+    switch(type)
+    {
+        case WT_Sine: return "sine";
+        case WT_Square: return "square";
+        case WT_Sawtooth: return "sawtooth";
+        case WT_Triangle: return "triangle";
+        case WT_Impulse: return "impulse";
+        case WT_WhiteNoise: return "noise";
+    }
+    return "(unknown)";
+}
+
+static inline ALuint dither_rng(ALuint *seed)
+{
+    *seed = (*seed * 96314165) + 907633515;
+    return *seed;
+}
+
+static void ApplySin(ALfloat *data, ALdouble g, ALuint srate, ALuint freq)
+{
+    ALdouble smps_per_cycle = (ALdouble)srate / freq;
+    ALuint i;
+    for(i = 0;i < srate;i++)
+    {
+        ALdouble ival;
+        data[i] += (ALfloat)(sin(modf(i/smps_per_cycle, &ival) * 2.0*M_PI) * g);
+    }
+}
+
+/* Generates waveforms using additive synthesis. Each waveform is constructed
+ * by summing one or more sine waves, up to (and excluding) nyquist.
+ */
+static ALuint CreateWave(enum WaveType type, ALuint freq, ALuint srate, ALfloat gain)
+{
+    ALuint seed = 22222;
+    ALuint data_size;
+    ALfloat *data;
+    ALuint buffer;
+    ALenum err;
+    ALuint i;
+
+    data_size = (ALuint)(srate * sizeof(ALfloat));
+    data = calloc(1, data_size);
+    switch(type)
+    {
+        case WT_Sine:
+            ApplySin(data, 1.0, srate, freq);
+            break;
+        case WT_Square:
+            for(i = 1;freq*i < srate/2;i+=2)
+                ApplySin(data, 4.0/M_PI * 1.0/i, srate, freq*i);
+            break;
+        case WT_Sawtooth:
+            for(i = 1;freq*i < srate/2;i++)
+                ApplySin(data, 2.0/M_PI * ((i&1)*2 - 1.0) / i, srate, freq*i);
+            break;
+        case WT_Triangle:
+            for(i = 1;freq*i < srate/2;i+=2)
+                ApplySin(data, 8.0/(M_PI*M_PI) * (1.0 - (i&2)) / (i*i), srate, freq*i);
+            break;
+        case WT_Impulse:
+            /* NOTE: Impulse isn't handled using additive synthesis, and is
+             * instead just a non-0 sample at a given rate. This can still be
+             * useful to test (other than resampling, the ALSOFT_DEFAULT_REVERB
+             * environment variable can prove useful here to test the reverb
+             * response).
+             */
+            for(i = 0;i < srate;i++)
+                data[i] = (i%(srate/freq)) ? 0.0f : 1.0f;
+            break;
+        case WT_WhiteNoise:
+            /* NOTE: WhiteNoise is just uniform set of uncorrelated values, and
+             * is not influenced by the waveform frequency.
+             */
+            for(i = 0;i < srate;i++)
+            {
+                ALuint rng0 = dither_rng(&seed);
+                ALuint rng1 = dither_rng(&seed);
+                data[i] = (ALfloat)(rng0*(1.0/UINT_MAX) - rng1*(1.0/UINT_MAX));
+            }
+            break;
+    }
+
+    if(gain != 1.0f)
+    {
+        for(i = 0;i < srate;i++)
+            data[i] *= gain;
+    }
+
+    /* Buffer the audio data into a new buffer object. */
+    buffer = 0;
+    alGenBuffers(1, &buffer);
+    alBufferData(buffer, AL_FORMAT_MONO_FLOAT32, data, (ALsizei)data_size, (ALsizei)srate);
+    free(data);
+
+    /* Check if an error occured, and clean up if so. */
+    err = alGetError();
+    if(err != AL_NO_ERROR)
+    {
+        fprintf(stderr, "OpenAL Error: %s\n", alGetString(err));
+        if(alIsBuffer(buffer))
+            alDeleteBuffers(1, &buffer);
+        return 0;
+    }
+
+    return buffer;
+}
+
+
+int main(int argc, char *argv[])
+{
+    enum WaveType wavetype = WT_Sine;
+    const char *appname = argv[0];
+    ALuint source, buffer;
+    ALint last_pos, num_loops;
+    ALint max_loops = 4;
+    ALint srate = -1;
+    ALint tone_freq = 1000;
+    ALCint dev_rate;
+    ALenum state;
+    ALfloat gain = 1.0f;
+    int i;
+
+    argv++; argc--;
+    if(InitAL(&argv, &argc) != 0)
+        return 1;
+
+    if(!alIsExtensionPresent("AL_EXT_FLOAT32"))
+    {
+        fprintf(stderr, "Required AL_EXT_FLOAT32 extension not supported on this device!\n");
+        CloseAL();
+        return 1;
+    }
+
+    for(i = 0;i < argc;i++)
+    {
+        if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "-?") == 0
+            || strcmp(argv[i], "--help") == 0)
+        {
+            fprintf(stderr, "OpenAL Tone Generator\n"
+"\n"
+"Usage: %s [-device <name>] <options>\n"
+"\n"
+"Available options:\n"
+"  --help/-h                 This help text\n"
+"  -t <seconds>              Time to play a tone (default 5 seconds)\n"
+"  --waveform/-w <type>      Waveform type: sine (default), square, sawtooth,\n"
+"                                triangle, impulse, noise\n"
+"  --freq/-f <hz>            Tone frequency (default 1000 hz)\n"
+"  --gain/-g <gain>          gain 0.0 to 1 (default 1)\n"
+"  --srate/-s <sample rate>  Sampling rate (default output rate)\n",
+                appname
+            );
+            CloseAL();
+            return 1;
+        }
+        else if(i+1 < argc && strcmp(argv[i], "-t") == 0)
+        {
+            i++;
+            max_loops = atoi(argv[i]) - 1;
+        }
+        else if(i+1 < argc && (strcmp(argv[i], "--waveform") == 0 || strcmp(argv[i], "-w") == 0))
+        {
+            i++;
+            if(strcmp(argv[i], "sine") == 0)
+                wavetype = WT_Sine;
+            else if(strcmp(argv[i], "square") == 0)
+                wavetype = WT_Square;
+            else if(strcmp(argv[i], "sawtooth") == 0)
+                wavetype = WT_Sawtooth;
+            else if(strcmp(argv[i], "triangle") == 0)
+                wavetype = WT_Triangle;
+            else if(strcmp(argv[i], "impulse") == 0)
+                wavetype = WT_Impulse;
+            else if(strcmp(argv[i], "noise") == 0)
+                wavetype = WT_WhiteNoise;
+            else
+                fprintf(stderr, "Unhandled waveform: %s\n", argv[i]);
+        }
+        else if(i+1 < argc && (strcmp(argv[i], "--freq") == 0 || strcmp(argv[i], "-f") == 0))
+        {
+            i++;
+            tone_freq = atoi(argv[i]);
+            if(tone_freq < 1)
+            {
+                fprintf(stderr, "Invalid tone frequency: %s (min: 1hz)\n", argv[i]);
+                tone_freq = 1;
+            }
+        }
+        else if(i+1 < argc && (strcmp(argv[i], "--gain") == 0 || strcmp(argv[i], "-g") == 0))
+        {
+            i++;
+            gain = (ALfloat)atof(argv[i]);
+            if(gain < 0.0f || gain > 1.0f)
+            {
+                fprintf(stderr, "Invalid gain: %s (min: 0.0, max 1.0)\n", argv[i]);
+                gain = 1.0f;
+            }
+        }
+        else if(i+1 < argc && (strcmp(argv[i], "--srate") == 0 || strcmp(argv[i], "-s") == 0))
+        {
+            i++;
+            srate = atoi(argv[i]);
+            if(srate < 40)
+            {
+                fprintf(stderr, "Invalid sample rate: %s (min: 40hz)\n", argv[i]);
+                srate = 40;
+            }
+        }
+    }
+
+    {
+        ALCdevice *device = alcGetContextsDevice(alcGetCurrentContext());
+        alcGetIntegerv(device, ALC_FREQUENCY, 1, &dev_rate);
+        assert(alcGetError(device)==ALC_NO_ERROR && "Failed to get device sample rate");
+    }
+    if(srate < 0)
+        srate = dev_rate;
+
+    /* Load the sound into a buffer. */
+    buffer = CreateWave(wavetype, (ALuint)tone_freq, (ALuint)srate, gain);
+    if(!buffer)
+    {
+        CloseAL();
+        return 1;
+    }
+
+    printf("Playing %dhz %s-wave tone with %dhz sample rate and %dhz output, for %d second%s...\n",
+           tone_freq, GetWaveTypeName(wavetype), srate, dev_rate, max_loops+1, max_loops?"s":"");
+    fflush(stdout);
+
+    /* Create the source to play the sound with. */
+    source = 0;
+    alGenSources(1, &source);
+    alSourcei(source, AL_BUFFER, (ALint)buffer);
+    assert(alGetError()==AL_NO_ERROR && "Failed to setup sound source");
+
+    /* Play the sound for a while. */
+    num_loops = 0;
+    last_pos = 0;
+    alSourcei(source, AL_LOOPING, (max_loops > 0) ? AL_TRUE : AL_FALSE);
+    alSourcePlay(source);
+    do {
+        ALint pos;
+        al_nssleep(10000000);
+        alGetSourcei(source, AL_SAMPLE_OFFSET, &pos);
+        alGetSourcei(source, AL_SOURCE_STATE, &state);
+        if(pos < last_pos && state == AL_PLAYING)
+        {
+            ++num_loops;
+            if(num_loops >= max_loops)
+                alSourcei(source, AL_LOOPING, AL_FALSE);
+            printf("%d...\n", max_loops - num_loops + 1);
+            fflush(stdout);
+        }
+        last_pos = pos;
+    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);
+
+    /* All done. Delete resources, and close OpenAL. */
+    alDeleteSources(1, &source);
+    alDeleteBuffers(1, &buffer);
+
+    /* Close up OpenAL. */
+    CloseAL();
+
+    return 0;
+}
diff --git a/examples/common/alhelpers.c b/examples/common/alhelpers.c
new file mode 100644 (file)
index 0000000..6627f70
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+ * OpenAL Helpers
+ *
+ * Copyright (c) 2011 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/* This file contains routines to help with some menial OpenAL-related tasks,
+ * such as opening a device and setting up a context, closing the device and
+ * destroying its context, converting between frame counts and byte lengths,
+ * finding an appropriate buffer format, and getting readable strings for
+ * channel configs and sample types. */
+
+#include "alhelpers.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+
+#include "AL/al.h"
+#include "AL/alc.h"
+#include "AL/alext.h"
+
+
+/* InitAL opens a device and sets up a context using default attributes, making
+ * the program ready to call OpenAL functions. */
+int InitAL(char ***argv, int *argc)
+{
+    const ALCchar *name;
+    ALCdevice *device;
+    ALCcontext *ctx;
+
+    /* Open and initialize a device */
+    device = NULL;
+    if(argc && argv && *argc > 1 && strcmp((*argv)[0], "-device") == 0)
+    {
+        device = alcOpenDevice((*argv)[1]);
+        if(!device)
+            fprintf(stderr, "Failed to open \"%s\", trying default\n", (*argv)[1]);
+        (*argv) += 2;
+        (*argc) -= 2;
+    }
+    if(!device)
+        device = alcOpenDevice(NULL);
+    if(!device)
+    {
+        fprintf(stderr, "Could not open a device!\n");
+        return 1;
+    }
+
+    ctx = alcCreateContext(device, NULL);
+    if(ctx == NULL || alcMakeContextCurrent(ctx) == ALC_FALSE)
+    {
+        if(ctx != NULL)
+            alcDestroyContext(ctx);
+        alcCloseDevice(device);
+        fprintf(stderr, "Could not set a context!\n");
+        return 1;
+    }
+
+    name = NULL;
+    if(alcIsExtensionPresent(device, "ALC_ENUMERATE_ALL_EXT"))
+        name = alcGetString(device, ALC_ALL_DEVICES_SPECIFIER);
+    if(!name || alcGetError(device) != AL_NO_ERROR)
+        name = alcGetString(device, ALC_DEVICE_SPECIFIER);
+    printf("Opened \"%s\"\n", name);
+
+    return 0;
+}
+
+/* CloseAL closes the device belonging to the current context, and destroys the
+ * context. */
+void CloseAL(void)
+{
+    ALCdevice *device;
+    ALCcontext *ctx;
+
+    ctx = alcGetCurrentContext();
+    if(ctx == NULL)
+        return;
+
+    device = alcGetContextsDevice(ctx);
+
+    alcMakeContextCurrent(NULL);
+    alcDestroyContext(ctx);
+    alcCloseDevice(device);
+}
+
+
+const char *FormatName(ALenum format)
+{
+    switch(format)
+    {
+    case AL_FORMAT_MONO8: return "Mono, U8";
+    case AL_FORMAT_MONO16: return "Mono, S16";
+    case AL_FORMAT_MONO_FLOAT32: return "Mono, Float32";
+    case AL_FORMAT_MONO_MULAW: return "Mono, muLaw";
+    case AL_FORMAT_MONO_ALAW_EXT: return "Mono, aLaw";
+    case AL_FORMAT_MONO_IMA4: return "Mono, IMA4 ADPCM";
+    case AL_FORMAT_MONO_MSADPCM_SOFT: return "Mono, MS ADPCM";
+    case AL_FORMAT_STEREO8: return "Stereo, U8";
+    case AL_FORMAT_STEREO16: return "Stereo, S16";
+    case AL_FORMAT_STEREO_FLOAT32: return "Stereo, Float32";
+    case AL_FORMAT_STEREO_MULAW: return "Stereo, muLaw";
+    case AL_FORMAT_STEREO_ALAW_EXT: return "Stereo, aLaw";
+    case AL_FORMAT_STEREO_IMA4: return "Stereo, IMA4 ADPCM";
+    case AL_FORMAT_STEREO_MSADPCM_SOFT: return "Stereo, MS ADPCM";
+    case AL_FORMAT_QUAD8: return "Quadraphonic, U8";
+    case AL_FORMAT_QUAD16: return "Quadraphonic, S16";
+    case AL_FORMAT_QUAD32: return "Quadraphonic, Float32";
+    case AL_FORMAT_QUAD_MULAW: return "Quadraphonic, muLaw";
+    case AL_FORMAT_51CHN8: return "5.1 Surround, U8";
+    case AL_FORMAT_51CHN16: return "5.1 Surround, S16";
+    case AL_FORMAT_51CHN32: return "5.1 Surround, Float32";
+    case AL_FORMAT_51CHN_MULAW: return "5.1 Surround, muLaw";
+    case AL_FORMAT_61CHN8: return "6.1 Surround, U8";
+    case AL_FORMAT_61CHN16: return "6.1 Surround, S16";
+    case AL_FORMAT_61CHN32: return "6.1 Surround, Float32";
+    case AL_FORMAT_61CHN_MULAW: return "6.1 Surround, muLaw";
+    case AL_FORMAT_71CHN8: return "7.1 Surround, U8";
+    case AL_FORMAT_71CHN16: return "7.1 Surround, S16";
+    case AL_FORMAT_71CHN32: return "7.1 Surround, Float32";
+    case AL_FORMAT_71CHN_MULAW: return "7.1 Surround, muLaw";
+    case AL_FORMAT_BFORMAT2D_8: return "B-Format 2D, U8";
+    case AL_FORMAT_BFORMAT2D_16: return "B-Format 2D, S16";
+    case AL_FORMAT_BFORMAT2D_FLOAT32: return "B-Format 2D, Float32";
+    case AL_FORMAT_BFORMAT2D_MULAW: return "B-Format 2D, muLaw";
+    case AL_FORMAT_BFORMAT3D_8: return "B-Format 3D, U8";
+    case AL_FORMAT_BFORMAT3D_16: return "B-Format 3D, S16";
+    case AL_FORMAT_BFORMAT3D_FLOAT32: return "B-Format 3D, Float32";
+    case AL_FORMAT_BFORMAT3D_MULAW: return "B-Format 3D, muLaw";
+    case AL_FORMAT_UHJ2CHN8_SOFT: return "UHJ 2-channel, U8";
+    case AL_FORMAT_UHJ2CHN16_SOFT: return "UHJ 2-channel, S16";
+    case AL_FORMAT_UHJ2CHN_FLOAT32_SOFT: return "UHJ 2-channel, Float32";
+    case AL_FORMAT_UHJ3CHN8_SOFT: return "UHJ 3-channel, U8";
+    case AL_FORMAT_UHJ3CHN16_SOFT: return "UHJ 3-channel, S16";
+    case AL_FORMAT_UHJ3CHN_FLOAT32_SOFT: return "UHJ 3-channel, Float32";
+    case AL_FORMAT_UHJ4CHN8_SOFT: return "UHJ 4-channel, U8";
+    case AL_FORMAT_UHJ4CHN16_SOFT: return "UHJ 4-channel, S16";
+    case AL_FORMAT_UHJ4CHN_FLOAT32_SOFT: return "UHJ 4-channel, Float32";
+    }
+    return "Unknown Format";
+}
+
+
+#ifdef _WIN32
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <mmsystem.h>
+
+int altime_get(void)
+{
+    static int start_time = 0;
+    int cur_time;
+    union {
+        FILETIME ftime;
+        ULARGE_INTEGER ulint;
+    } systime;
+    GetSystemTimeAsFileTime(&systime.ftime);
+    /* FILETIME is in 100-nanosecond units, or 1/10th of a microsecond. */
+    cur_time = (int)(systime.ulint.QuadPart/10000);
+
+    if(!start_time)
+        start_time = cur_time;
+    return cur_time - start_time;
+}
+
+void al_nssleep(unsigned long nsec)
+{
+    Sleep(nsec / 1000000);
+}
+
+#else
+
+#include <sys/time.h>
+#include <unistd.h>
+#include <time.h>
+
+int altime_get(void)
+{
+    static int start_time = 0u;
+    int cur_time;
+
+#if _POSIX_TIMERS > 0
+    struct timespec ts;
+    int ret = clock_gettime(CLOCK_REALTIME, &ts);
+    if(ret != 0) return 0;
+    cur_time = (int)(ts.tv_sec*1000 + ts.tv_nsec/1000000);
+#else /* _POSIX_TIMERS > 0 */
+    struct timeval tv;
+    int ret = gettimeofday(&tv, NULL);
+    if(ret != 0) return 0;
+    cur_time = (int)(tv.tv_sec*1000 + tv.tv_usec/1000);
+#endif
+
+    if(!start_time)
+        start_time = cur_time;
+    return cur_time - start_time;
+}
+
+void al_nssleep(unsigned long nsec)
+{
+    struct timespec ts, rem;
+    ts.tv_sec = (time_t)(nsec / 1000000000ul);
+    ts.tv_nsec = (long)(nsec % 1000000000ul);
+    while(nanosleep(&ts, &rem) == -1 && errno == EINTR)
+        ts = rem;
+}
+
+#endif
diff --git a/examples/common/alhelpers.h b/examples/common/alhelpers.h
new file mode 100644 (file)
index 0000000..34f7386
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef ALHELPERS_H
+#define ALHELPERS_H
+
+#include "AL/al.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Some helper functions to get the name from the format enums. */
+const char *FormatName(ALenum type);
+
+/* Easy device init/deinit functions. InitAL returns 0 on success. */
+int InitAL(char ***argv, int *argc);
+void CloseAL(void);
+
+/* Cross-platform timeget and sleep functions. */
+int altime_get(void);
+void al_nssleep(unsigned long nsec);
+
+/* C doesn't allow casting between function and non-function pointer types, so
+ * with C99 we need to use a union to reinterpret the pointer type. Pre-C99
+ * still needs to use a normal cast and live with the warning (C++ is fine with
+ * a regular reinterpret_cast).
+ */
+#if __STDC_VERSION__ >= 199901L
+#define FUNCTION_CAST(T, ptr) (union{void *p; T f;}){ptr}.f
+#elif defined(__cplusplus)
+#define FUNCTION_CAST(T, ptr) reinterpret_cast<T>(ptr)
+#else
+#define FUNCTION_CAST(T, ptr) (T)(ptr)
+#endif
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif /* ALHELPERS_H */
diff --git a/hrtf/Default HRTF.mhr b/hrtf/Default HRTF.mhr
new file mode 100644 (file)
index 0000000..4955167
Binary files /dev/null and b/hrtf/Default HRTF.mhr differ
diff --git a/include/AL/al.h b/include/AL/al.h
new file mode 100644 (file)
index 0000000..5071fa5
--- /dev/null
@@ -0,0 +1,674 @@
+#ifndef AL_AL_H
+#define AL_AL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef AL_API
+ #if defined(AL_LIBTYPE_STATIC)
+  #define AL_API
+ #elif defined(_WIN32)
+  #define AL_API __declspec(dllimport)
+ #else
+  #define AL_API extern
+ #endif
+#endif
+
+#ifdef _WIN32
+ #define AL_APIENTRY __cdecl
+#else
+ #define AL_APIENTRY
+#endif
+
+
+/* Deprecated macros. */
+#define OPENAL
+#define ALAPI                                    AL_API
+#define ALAPIENTRY                               AL_APIENTRY
+#define AL_INVALID                               (-1)
+#define AL_ILLEGAL_ENUM                          AL_INVALID_ENUM
+#define AL_ILLEGAL_COMMAND                       AL_INVALID_OPERATION
+
+/* Supported AL versions. */
+#define AL_VERSION_1_0
+#define AL_VERSION_1_1
+
+/** 8-bit boolean */
+typedef char ALboolean;
+
+/** character */
+typedef char ALchar;
+
+/** signed 8-bit integer */
+typedef signed char ALbyte;
+
+/** unsigned 8-bit integer */
+typedef unsigned char ALubyte;
+
+/** signed 16-bit integer */
+typedef short ALshort;
+
+/** unsigned 16-bit integer */
+typedef unsigned short ALushort;
+
+/** signed 32-bit integer */
+typedef int ALint;
+
+/** unsigned 32-bit integer */
+typedef unsigned int ALuint;
+
+/** non-negative 32-bit integer size */
+typedef int ALsizei;
+
+/** 32-bit enumeration value */
+typedef int ALenum;
+
+/** 32-bit IEEE-754 floating-point */
+typedef float ALfloat;
+
+/** 64-bit IEEE-754 floating-point */
+typedef double ALdouble;
+
+/** void type (opaque pointers only) */
+typedef void ALvoid;
+
+
+/* Enumeration values begin at column 50. Do not use tabs. */
+
+/** No distance model or no buffer */
+#define AL_NONE                                  0
+
+/** Boolean False. */
+#define AL_FALSE                                 0
+
+/** Boolean True. */
+#define AL_TRUE                                  1
+
+
+/**
+ * Relative source.
+ * Type:    ALboolean
+ * Range:   [AL_FALSE, AL_TRUE]
+ * Default: AL_FALSE
+ *
+ * Specifies if the source uses relative coordinates.
+ */
+#define AL_SOURCE_RELATIVE                       0x202
+
+
+/**
+ * Inner cone angle, in degrees.
+ * Type:    ALint, ALfloat
+ * Range:   [0 - 360]
+ * Default: 360
+ *
+ * The angle covered by the inner cone, the area within which the source will
+ * not be attenuated by direction.
+ */
+#define AL_CONE_INNER_ANGLE                      0x1001
+
+/**
+ * Outer cone angle, in degrees.
+ * Range:   [0 - 360]
+ * Default: 360
+ *
+ * The angle covered by the outer cone, the area outside of which the source
+ * will be fully attenuated by direction.
+ */
+#define AL_CONE_OUTER_ANGLE                      0x1002
+
+/**
+ * Source pitch.
+ * Type:    ALfloat
+ * Range:   [0.5 - 2.0]
+ * Default: 1.0
+ *
+ * A multiplier for the sample rate of the source's buffer.
+ */
+#define AL_PITCH                                 0x1003
+
+/**
+ * Source or listener position.
+ * Type:    ALfloat[3], ALint[3]
+ * Default: {0, 0, 0}
+ *
+ * The source or listener location in three dimensional space.
+ *
+ * OpenAL uses a right handed coordinate system, like OpenGL, where with a
+ * default view, X points right (thumb), Y points up (index finger), and Z
+ * points towards the viewer/camera (middle finger).
+ *
+ * To change from or to a left handed coordinate system, negate the Z
+ * component.
+ */
+#define AL_POSITION                              0x1004
+
+/**
+ * Source direction.
+ * Type:    ALfloat[3], ALint[3]
+ * Default: {0, 0, 0}
+ *
+ * Specifies the current direction in local space. A zero-length vector
+ * specifies an omni-directional source (cone is ignored).
+ *
+ * To change from or to a left handed coordinate system, negate the Z
+ * component.
+ */
+#define AL_DIRECTION                             0x1005
+
+/**
+ * Source or listener velocity.
+ * Type:    ALfloat[3], ALint[3]
+ * Default: {0, 0, 0}
+ *
+ * Specifies the current velocity, relative to the position.
+ *
+ * To change from or to a left handed coordinate system, negate the Z
+ * component.
+ */
+#define AL_VELOCITY                              0x1006
+
+/**
+ * Source looping.
+ * Type:    ALboolean
+ * Range:   [AL_FALSE, AL_TRUE]
+ * Default: AL_FALSE
+ *
+ * Specifies whether source playback loops.
+ */
+#define AL_LOOPING                               0x1007
+
+/**
+ * Source buffer.
+ * Type:    ALuint
+ * Range:   any valid Buffer ID
+ * Default: AL_NONE
+ *
+ * Specifies the buffer to provide sound samples for a source.
+ */
+#define AL_BUFFER                                0x1009
+
+/**
+ * Source or listener gain.
+ * Type:  ALfloat
+ * Range: [0.0 - ]
+ *
+ * For sources, an initial linear gain value (before attenuation is applied).
+ * For the listener, an output linear gain adjustment.
+ *
+ * A value of 1.0 means unattenuated. Each division by 2 equals an attenuation
+ * of about -6dB. Each multiplication by 2 equals an amplification of about
+ * +6dB.
+ */
+#define AL_GAIN                                  0x100A
+
+/**
+ * Minimum source gain.
+ * Type:  ALfloat
+ * Range: [0.0 - 1.0]
+ *
+ * The minimum gain allowed for a source, after distance and cone attenuation
+ * are applied (if applicable).
+ */
+#define AL_MIN_GAIN                              0x100D
+
+/**
+ * Maximum source gain.
+ * Type:  ALfloat
+ * Range: [0.0 - 1.0]
+ *
+ * The maximum gain allowed for a source, after distance and cone attenuation
+ * are applied (if applicable).
+ */
+#define AL_MAX_GAIN                              0x100E
+
+/**
+ * Listener orientation.
+ * Type:    ALfloat[6]
+ * Default: {0.0, 0.0, -1.0, 0.0, 1.0, 0.0}
+ *
+ * Effectively two three dimensional vectors. The first vector is the front (or
+ * "at") and the second is the top (or "up"). Both vectors are relative to the
+ * listener position.
+ *
+ * To change from or to a left handed coordinate system, negate the Z
+ * component of both vectors.
+ */
+#define AL_ORIENTATION                           0x100F
+
+/**
+ * Source state (query only).
+ * Type:  ALenum
+ * Range: [AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED]
+ */
+#define AL_SOURCE_STATE                          0x1010
+
+/* Source state values. */
+#define AL_INITIAL                               0x1011
+#define AL_PLAYING                               0x1012
+#define AL_PAUSED                                0x1013
+#define AL_STOPPED                               0x1014
+
+/**
+ * Source Buffer Queue size (query only).
+ * Type: ALint
+ *
+ * The number of buffers queued using alSourceQueueBuffers, minus the buffers
+ * removed with alSourceUnqueueBuffers.
+ */
+#define AL_BUFFERS_QUEUED                        0x1015
+
+/**
+ * Source Buffer Queue processed count (query only).
+ * Type: ALint
+ *
+ * The number of queued buffers that have been fully processed, and can be
+ * removed with alSourceUnqueueBuffers.
+ *
+ * Looping sources will never fully process buffers because they will be set to
+ * play again for when the source loops.
+ */
+#define AL_BUFFERS_PROCESSED                     0x1016
+
+/**
+ * Source reference distance.
+ * Type:    ALfloat
+ * Range:   [0.0 - ]
+ * Default: 1.0
+ *
+ * The distance in units that no distance attenuation occurs.
+ *
+ * At 0.0, no distance attenuation occurs with non-linear attenuation models.
+ */
+#define AL_REFERENCE_DISTANCE                    0x1020
+
+/**
+ * Source rolloff factor.
+ * Type:    ALfloat
+ * Range:   [0.0 - ]
+ * Default: 1.0
+ *
+ * Multiplier to exaggerate or diminish distance attenuation.
+ *
+ * At 0.0, no distance attenuation ever occurs.
+ */
+#define AL_ROLLOFF_FACTOR                        0x1021
+
+/**
+ * Outer cone gain.
+ * Type:    ALfloat
+ * Range:   [0.0 - 1.0]
+ * Default: 0.0
+ *
+ * The gain attenuation applied when the listener is outside of the source's
+ * outer cone angle.
+ */
+#define AL_CONE_OUTER_GAIN                       0x1022
+
+/**
+ * Source maximum distance.
+ * Type:    ALfloat
+ * Range:   [0.0 - ]
+ * Default: FLT_MAX
+ *
+ * The distance above which the source is not attenuated any further with a
+ * clamped distance model, or where attenuation reaches 0.0 gain for linear
+ * distance models with a default rolloff factor.
+ */
+#define AL_MAX_DISTANCE                          0x1023
+
+/** Source buffer offset, in seconds */
+#define AL_SEC_OFFSET                            0x1024
+/** Source buffer offset, in sample frames */
+#define AL_SAMPLE_OFFSET                         0x1025
+/** Source buffer offset, in bytes */
+#define AL_BYTE_OFFSET                           0x1026
+
+/**
+ * Source type (query only).
+ * Type:  ALenum
+ * Range: [AL_STATIC, AL_STREAMING, AL_UNDETERMINED]
+ *
+ * A Source is Static if a Buffer has been attached using AL_BUFFER.
+ *
+ * A Source is Streaming if one or more Buffers have been attached using
+ * alSourceQueueBuffers.
+ *
+ * A Source is Undetermined when it has the NULL buffer attached using
+ * AL_BUFFER.
+ */
+#define AL_SOURCE_TYPE                           0x1027
+
+/* Source type values. */
+#define AL_STATIC                                0x1028
+#define AL_STREAMING                             0x1029
+#define AL_UNDETERMINED                          0x1030
+
+/** Unsigned 8-bit mono buffer format. */
+#define AL_FORMAT_MONO8                          0x1100
+/** Signed 16-bit mono buffer format. */
+#define AL_FORMAT_MONO16                         0x1101
+/** Unsigned 8-bit stereo buffer format. */
+#define AL_FORMAT_STEREO8                        0x1102
+/** Signed 16-bit stereo buffer format. */
+#define AL_FORMAT_STEREO16                       0x1103
+
+/** Buffer frequency/sample rate (query only). */
+#define AL_FREQUENCY                             0x2001
+/** Buffer bits per sample (query only). */
+#define AL_BITS                                  0x2002
+/** Buffer channel count (query only). */
+#define AL_CHANNELS                              0x2003
+/** Buffer data size in bytes (query only). */
+#define AL_SIZE                                  0x2004
+
+/* Buffer state. Not for public use. */
+#define AL_UNUSED                                0x2010
+#define AL_PENDING                               0x2011
+#define AL_PROCESSED                             0x2012
+
+
+/** No error. */
+#define AL_NO_ERROR                              0
+
+/** Invalid name (ID) passed to an AL call. */
+#define AL_INVALID_NAME                          0xA001
+
+/** Invalid enumeration passed to AL call. */
+#define AL_INVALID_ENUM                          0xA002
+
+/** Invalid value passed to AL call. */
+#define AL_INVALID_VALUE                         0xA003
+
+/** Illegal AL call. */
+#define AL_INVALID_OPERATION                     0xA004
+
+/** Not enough memory to execute the AL call. */
+#define AL_OUT_OF_MEMORY                         0xA005
+
+
+/** Context string: Vendor name. */
+#define AL_VENDOR                                0xB001
+/** Context string: Version. */
+#define AL_VERSION                               0xB002
+/** Context string: Renderer name. */
+#define AL_RENDERER                              0xB003
+/** Context string: Space-separated extension list. */
+#define AL_EXTENSIONS                            0xB004
+
+/**
+ * Doppler scale.
+ * Type:    ALfloat
+ * Range:   [0.0 - ]
+ * Default: 1.0
+ *
+ * Scale for source and listener velocities.
+ */
+#define AL_DOPPLER_FACTOR                        0xC000
+
+/**
+ * Doppler velocity (deprecated).
+ *
+ * A multiplier applied to the Speed of Sound.
+ */
+#define AL_DOPPLER_VELOCITY                      0xC001
+
+/**
+ * Speed of Sound, in units per second.
+ * Type:    ALfloat
+ * Range:   [0.0001 - ]
+ * Default: 343.3
+ *
+ * The speed at which sound waves are assumed to travel, when calculating the
+ * doppler effect from source and listener velocities.
+ */
+#define AL_SPEED_OF_SOUND                        0xC003
+
+/**
+ * Distance attenuation model.
+ * Type:    ALenum
+ * Range:   [AL_NONE, AL_INVERSE_DISTANCE, AL_INVERSE_DISTANCE_CLAMPED,
+ *           AL_LINEAR_DISTANCE, AL_LINEAR_DISTANCE_CLAMPED,
+ *           AL_EXPONENT_DISTANCE, AL_EXPONENT_DISTANCE_CLAMPED]
+ * Default: AL_INVERSE_DISTANCE_CLAMPED
+ *
+ * The model by which sources attenuate with distance.
+ *
+ * None     - No distance attenuation.
+ * Inverse  - Doubling the distance halves the source gain.
+ * Linear   - Linear gain scaling between the reference and max distances.
+ * Exponent - Exponential gain dropoff.
+ *
+ * Clamped variations work like the non-clamped counterparts, except the
+ * distance calculated is clamped between the reference and max distances.
+ */
+#define AL_DISTANCE_MODEL                        0xD000
+
+/* Distance model values. */
+#define AL_INVERSE_DISTANCE                      0xD001
+#define AL_INVERSE_DISTANCE_CLAMPED              0xD002
+#define AL_LINEAR_DISTANCE                       0xD003
+#define AL_LINEAR_DISTANCE_CLAMPED               0xD004
+#define AL_EXPONENT_DISTANCE                     0xD005
+#define AL_EXPONENT_DISTANCE_CLAMPED             0xD006
+
+#ifndef AL_NO_PROTOTYPES
+/* Renderer State management. */
+AL_API void AL_APIENTRY alEnable(ALenum capability);
+AL_API void AL_APIENTRY alDisable(ALenum capability);
+AL_API ALboolean AL_APIENTRY alIsEnabled(ALenum capability);
+
+/* Context state setting. */
+AL_API void AL_APIENTRY alDopplerFactor(ALfloat value);
+AL_API void AL_APIENTRY alDopplerVelocity(ALfloat value);
+AL_API void AL_APIENTRY alSpeedOfSound(ALfloat value);
+AL_API void AL_APIENTRY alDistanceModel(ALenum distanceModel);
+
+/* Context state retrieval. */
+AL_API const ALchar* AL_APIENTRY alGetString(ALenum param);
+AL_API void AL_APIENTRY alGetBooleanv(ALenum param, ALboolean *values);
+AL_API void AL_APIENTRY alGetIntegerv(ALenum param, ALint *values);
+AL_API void AL_APIENTRY alGetFloatv(ALenum param, ALfloat *values);
+AL_API void AL_APIENTRY alGetDoublev(ALenum param, ALdouble *values);
+AL_API ALboolean AL_APIENTRY alGetBoolean(ALenum param);
+AL_API ALint AL_APIENTRY alGetInteger(ALenum param);
+AL_API ALfloat AL_APIENTRY alGetFloat(ALenum param);
+AL_API ALdouble AL_APIENTRY alGetDouble(ALenum param);
+
+/**
+ * Obtain the first error generated in the AL context since the last call to
+ * this function.
+ */
+AL_API ALenum AL_APIENTRY alGetError(void);
+
+/** Query for the presence of an extension on the AL context. */
+AL_API ALboolean AL_APIENTRY alIsExtensionPresent(const ALchar *extname);
+/**
+ * Retrieve the address of a function. The returned function may be context-
+ * specific.
+ */
+AL_API void* AL_APIENTRY alGetProcAddress(const ALchar *fname);
+/**
+ * Retrieve the value of an enum. The returned value may be context-specific.
+ */
+AL_API ALenum AL_APIENTRY alGetEnumValue(const ALchar *ename);
+
+
+/* Set listener parameters. */
+AL_API void AL_APIENTRY alListenerf(ALenum param, ALfloat value);
+AL_API void AL_APIENTRY alListener3f(ALenum param, ALfloat value1, ALfloat value2, ALfloat value3);
+AL_API void AL_APIENTRY alListenerfv(ALenum param, const ALfloat *values);
+AL_API void AL_APIENTRY alListeneri(ALenum param, ALint value);
+AL_API void AL_APIENTRY alListener3i(ALenum param, ALint value1, ALint value2, ALint value3);
+AL_API void AL_APIENTRY alListeneriv(ALenum param, const ALint *values);
+
+/* Get listener parameters. */
+AL_API void AL_APIENTRY alGetListenerf(ALenum param, ALfloat *value);
+AL_API void AL_APIENTRY alGetListener3f(ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3);
+AL_API void AL_APIENTRY alGetListenerfv(ALenum param, ALfloat *values);
+AL_API void AL_APIENTRY alGetListeneri(ALenum param, ALint *value);
+AL_API void AL_APIENTRY alGetListener3i(ALenum param, ALint *value1, ALint *value2, ALint *value3);
+AL_API void AL_APIENTRY alGetListeneriv(ALenum param, ALint *values);
+
+
+/** Create source objects. */
+AL_API void AL_APIENTRY alGenSources(ALsizei n, ALuint *sources);
+/** Delete source objects. */
+AL_API void AL_APIENTRY alDeleteSources(ALsizei n, const ALuint *sources);
+/** Verify an ID is for a valid source. */
+AL_API ALboolean AL_APIENTRY alIsSource(ALuint source);
+
+/* Set source parameters. */
+AL_API void AL_APIENTRY alSourcef(ALuint source, ALenum param, ALfloat value);
+AL_API void AL_APIENTRY alSource3f(ALuint source, ALenum param, ALfloat value1, ALfloat value2, ALfloat value3);
+AL_API void AL_APIENTRY alSourcefv(ALuint source, ALenum param, const ALfloat *values);
+AL_API void AL_APIENTRY alSourcei(ALuint source, ALenum param, ALint value);
+AL_API void AL_APIENTRY alSource3i(ALuint source, ALenum param, ALint value1, ALint value2, ALint value3);
+AL_API void AL_APIENTRY alSourceiv(ALuint source, ALenum param, const ALint *values);
+
+/* Get source parameters. */
+AL_API void AL_APIENTRY alGetSourcef(ALuint source, ALenum param, ALfloat *value);
+AL_API void AL_APIENTRY alGetSource3f(ALuint source, ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3);
+AL_API void AL_APIENTRY alGetSourcefv(ALuint source, ALenum param, ALfloat *values);
+AL_API void AL_APIENTRY alGetSourcei(ALuint source,  ALenum param, ALint *value);
+AL_API void AL_APIENTRY alGetSource3i(ALuint source, ALenum param, ALint *value1, ALint *value2, ALint *value3);
+AL_API void AL_APIENTRY alGetSourceiv(ALuint source,  ALenum param, ALint *values);
+
+
+/** Play, restart, or resume a source, setting its state to AL_PLAYING. */
+AL_API void AL_APIENTRY alSourcePlay(ALuint source);
+/** Stop a source, setting its state to AL_STOPPED if playing or paused. */
+AL_API void AL_APIENTRY alSourceStop(ALuint source);
+/** Rewind a source, setting its state to AL_INITIAL. */
+AL_API void AL_APIENTRY alSourceRewind(ALuint source);
+/** Pause a source, setting its state to AL_PAUSED if playing. */
+AL_API void AL_APIENTRY alSourcePause(ALuint source);
+
+/** Play, restart, or resume a list of sources atomically. */
+AL_API void AL_APIENTRY alSourcePlayv(ALsizei n, const ALuint *sources);
+/** Stop a list of sources atomically. */
+AL_API void AL_APIENTRY alSourceStopv(ALsizei n, const ALuint *sources);
+/** Rewind a list of sources atomically. */
+AL_API void AL_APIENTRY alSourceRewindv(ALsizei n, const ALuint *sources);
+/** Pause a list of sources atomically. */
+AL_API void AL_APIENTRY alSourcePausev(ALsizei n, const ALuint *sources);
+
+/** Queue buffers onto a source */
+AL_API void AL_APIENTRY alSourceQueueBuffers(ALuint source, ALsizei nb, const ALuint *buffers);
+/** Unqueue processed buffers from a source */
+AL_API void AL_APIENTRY alSourceUnqueueBuffers(ALuint source, ALsizei nb, ALuint *buffers);
+
+
+/** Create buffer objects */
+AL_API void AL_APIENTRY alGenBuffers(ALsizei n, ALuint *buffers);
+/** Delete buffer objects */
+AL_API void AL_APIENTRY alDeleteBuffers(ALsizei n, const ALuint *buffers);
+/** Verify an ID is a valid buffer (including the NULL buffer) */
+AL_API ALboolean AL_APIENTRY alIsBuffer(ALuint buffer);
+
+/**
+ * Copies data into the buffer, interpreting it using the specified format and
+ * samplerate.
+ */
+AL_API void AL_APIENTRY alBufferData(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei samplerate);
+
+/* Set buffer parameters. */
+AL_API void AL_APIENTRY alBufferf(ALuint buffer, ALenum param, ALfloat value);
+AL_API void AL_APIENTRY alBuffer3f(ALuint buffer, ALenum param, ALfloat value1, ALfloat value2, ALfloat value3);
+AL_API void AL_APIENTRY alBufferfv(ALuint buffer, ALenum param, const ALfloat *values);
+AL_API void AL_APIENTRY alBufferi(ALuint buffer, ALenum param, ALint value);
+AL_API void AL_APIENTRY alBuffer3i(ALuint buffer, ALenum param, ALint value1, ALint value2, ALint value3);
+AL_API void AL_APIENTRY alBufferiv(ALuint buffer, ALenum param, const ALint *values);
+
+/* Get buffer parameters. */
+AL_API void AL_APIENTRY alGetBufferf(ALuint buffer, ALenum param, ALfloat *value);
+AL_API void AL_APIENTRY alGetBuffer3f(ALuint buffer, ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3);
+AL_API void AL_APIENTRY alGetBufferfv(ALuint buffer, ALenum param, ALfloat *values);
+AL_API void AL_APIENTRY alGetBufferi(ALuint buffer, ALenum param, ALint *value);
+AL_API void AL_APIENTRY alGetBuffer3i(ALuint buffer, ALenum param, ALint *value1, ALint *value2, ALint *value3);
+AL_API void AL_APIENTRY alGetBufferiv(ALuint buffer, ALenum param, ALint *values);
+#endif /* AL_NO_PROTOTYPES */
+
+/* Pointer-to-function types, useful for storing dynamically loaded AL entry
+ * points.
+ */
+typedef void          (AL_APIENTRY *LPALENABLE)(ALenum capability);
+typedef void          (AL_APIENTRY *LPALDISABLE)(ALenum capability);
+typedef ALboolean     (AL_APIENTRY *LPALISENABLED)(ALenum capability);
+typedef const ALchar* (AL_APIENTRY *LPALGETSTRING)(ALenum param);
+typedef void          (AL_APIENTRY *LPALGETBOOLEANV)(ALenum param, ALboolean *values);
+typedef void          (AL_APIENTRY *LPALGETINTEGERV)(ALenum param, ALint *values);
+typedef void          (AL_APIENTRY *LPALGETFLOATV)(ALenum param, ALfloat *values);
+typedef void          (AL_APIENTRY *LPALGETDOUBLEV)(ALenum param, ALdouble *values);
+typedef ALboolean     (AL_APIENTRY *LPALGETBOOLEAN)(ALenum param);
+typedef ALint         (AL_APIENTRY *LPALGETINTEGER)(ALenum param);
+typedef ALfloat       (AL_APIENTRY *LPALGETFLOAT)(ALenum param);
+typedef ALdouble      (AL_APIENTRY *LPALGETDOUBLE)(ALenum param);
+typedef ALenum        (AL_APIENTRY *LPALGETERROR)(void);
+typedef ALboolean     (AL_APIENTRY *LPALISEXTENSIONPRESENT)(const ALchar *extname);
+typedef void*         (AL_APIENTRY *LPALGETPROCADDRESS)(const ALchar *fname);
+typedef ALenum        (AL_APIENTRY *LPALGETENUMVALUE)(const ALchar *ename);
+typedef void          (AL_APIENTRY *LPALLISTENERF)(ALenum param, ALfloat value);
+typedef void          (AL_APIENTRY *LPALLISTENER3F)(ALenum param, ALfloat value1, ALfloat value2, ALfloat value3);
+typedef void          (AL_APIENTRY *LPALLISTENERFV)(ALenum param, const ALfloat *values);
+typedef void          (AL_APIENTRY *LPALLISTENERI)(ALenum param, ALint value);
+typedef void          (AL_APIENTRY *LPALLISTENER3I)(ALenum param, ALint value1, ALint value2, ALint value3);
+typedef void          (AL_APIENTRY *LPALLISTENERIV)(ALenum param, const ALint *values);
+typedef void          (AL_APIENTRY *LPALGETLISTENERF)(ALenum param, ALfloat *value);
+typedef void          (AL_APIENTRY *LPALGETLISTENER3F)(ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3);
+typedef void          (AL_APIENTRY *LPALGETLISTENERFV)(ALenum param, ALfloat *values);
+typedef void          (AL_APIENTRY *LPALGETLISTENERI)(ALenum param, ALint *value);
+typedef void          (AL_APIENTRY *LPALGETLISTENER3I)(ALenum param, ALint *value1, ALint *value2, ALint *value3);
+typedef void          (AL_APIENTRY *LPALGETLISTENERIV)(ALenum param, ALint *values);
+typedef void          (AL_APIENTRY *LPALGENSOURCES)(ALsizei n, ALuint *sources);
+typedef void          (AL_APIENTRY *LPALDELETESOURCES)(ALsizei n, const ALuint *sources);
+typedef ALboolean     (AL_APIENTRY *LPALISSOURCE)(ALuint source);
+typedef void          (AL_APIENTRY *LPALSOURCEF)(ALuint source, ALenum param, ALfloat value);
+typedef void          (AL_APIENTRY *LPALSOURCE3F)(ALuint source, ALenum param, ALfloat value1, ALfloat value2, ALfloat value3);
+typedef void          (AL_APIENTRY *LPALSOURCEFV)(ALuint source, ALenum param, const ALfloat *values);
+typedef void          (AL_APIENTRY *LPALSOURCEI)(ALuint source, ALenum param, ALint value);
+typedef void          (AL_APIENTRY *LPALSOURCE3I)(ALuint source, ALenum param, ALint value1, ALint value2, ALint value3);
+typedef void          (AL_APIENTRY *LPALSOURCEIV)(ALuint source, ALenum param, const ALint *values);
+typedef void          (AL_APIENTRY *LPALGETSOURCEF)(ALuint source, ALenum param, ALfloat *value);
+typedef void          (AL_APIENTRY *LPALGETSOURCE3F)(ALuint source, ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3);
+typedef void          (AL_APIENTRY *LPALGETSOURCEFV)(ALuint source, ALenum param, ALfloat *values);
+typedef void          (AL_APIENTRY *LPALGETSOURCEI)(ALuint source, ALenum param, ALint *value);
+typedef void          (AL_APIENTRY *LPALGETSOURCE3I)(ALuint source, ALenum param, ALint *value1, ALint *value2, ALint *value3);
+typedef void          (AL_APIENTRY *LPALGETSOURCEIV)(ALuint source, ALenum param, ALint *values);
+typedef void          (AL_APIENTRY *LPALSOURCEPLAYV)(ALsizei n, const ALuint *sources);
+typedef void          (AL_APIENTRY *LPALSOURCESTOPV)(ALsizei n, const ALuint *sources);
+typedef void          (AL_APIENTRY *LPALSOURCEREWINDV)(ALsizei n, const ALuint *sources);
+typedef void          (AL_APIENTRY *LPALSOURCEPAUSEV)(ALsizei n, const ALuint *sources);
+typedef void          (AL_APIENTRY *LPALSOURCEPLAY)(ALuint source);
+typedef void          (AL_APIENTRY *LPALSOURCESTOP)(ALuint source);
+typedef void          (AL_APIENTRY *LPALSOURCEREWIND)(ALuint source);
+typedef void          (AL_APIENTRY *LPALSOURCEPAUSE)(ALuint source);
+typedef void          (AL_APIENTRY *LPALSOURCEQUEUEBUFFERS)(ALuint source, ALsizei nb, const ALuint *buffers);
+typedef void          (AL_APIENTRY *LPALSOURCEUNQUEUEBUFFERS)(ALuint source, ALsizei nb, ALuint *buffers);
+typedef void          (AL_APIENTRY *LPALGENBUFFERS)(ALsizei n, ALuint *buffers);
+typedef void          (AL_APIENTRY *LPALDELETEBUFFERS)(ALsizei n, const ALuint *buffers);
+typedef ALboolean     (AL_APIENTRY *LPALISBUFFER)(ALuint buffer);
+typedef void          (AL_APIENTRY *LPALBUFFERDATA)(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei samplerate);
+typedef void          (AL_APIENTRY *LPALBUFFERF)(ALuint buffer, ALenum param, ALfloat value);
+typedef void          (AL_APIENTRY *LPALBUFFER3F)(ALuint buffer, ALenum param, ALfloat value1, ALfloat value2, ALfloat value3);
+typedef void          (AL_APIENTRY *LPALBUFFERFV)(ALuint buffer, ALenum param, const ALfloat *values);
+typedef void          (AL_APIENTRY *LPALBUFFERI)(ALuint buffer, ALenum param, ALint value);
+typedef void          (AL_APIENTRY *LPALBUFFER3I)(ALuint buffer, ALenum param, ALint value1, ALint value2, ALint value3);
+typedef void          (AL_APIENTRY *LPALBUFFERIV)(ALuint buffer, ALenum param, const ALint *values);
+typedef void          (AL_APIENTRY *LPALGETBUFFERF)(ALuint buffer, ALenum param, ALfloat *value);
+typedef void          (AL_APIENTRY *LPALGETBUFFER3F)(ALuint buffer, ALenum param, ALfloat *value1, ALfloat *value2, ALfloat *value3);
+typedef void          (AL_APIENTRY *LPALGETBUFFERFV)(ALuint buffer, ALenum param, ALfloat *values);
+typedef void          (AL_APIENTRY *LPALGETBUFFERI)(ALuint buffer, ALenum param, ALint *value);
+typedef void          (AL_APIENTRY *LPALGETBUFFER3I)(ALuint buffer, ALenum param, ALint *value1, ALint *value2, ALint *value3);
+typedef void          (AL_APIENTRY *LPALGETBUFFERIV)(ALuint buffer, ALenum param, ALint *values);
+typedef void          (AL_APIENTRY *LPALDOPPLERFACTOR)(ALfloat value);
+typedef void          (AL_APIENTRY *LPALDOPPLERVELOCITY)(ALfloat value);
+typedef void          (AL_APIENTRY *LPALSPEEDOFSOUND)(ALfloat value);
+typedef void          (AL_APIENTRY *LPALDISTANCEMODEL)(ALenum distanceModel);
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif /* AL_AL_H */
diff --git a/include/AL/alc.h b/include/AL/alc.h
new file mode 100644 (file)
index 0000000..6d21033
--- /dev/null
@@ -0,0 +1,274 @@
+#ifndef AL_ALC_H
+#define AL_ALC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef ALC_API
+ #if defined(AL_LIBTYPE_STATIC)
+  #define ALC_API
+ #elif defined(_WIN32)
+  #define ALC_API __declspec(dllimport)
+ #else
+  #define ALC_API extern
+ #endif
+#endif
+
+#ifdef _WIN32
+ #define ALC_APIENTRY __cdecl
+#else
+ #define ALC_APIENTRY
+#endif
+
+
+/* Deprecated macros. */
+#define ALCAPI                                   ALC_API
+#define ALCAPIENTRY                              ALC_APIENTRY
+#define ALC_INVALID                              0
+
+/** Supported ALC version? */
+#define ALC_VERSION_0_1                          1
+
+/** Opaque device handle */
+typedef struct ALCdevice ALCdevice;
+/** Opaque context handle */
+typedef struct ALCcontext ALCcontext;
+
+/** 8-bit boolean */
+typedef char ALCboolean;
+
+/** character */
+typedef char ALCchar;
+
+/** signed 8-bit integer */
+typedef signed char ALCbyte;
+
+/** unsigned 8-bit integer */
+typedef unsigned char ALCubyte;
+
+/** signed 16-bit integer */
+typedef short ALCshort;
+
+/** unsigned 16-bit integer */
+typedef unsigned short ALCushort;
+
+/** signed 32-bit integer */
+typedef int ALCint;
+
+/** unsigned 32-bit integer */
+typedef unsigned int ALCuint;
+
+/** non-negative 32-bit integer size */
+typedef int ALCsizei;
+
+/** 32-bit enumeration value */
+typedef int ALCenum;
+
+/** 32-bit IEEE-754 floating-point */
+typedef float ALCfloat;
+
+/** 64-bit IEEE-754 floating-point */
+typedef double ALCdouble;
+
+/** void type (for opaque pointers only) */
+typedef void ALCvoid;
+
+
+/* Enumeration values begin at column 50. Do not use tabs. */
+
+/** Boolean False. */
+#define ALC_FALSE                                0
+
+/** Boolean True. */
+#define ALC_TRUE                                 1
+
+/** Context attribute: <int> Hz. */
+#define ALC_FREQUENCY                            0x1007
+
+/** Context attribute: <int> Hz. */
+#define ALC_REFRESH                              0x1008
+
+/** Context attribute: AL_TRUE or AL_FALSE synchronous context? */
+#define ALC_SYNC                                 0x1009
+
+/** Context attribute: <int> requested Mono (3D) Sources. */
+#define ALC_MONO_SOURCES                         0x1010
+
+/** Context attribute: <int> requested Stereo Sources. */
+#define ALC_STEREO_SOURCES                       0x1011
+
+/** No error. */
+#define ALC_NO_ERROR                             0
+
+/** Invalid device handle. */
+#define ALC_INVALID_DEVICE                       0xA001
+
+/** Invalid context handle. */
+#define ALC_INVALID_CONTEXT                      0xA002
+
+/** Invalid enumeration passed to an ALC call. */
+#define ALC_INVALID_ENUM                         0xA003
+
+/** Invalid value passed to an ALC call. */
+#define ALC_INVALID_VALUE                        0xA004
+
+/** Out of memory. */
+#define ALC_OUT_OF_MEMORY                        0xA005
+
+
+/** Runtime ALC major version. */
+#define ALC_MAJOR_VERSION                        0x1000
+/** Runtime ALC minor version. */
+#define ALC_MINOR_VERSION                        0x1001
+
+/** Context attribute list size. */
+#define ALC_ATTRIBUTES_SIZE                      0x1002
+/** Context attribute list properties. */
+#define ALC_ALL_ATTRIBUTES                       0x1003
+
+/** String for the default device specifier. */
+#define ALC_DEFAULT_DEVICE_SPECIFIER             0x1004
+/**
+ * Device specifier string.
+ *
+ * If device handle is NULL, it is instead a null-character separated list of
+ * strings of known device specifiers (list ends with an empty string).
+ */
+#define ALC_DEVICE_SPECIFIER                     0x1005
+/** String for space-separated list of ALC extensions. */
+#define ALC_EXTENSIONS                           0x1006
+
+
+/** Capture extension */
+#define ALC_EXT_CAPTURE 1
+/**
+ * Capture device specifier string.
+ *
+ * If device handle is NULL, it is instead a null-character separated list of
+ * strings of known capture device specifiers (list ends with an empty string).
+ */
+#define ALC_CAPTURE_DEVICE_SPECIFIER             0x310
+/** String for the default capture device specifier. */
+#define ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER     0x311
+/** Number of sample frames available for capture. */
+#define ALC_CAPTURE_SAMPLES                      0x312
+
+
+/** Enumerate All extension */
+#define ALC_ENUMERATE_ALL_EXT 1
+/** String for the default extended device specifier. */
+#define ALC_DEFAULT_ALL_DEVICES_SPECIFIER        0x1012
+/**
+ * Device's extended specifier string.
+ *
+ * If device handle is NULL, it is instead a null-character separated list of
+ * strings of known extended device specifiers (list ends with an empty string).
+ */
+#define ALC_ALL_DEVICES_SPECIFIER                0x1013
+
+
+#ifndef ALC_NO_PROTOTYPES
+/* Context management. */
+
+/** Create and attach a context to the given device. */
+ALC_API ALCcontext* ALC_APIENTRY alcCreateContext(ALCdevice *device, const ALCint *attrlist);
+/**
+ * Makes the given context the active process-wide context. Passing NULL clears
+ * the active context.
+ */
+ALC_API ALCboolean  ALC_APIENTRY alcMakeContextCurrent(ALCcontext *context);
+/** Resumes processing updates for the given context. */
+ALC_API void        ALC_APIENTRY alcProcessContext(ALCcontext *context);
+/** Suspends updates for the given context. */
+ALC_API void        ALC_APIENTRY alcSuspendContext(ALCcontext *context);
+/** Remove a context from its device and destroys it. */
+ALC_API void        ALC_APIENTRY alcDestroyContext(ALCcontext *context);
+/** Returns the currently active context. */
+ALC_API ALCcontext* ALC_APIENTRY alcGetCurrentContext(void);
+/** Returns the device that a particular context is attached to. */
+ALC_API ALCdevice*  ALC_APIENTRY alcGetContextsDevice(ALCcontext *context);
+
+/* Device management. */
+
+/** Opens the named playback device. */
+ALC_API ALCdevice* ALC_APIENTRY alcOpenDevice(const ALCchar *devicename);
+/** Closes the given playback device. */
+ALC_API ALCboolean ALC_APIENTRY alcCloseDevice(ALCdevice *device);
+
+/* Error support. */
+
+/** Obtain the most recent Device error. */
+ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device);
+
+/* Extension support. */
+
+/**
+ * Query for the presence of an extension on the device. Pass a NULL device to
+ * query a device-inspecific extension.
+ */
+ALC_API ALCboolean ALC_APIENTRY alcIsExtensionPresent(ALCdevice *device, const ALCchar *extname);
+/**
+ * Retrieve the address of a function. Given a non-NULL device, the returned
+ * function may be device-specific.
+ */
+ALC_API ALCvoid*   ALC_APIENTRY alcGetProcAddress(ALCdevice *device, const ALCchar *funcname);
+/**
+ * Retrieve the value of an enum. Given a non-NULL device, the returned value
+ * may be device-specific.
+ */
+ALC_API ALCenum    ALC_APIENTRY alcGetEnumValue(ALCdevice *device, const ALCchar *enumname);
+
+/* Query functions. */
+
+/** Returns information about the device, and error strings. */
+ALC_API const ALCchar* ALC_APIENTRY alcGetString(ALCdevice *device, ALCenum param);
+/** Returns information about the device and the version of OpenAL. */
+ALC_API void           ALC_APIENTRY alcGetIntegerv(ALCdevice *device, ALCenum param, ALCsizei size, ALCint *values);
+
+/* Capture functions. */
+
+/**
+ * Opens the named capture device with the given frequency, format, and buffer
+ * size.
+ */
+ALC_API ALCdevice* ALC_APIENTRY alcCaptureOpenDevice(const ALCchar *devicename, ALCuint frequency, ALCenum format, ALCsizei buffersize);
+/** Closes the given capture device. */
+ALC_API ALCboolean ALC_APIENTRY alcCaptureCloseDevice(ALCdevice *device);
+/** Starts capturing samples into the device buffer. */
+ALC_API void       ALC_APIENTRY alcCaptureStart(ALCdevice *device);
+/** Stops capturing samples. Samples in the device buffer remain available. */
+ALC_API void       ALC_APIENTRY alcCaptureStop(ALCdevice *device);
+/** Reads samples from the device buffer. */
+ALC_API void       ALC_APIENTRY alcCaptureSamples(ALCdevice *device, ALCvoid *buffer, ALCsizei samples);
+#endif /* ALC_NO_PROTOTYPES */
+
+/* Pointer-to-function types, useful for storing dynamically loaded ALC entry
+ * points.
+ */
+typedef ALCcontext*    (ALC_APIENTRY *LPALCCREATECONTEXT)(ALCdevice *device, const ALCint *attrlist);
+typedef ALCboolean     (ALC_APIENTRY *LPALCMAKECONTEXTCURRENT)(ALCcontext *context);
+typedef void           (ALC_APIENTRY *LPALCPROCESSCONTEXT)(ALCcontext *context);
+typedef void           (ALC_APIENTRY *LPALCSUSPENDCONTEXT)(ALCcontext *context);
+typedef void           (ALC_APIENTRY *LPALCDESTROYCONTEXT)(ALCcontext *context);
+typedef ALCcontext*    (ALC_APIENTRY *LPALCGETCURRENTCONTEXT)(void);
+typedef ALCdevice*     (ALC_APIENTRY *LPALCGETCONTEXTSDEVICE)(ALCcontext *context);
+typedef ALCdevice*     (ALC_APIENTRY *LPALCOPENDEVICE)(const ALCchar *devicename);
+typedef ALCboolean     (ALC_APIENTRY *LPALCCLOSEDEVICE)(ALCdevice *device);
+typedef ALCenum        (ALC_APIENTRY *LPALCGETERROR)(ALCdevice *device);
+typedef ALCboolean     (ALC_APIENTRY *LPALCISEXTENSIONPRESENT)(ALCdevice *device, const ALCchar *extname);
+typedef ALCvoid*       (ALC_APIENTRY *LPALCGETPROCADDRESS)(ALCdevice *device, const ALCchar *funcname);
+typedef ALCenum        (ALC_APIENTRY *LPALCGETENUMVALUE)(ALCdevice *device, const ALCchar *enumname);
+typedef const ALCchar* (ALC_APIENTRY *LPALCGETSTRING)(ALCdevice *device, ALCenum param);
+typedef void           (ALC_APIENTRY *LPALCGETINTEGERV)(ALCdevice *device, ALCenum param, ALCsizei size, ALCint *values);
+typedef ALCdevice*     (ALC_APIENTRY *LPALCCAPTUREOPENDEVICE)(const ALCchar *devicename, ALCuint frequency, ALCenum format, ALCsizei buffersize);
+typedef ALCboolean     (ALC_APIENTRY *LPALCCAPTURECLOSEDEVICE)(ALCdevice *device);
+typedef void           (ALC_APIENTRY *LPALCCAPTURESTART)(ALCdevice *device);
+typedef void           (ALC_APIENTRY *LPALCCAPTURESTOP)(ALCdevice *device);
+typedef void           (ALC_APIENTRY *LPALCCAPTURESAMPLES)(ALCdevice *device, ALCvoid *buffer, ALCsizei samples);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* AL_ALC_H */
diff --git a/include/AL/alext.h b/include/AL/alext.h
new file mode 100644 (file)
index 0000000..d313a99
--- /dev/null
@@ -0,0 +1,655 @@
+#ifndef AL_ALEXT_H
+#define AL_ALEXT_H
+
+#include <stddef.h>
+/* Define int64 and uint64 types */
+#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) ||             \
+    (defined(__cplusplus) && __cplusplus >= 201103L)
+#include <stdint.h>
+typedef int64_t _alsoft_int64_t;
+typedef uint64_t _alsoft_uint64_t;
+#elif defined(_WIN32)
+typedef __int64 _alsoft_int64_t;
+typedef unsigned __int64 _alsoft_uint64_t;
+#else
+/* Fallback if nothing above works */
+#include <stdint.h>
+typedef int64_t _alsoft_int64_t;
+typedef uint64_t _alsoft_uint64_t;
+#endif
+
+#include "alc.h"
+#include "al.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef AL_LOKI_IMA_ADPCM_format
+#define AL_LOKI_IMA_ADPCM_format 1
+#define AL_FORMAT_IMA_ADPCM_MONO16_EXT           0x10000
+#define AL_FORMAT_IMA_ADPCM_STEREO16_EXT         0x10001
+#endif
+
+#ifndef AL_LOKI_WAVE_format
+#define AL_LOKI_WAVE_format 1
+#define AL_FORMAT_WAVE_EXT                       0x10002
+#endif
+
+#ifndef AL_EXT_vorbis
+#define AL_EXT_vorbis 1
+#define AL_FORMAT_VORBIS_EXT                     0x10003
+#endif
+
+#ifndef AL_LOKI_quadriphonic
+#define AL_LOKI_quadriphonic 1
+#define AL_FORMAT_QUAD8_LOKI                     0x10004
+#define AL_FORMAT_QUAD16_LOKI                    0x10005
+#endif
+
+#ifndef AL_EXT_float32
+#define AL_EXT_float32 1
+#define AL_FORMAT_MONO_FLOAT32                   0x10010
+#define AL_FORMAT_STEREO_FLOAT32                 0x10011
+#endif
+
+#ifndef AL_EXT_double
+#define AL_EXT_double 1
+#define AL_FORMAT_MONO_DOUBLE_EXT                0x10012
+#define AL_FORMAT_STEREO_DOUBLE_EXT              0x10013
+#endif
+
+#ifndef AL_EXT_MULAW
+#define AL_EXT_MULAW 1
+#define AL_FORMAT_MONO_MULAW_EXT                 0x10014
+#define AL_FORMAT_STEREO_MULAW_EXT               0x10015
+#endif
+
+#ifndef AL_EXT_ALAW
+#define AL_EXT_ALAW 1
+#define AL_FORMAT_MONO_ALAW_EXT                  0x10016
+#define AL_FORMAT_STEREO_ALAW_EXT                0x10017
+#endif
+
+#ifndef ALC_LOKI_audio_channel
+#define ALC_LOKI_audio_channel 1
+#define ALC_CHAN_MAIN_LOKI                       0x500001
+#define ALC_CHAN_PCM_LOKI                        0x500002
+#define ALC_CHAN_CD_LOKI                         0x500003
+#endif
+
+#ifndef AL_EXT_MCFORMATS
+#define AL_EXT_MCFORMATS 1
+/* Provides support for surround sound buffer formats with 8, 16, and 32-bit
+ * samples.
+ *
+ * QUAD8: Unsigned 8-bit, Quadraphonic (Front Left, Front Right, Rear Left,
+ *        Rear Right).
+ * QUAD16: Signed 16-bit, Quadraphonic.
+ * QUAD32: 32-bit float, Quadraphonic.
+ * REAR8: Unsigned 8-bit, Rear Stereo (Rear Left, Rear Right).
+ * REAR16: Signed 16-bit, Rear Stereo.
+ * REAR32: 32-bit float, Rear Stereo.
+ * 51CHN8: Unsigned 8-bit, 5.1 Surround (Front Left, Front Right, Front Center,
+ *         LFE, Side Left, Side Right). Note that some audio systems may label
+ *         5.1's Side channels as Rear or Surround; they are equivalent for the
+ *         purposes of this extension.
+ * 51CHN16: Signed 16-bit, 5.1 Surround.
+ * 51CHN32: 32-bit float, 5.1 Surround.
+ * 61CHN8: Unsigned 8-bit, 6.1 Surround (Front Left, Front Right, Front Center,
+ *         LFE, Rear Center, Side Left, Side Right).
+ * 61CHN16: Signed 16-bit, 6.1 Surround.
+ * 61CHN32: 32-bit float, 6.1 Surround.
+ * 71CHN8: Unsigned 8-bit, 7.1 Surround (Front Left, Front Right, Front Center,
+ *         LFE, Rear Left, Rear Right, Side Left, Side Right).
+ * 71CHN16: Signed 16-bit, 7.1 Surround.
+ * 71CHN32: 32-bit float, 7.1 Surround.
+ */
+#define AL_FORMAT_QUAD8                          0x1204
+#define AL_FORMAT_QUAD16                         0x1205
+#define AL_FORMAT_QUAD32                         0x1206
+#define AL_FORMAT_REAR8                          0x1207
+#define AL_FORMAT_REAR16                         0x1208
+#define AL_FORMAT_REAR32                         0x1209
+#define AL_FORMAT_51CHN8                         0x120A
+#define AL_FORMAT_51CHN16                        0x120B
+#define AL_FORMAT_51CHN32                        0x120C
+#define AL_FORMAT_61CHN8                         0x120D
+#define AL_FORMAT_61CHN16                        0x120E
+#define AL_FORMAT_61CHN32                        0x120F
+#define AL_FORMAT_71CHN8                         0x1210
+#define AL_FORMAT_71CHN16                        0x1211
+#define AL_FORMAT_71CHN32                        0x1212
+#endif
+
+#ifndef AL_EXT_MULAW_MCFORMATS
+#define AL_EXT_MULAW_MCFORMATS 1
+#define AL_FORMAT_MONO_MULAW                     0x10014
+#define AL_FORMAT_STEREO_MULAW                   0x10015
+#define AL_FORMAT_QUAD_MULAW                     0x10021
+#define AL_FORMAT_REAR_MULAW                     0x10022
+#define AL_FORMAT_51CHN_MULAW                    0x10023
+#define AL_FORMAT_61CHN_MULAW                    0x10024
+#define AL_FORMAT_71CHN_MULAW                    0x10025
+#endif
+
+#ifndef AL_EXT_IMA4
+#define AL_EXT_IMA4 1
+#define AL_FORMAT_MONO_IMA4                      0x1300
+#define AL_FORMAT_STEREO_IMA4                    0x1301
+#endif
+
+#ifndef AL_EXT_STATIC_BUFFER
+#define AL_EXT_STATIC_BUFFER 1
+typedef void (AL_APIENTRY*PFNALBUFFERDATASTATICPROC)(const ALuint,ALenum,ALvoid*,ALsizei,ALsizei);
+#ifdef AL_ALEXT_PROTOTYPES
+void AL_APIENTRY alBufferDataStatic(const ALuint buffer, ALenum format, ALvoid *data, ALsizei size, ALsizei freq);
+#endif
+#endif
+
+#ifndef ALC_EXT_EFX
+#define ALC_EXT_EFX 1
+#include "efx.h"
+#endif
+
+#ifndef ALC_EXT_disconnect
+#define ALC_EXT_disconnect 1
+#define ALC_CONNECTED                            0x313
+#endif
+
+#ifndef ALC_EXT_thread_local_context
+#define ALC_EXT_thread_local_context 1
+typedef ALCboolean  (ALC_APIENTRY*PFNALCSETTHREADCONTEXTPROC)(ALCcontext *context);
+typedef ALCcontext* (ALC_APIENTRY*PFNALCGETTHREADCONTEXTPROC)(void);
+#ifdef AL_ALEXT_PROTOTYPES
+ALC_API ALCboolean  ALC_APIENTRY alcSetThreadContext(ALCcontext *context);
+ALC_API ALCcontext* ALC_APIENTRY alcGetThreadContext(void);
+#endif
+#endif
+
+#ifndef AL_EXT_source_distance_model
+#define AL_EXT_source_distance_model 1
+#define AL_SOURCE_DISTANCE_MODEL                 0x200
+#endif
+
+#ifndef AL_SOFT_buffer_sub_data
+#define AL_SOFT_buffer_sub_data 1
+#define AL_BYTE_RW_OFFSETS_SOFT                  0x1031
+#define AL_SAMPLE_RW_OFFSETS_SOFT                0x1032
+typedef void (AL_APIENTRY*PFNALBUFFERSUBDATASOFTPROC)(ALuint,ALenum,const ALvoid*,ALsizei,ALsizei);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alBufferSubDataSOFT(ALuint buffer,ALenum format,const ALvoid *data,ALsizei offset,ALsizei length);
+#endif
+#endif
+
+#ifndef AL_SOFT_loop_points
+#define AL_SOFT_loop_points 1
+#define AL_LOOP_POINTS_SOFT                      0x2015
+#endif
+
+#ifndef AL_EXT_FOLDBACK
+#define AL_EXT_FOLDBACK 1
+#define AL_EXT_FOLDBACK_NAME                     "AL_EXT_FOLDBACK"
+#define AL_FOLDBACK_EVENT_BLOCK                  0x4112
+#define AL_FOLDBACK_EVENT_START                  0x4111
+#define AL_FOLDBACK_EVENT_STOP                   0x4113
+#define AL_FOLDBACK_MODE_MONO                    0x4101
+#define AL_FOLDBACK_MODE_STEREO                  0x4102
+typedef void (AL_APIENTRY*LPALFOLDBACKCALLBACK)(ALenum,ALsizei);
+typedef void (AL_APIENTRY*LPALREQUESTFOLDBACKSTART)(ALenum,ALsizei,ALsizei,ALfloat*,LPALFOLDBACKCALLBACK);
+typedef void (AL_APIENTRY*LPALREQUESTFOLDBACKSTOP)(void);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alRequestFoldbackStart(ALenum mode,ALsizei count,ALsizei length,ALfloat *mem,LPALFOLDBACKCALLBACK callback);
+AL_API void AL_APIENTRY alRequestFoldbackStop(void);
+#endif
+#endif
+
+#ifndef ALC_EXT_DEDICATED
+#define ALC_EXT_DEDICATED 1
+#define AL_DEDICATED_GAIN                        0x0001
+#define AL_EFFECT_DEDICATED_DIALOGUE             0x9001
+#define AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT 0x9000
+#endif
+
+#ifndef AL_SOFT_buffer_samples
+#define AL_SOFT_buffer_samples 1
+/* Channel configurations */
+#define AL_MONO_SOFT                             0x1500
+#define AL_STEREO_SOFT                           0x1501
+#define AL_REAR_SOFT                             0x1502
+#define AL_QUAD_SOFT                             0x1503
+#define AL_5POINT1_SOFT                          0x1504
+#define AL_6POINT1_SOFT                          0x1505
+#define AL_7POINT1_SOFT                          0x1506
+
+/* Sample types */
+#define AL_BYTE_SOFT                             0x1400
+#define AL_UNSIGNED_BYTE_SOFT                    0x1401
+#define AL_SHORT_SOFT                            0x1402
+#define AL_UNSIGNED_SHORT_SOFT                   0x1403
+#define AL_INT_SOFT                              0x1404
+#define AL_UNSIGNED_INT_SOFT                     0x1405
+#define AL_FLOAT_SOFT                            0x1406
+#define AL_DOUBLE_SOFT                           0x1407
+#define AL_BYTE3_SOFT                            0x1408
+#define AL_UNSIGNED_BYTE3_SOFT                   0x1409
+
+/* Storage formats */
+#define AL_MONO8_SOFT                            0x1100
+#define AL_MONO16_SOFT                           0x1101
+#define AL_MONO32F_SOFT                          0x10010
+#define AL_STEREO8_SOFT                          0x1102
+#define AL_STEREO16_SOFT                         0x1103
+#define AL_STEREO32F_SOFT                        0x10011
+#define AL_QUAD8_SOFT                            0x1204
+#define AL_QUAD16_SOFT                           0x1205
+#define AL_QUAD32F_SOFT                          0x1206
+#define AL_REAR8_SOFT                            0x1207
+#define AL_REAR16_SOFT                           0x1208
+#define AL_REAR32F_SOFT                          0x1209
+#define AL_5POINT1_8_SOFT                        0x120A
+#define AL_5POINT1_16_SOFT                       0x120B
+#define AL_5POINT1_32F_SOFT                      0x120C
+#define AL_6POINT1_8_SOFT                        0x120D
+#define AL_6POINT1_16_SOFT                       0x120E
+#define AL_6POINT1_32F_SOFT                      0x120F
+#define AL_7POINT1_8_SOFT                        0x1210
+#define AL_7POINT1_16_SOFT                       0x1211
+#define AL_7POINT1_32F_SOFT                      0x1212
+
+/* Buffer attributes */
+#define AL_INTERNAL_FORMAT_SOFT                  0x2008
+#define AL_BYTE_LENGTH_SOFT                      0x2009
+#define AL_SAMPLE_LENGTH_SOFT                    0x200A
+#define AL_SEC_LENGTH_SOFT                       0x200B
+
+typedef void (AL_APIENTRY*LPALBUFFERSAMPLESSOFT)(ALuint,ALuint,ALenum,ALsizei,ALenum,ALenum,const ALvoid*);
+typedef void (AL_APIENTRY*LPALBUFFERSUBSAMPLESSOFT)(ALuint,ALsizei,ALsizei,ALenum,ALenum,const ALvoid*);
+typedef void (AL_APIENTRY*LPALGETBUFFERSAMPLESSOFT)(ALuint,ALsizei,ALsizei,ALenum,ALenum,ALvoid*);
+typedef ALboolean (AL_APIENTRY*LPALISBUFFERFORMATSUPPORTEDSOFT)(ALenum);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alBufferSamplesSOFT(ALuint buffer, ALuint samplerate, ALenum internalformat, ALsizei samples, ALenum channels, ALenum type, const ALvoid *data);
+AL_API void AL_APIENTRY alBufferSubSamplesSOFT(ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, const ALvoid *data);
+AL_API void AL_APIENTRY alGetBufferSamplesSOFT(ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, ALvoid *data);
+AL_API ALboolean AL_APIENTRY alIsBufferFormatSupportedSOFT(ALenum format);
+#endif
+#endif
+
+#ifndef AL_SOFT_direct_channels
+#define AL_SOFT_direct_channels 1
+#define AL_DIRECT_CHANNELS_SOFT                  0x1033
+#endif
+
+#ifndef ALC_SOFT_loopback
+#define ALC_SOFT_loopback 1
+#define ALC_FORMAT_CHANNELS_SOFT                 0x1990
+#define ALC_FORMAT_TYPE_SOFT                     0x1991
+
+/* Sample types */
+#define ALC_BYTE_SOFT                            0x1400
+#define ALC_UNSIGNED_BYTE_SOFT                   0x1401
+#define ALC_SHORT_SOFT                           0x1402
+#define ALC_UNSIGNED_SHORT_SOFT                  0x1403
+#define ALC_INT_SOFT                             0x1404
+#define ALC_UNSIGNED_INT_SOFT                    0x1405
+#define ALC_FLOAT_SOFT                           0x1406
+
+/* Channel configurations */
+#define ALC_MONO_SOFT                            0x1500
+#define ALC_STEREO_SOFT                          0x1501
+#define ALC_QUAD_SOFT                            0x1503
+#define ALC_5POINT1_SOFT                         0x1504
+#define ALC_6POINT1_SOFT                         0x1505
+#define ALC_7POINT1_SOFT                         0x1506
+
+typedef ALCdevice* (ALC_APIENTRY*LPALCLOOPBACKOPENDEVICESOFT)(const ALCchar*);
+typedef ALCboolean (ALC_APIENTRY*LPALCISRENDERFORMATSUPPORTEDSOFT)(ALCdevice*,ALCsizei,ALCenum,ALCenum);
+typedef void (ALC_APIENTRY*LPALCRENDERSAMPLESSOFT)(ALCdevice*,ALCvoid*,ALCsizei);
+#ifdef AL_ALEXT_PROTOTYPES
+ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar *deviceName);
+ALC_API ALCboolean ALC_APIENTRY alcIsRenderFormatSupportedSOFT(ALCdevice *device, ALCsizei freq, ALCenum channels, ALCenum type);
+ALC_API void ALC_APIENTRY alcRenderSamplesSOFT(ALCdevice *device, ALCvoid *buffer, ALCsizei samples);
+#endif
+#endif
+
+#ifndef AL_EXT_STEREO_ANGLES
+#define AL_EXT_STEREO_ANGLES 1
+#define AL_STEREO_ANGLES                         0x1030
+#endif
+
+#ifndef AL_EXT_SOURCE_RADIUS
+#define AL_EXT_SOURCE_RADIUS 1
+#define AL_SOURCE_RADIUS                         0x1031
+#endif
+
+#ifndef AL_SOFT_source_latency
+#define AL_SOFT_source_latency 1
+#define AL_SAMPLE_OFFSET_LATENCY_SOFT            0x1200
+#define AL_SEC_OFFSET_LATENCY_SOFT               0x1201
+typedef _alsoft_int64_t ALint64SOFT;
+typedef _alsoft_uint64_t ALuint64SOFT;
+typedef void (AL_APIENTRY*LPALSOURCEDSOFT)(ALuint,ALenum,ALdouble);
+typedef void (AL_APIENTRY*LPALSOURCE3DSOFT)(ALuint,ALenum,ALdouble,ALdouble,ALdouble);
+typedef void (AL_APIENTRY*LPALSOURCEDVSOFT)(ALuint,ALenum,const ALdouble*);
+typedef void (AL_APIENTRY*LPALGETSOURCEDSOFT)(ALuint,ALenum,ALdouble*);
+typedef void (AL_APIENTRY*LPALGETSOURCE3DSOFT)(ALuint,ALenum,ALdouble*,ALdouble*,ALdouble*);
+typedef void (AL_APIENTRY*LPALGETSOURCEDVSOFT)(ALuint,ALenum,ALdouble*);
+typedef void (AL_APIENTRY*LPALSOURCEI64SOFT)(ALuint,ALenum,ALint64SOFT);
+typedef void (AL_APIENTRY*LPALSOURCE3I64SOFT)(ALuint,ALenum,ALint64SOFT,ALint64SOFT,ALint64SOFT);
+typedef void (AL_APIENTRY*LPALSOURCEI64VSOFT)(ALuint,ALenum,const ALint64SOFT*);
+typedef void (AL_APIENTRY*LPALGETSOURCEI64SOFT)(ALuint,ALenum,ALint64SOFT*);
+typedef void (AL_APIENTRY*LPALGETSOURCE3I64SOFT)(ALuint,ALenum,ALint64SOFT*,ALint64SOFT*,ALint64SOFT*);
+typedef void (AL_APIENTRY*LPALGETSOURCEI64VSOFT)(ALuint,ALenum,ALint64SOFT*);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alSourcedSOFT(ALuint source, ALenum param, ALdouble value);
+AL_API void AL_APIENTRY alSource3dSOFT(ALuint source, ALenum param, ALdouble value1, ALdouble value2, ALdouble value3);
+AL_API void AL_APIENTRY alSourcedvSOFT(ALuint source, ALenum param, const ALdouble *values);
+AL_API void AL_APIENTRY alGetSourcedSOFT(ALuint source, ALenum param, ALdouble *value);
+AL_API void AL_APIENTRY alGetSource3dSOFT(ALuint source, ALenum param, ALdouble *value1, ALdouble *value2, ALdouble *value3);
+AL_API void AL_APIENTRY alGetSourcedvSOFT(ALuint source, ALenum param, ALdouble *values);
+AL_API void AL_APIENTRY alSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT value);
+AL_API void AL_APIENTRY alSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT value1, ALint64SOFT value2, ALint64SOFT value3);
+AL_API void AL_APIENTRY alSourcei64vSOFT(ALuint source, ALenum param, const ALint64SOFT *values);
+AL_API void AL_APIENTRY alGetSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT *value);
+AL_API void AL_APIENTRY alGetSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT *value1, ALint64SOFT *value2, ALint64SOFT *value3);
+AL_API void AL_APIENTRY alGetSourcei64vSOFT(ALuint source, ALenum param, ALint64SOFT *values);
+#endif
+#endif
+
+#ifndef ALC_EXT_DEFAULT_FILTER_ORDER
+#define ALC_EXT_DEFAULT_FILTER_ORDER 1
+#define ALC_DEFAULT_FILTER_ORDER                 0x1100
+#endif
+
+#ifndef AL_SOFT_deferred_updates
+#define AL_SOFT_deferred_updates 1
+#define AL_DEFERRED_UPDATES_SOFT                 0xC002
+typedef void (AL_APIENTRY*LPALDEFERUPDATESSOFT)(void);
+typedef void (AL_APIENTRY*LPALPROCESSUPDATESSOFT)(void);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alDeferUpdatesSOFT(void);
+AL_API void AL_APIENTRY alProcessUpdatesSOFT(void);
+#endif
+#endif
+
+#ifndef AL_SOFT_block_alignment
+#define AL_SOFT_block_alignment 1
+#define AL_UNPACK_BLOCK_ALIGNMENT_SOFT           0x200C
+#define AL_PACK_BLOCK_ALIGNMENT_SOFT             0x200D
+#endif
+
+#ifndef AL_SOFT_MSADPCM
+#define AL_SOFT_MSADPCM 1
+#define AL_FORMAT_MONO_MSADPCM_SOFT              0x1302
+#define AL_FORMAT_STEREO_MSADPCM_SOFT            0x1303
+#endif
+
+#ifndef AL_SOFT_source_length
+#define AL_SOFT_source_length 1
+/*#define AL_BYTE_LENGTH_SOFT                      0x2009*/
+/*#define AL_SAMPLE_LENGTH_SOFT                    0x200A*/
+/*#define AL_SEC_LENGTH_SOFT                       0x200B*/
+#endif
+
+#ifndef AL_SOFT_buffer_length_query
+#define AL_SOFT_buffer_length_query 1
+/*#define AL_BYTE_LENGTH_SOFT                      0x2009*/
+/*#define AL_SAMPLE_LENGTH_SOFT                    0x200A*/
+/*#define AL_SEC_LENGTH_SOFT                       0x200B*/
+#endif
+
+#ifndef ALC_SOFT_pause_device
+#define ALC_SOFT_pause_device 1
+typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device);
+typedef void (ALC_APIENTRY*LPALCDEVICERESUMESOFT)(ALCdevice *device);
+#ifdef AL_ALEXT_PROTOTYPES
+ALC_API void ALC_APIENTRY alcDevicePauseSOFT(ALCdevice *device);
+ALC_API void ALC_APIENTRY alcDeviceResumeSOFT(ALCdevice *device);
+#endif
+#endif
+
+#ifndef AL_EXT_BFORMAT
+#define AL_EXT_BFORMAT 1
+/* Provides support for B-Format ambisonic buffers (first-order, FuMa scaling
+ * and layout).
+ *
+ * BFORMAT2D_8: Unsigned 8-bit, 3-channel non-periphonic (WXY).
+ * BFORMAT2D_16: Signed 16-bit, 3-channel non-periphonic (WXY).
+ * BFORMAT2D_FLOAT32: 32-bit float, 3-channel non-periphonic (WXY).
+ * BFORMAT3D_8: Unsigned 8-bit, 4-channel periphonic (WXYZ).
+ * BFORMAT3D_16: Signed 16-bit, 4-channel periphonic (WXYZ).
+ * BFORMAT3D_FLOAT32: 32-bit float, 4-channel periphonic (WXYZ).
+ */
+#define AL_FORMAT_BFORMAT2D_8                    0x20021
+#define AL_FORMAT_BFORMAT2D_16                   0x20022
+#define AL_FORMAT_BFORMAT2D_FLOAT32              0x20023
+#define AL_FORMAT_BFORMAT3D_8                    0x20031
+#define AL_FORMAT_BFORMAT3D_16                   0x20032
+#define AL_FORMAT_BFORMAT3D_FLOAT32              0x20033
+#endif
+
+#ifndef AL_EXT_MULAW_BFORMAT
+#define AL_EXT_MULAW_BFORMAT 1
+#define AL_FORMAT_BFORMAT2D_MULAW                0x10031
+#define AL_FORMAT_BFORMAT3D_MULAW                0x10032
+#endif
+
+#ifndef ALC_SOFT_HRTF
+#define ALC_SOFT_HRTF 1
+#define ALC_HRTF_SOFT                            0x1992
+#define ALC_DONT_CARE_SOFT                       0x0002
+#define ALC_HRTF_STATUS_SOFT                     0x1993
+#define ALC_HRTF_DISABLED_SOFT                   0x0000
+#define ALC_HRTF_ENABLED_SOFT                    0x0001
+#define ALC_HRTF_DENIED_SOFT                     0x0002
+#define ALC_HRTF_REQUIRED_SOFT                   0x0003
+#define ALC_HRTF_HEADPHONES_DETECTED_SOFT        0x0004
+#define ALC_HRTF_UNSUPPORTED_FORMAT_SOFT         0x0005
+#define ALC_NUM_HRTF_SPECIFIERS_SOFT             0x1994
+#define ALC_HRTF_SPECIFIER_SOFT                  0x1995
+#define ALC_HRTF_ID_SOFT                         0x1996
+typedef const ALCchar* (ALC_APIENTRY*LPALCGETSTRINGISOFT)(ALCdevice *device, ALCenum paramName, ALCsizei index);
+typedef ALCboolean (ALC_APIENTRY*LPALCRESETDEVICESOFT)(ALCdevice *device, const ALCint *attribs);
+#ifdef AL_ALEXT_PROTOTYPES
+ALC_API const ALCchar* ALC_APIENTRY alcGetStringiSOFT(ALCdevice *device, ALCenum paramName, ALCsizei index);
+ALC_API ALCboolean ALC_APIENTRY alcResetDeviceSOFT(ALCdevice *device, const ALCint *attribs);
+#endif
+#endif
+
+#ifndef AL_SOFT_gain_clamp_ex
+#define AL_SOFT_gain_clamp_ex 1
+#define AL_GAIN_LIMIT_SOFT                       0x200E
+#endif
+
+#ifndef AL_SOFT_source_resampler
+#define AL_SOFT_source_resampler
+#define AL_NUM_RESAMPLERS_SOFT                   0x1210
+#define AL_DEFAULT_RESAMPLER_SOFT                0x1211
+#define AL_SOURCE_RESAMPLER_SOFT                 0x1212
+#define AL_RESAMPLER_NAME_SOFT                   0x1213
+typedef const ALchar* (AL_APIENTRY*LPALGETSTRINGISOFT)(ALenum pname, ALsizei index);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API const ALchar* AL_APIENTRY alGetStringiSOFT(ALenum pname, ALsizei index);
+#endif
+#endif
+
+#ifndef AL_SOFT_source_spatialize
+#define AL_SOFT_source_spatialize
+#define AL_SOURCE_SPATIALIZE_SOFT                0x1214
+#define AL_AUTO_SOFT                             0x0002
+#endif
+
+#ifndef ALC_SOFT_output_limiter
+#define ALC_SOFT_output_limiter
+#define ALC_OUTPUT_LIMITER_SOFT                  0x199A
+#endif
+
+#ifndef ALC_SOFT_device_clock
+#define ALC_SOFT_device_clock 1
+typedef _alsoft_int64_t ALCint64SOFT;
+typedef _alsoft_uint64_t ALCuint64SOFT;
+#define ALC_DEVICE_CLOCK_SOFT                    0x1600
+#define ALC_DEVICE_LATENCY_SOFT                  0x1601
+#define ALC_DEVICE_CLOCK_LATENCY_SOFT            0x1602
+#define AL_SAMPLE_OFFSET_CLOCK_SOFT              0x1202
+#define AL_SEC_OFFSET_CLOCK_SOFT                 0x1203
+typedef void (ALC_APIENTRY*LPALCGETINTEGER64VSOFT)(ALCdevice *device, ALCenum pname, ALsizei size, ALCint64SOFT *values);
+#ifdef AL_ALEXT_PROTOTYPES
+ALC_API void ALC_APIENTRY alcGetInteger64vSOFT(ALCdevice *device, ALCenum pname, ALsizei size, ALCint64SOFT *values);
+#endif
+#endif
+
+#ifndef AL_SOFT_direct_channels_remix
+#define AL_SOFT_direct_channels_remix 1
+#define AL_DROP_UNMATCHED_SOFT                   0x0001
+#define AL_REMIX_UNMATCHED_SOFT                  0x0002
+#endif
+
+#ifndef AL_SOFT_bformat_ex
+#define AL_SOFT_bformat_ex 1
+#define AL_AMBISONIC_LAYOUT_SOFT                 0x1997
+#define AL_AMBISONIC_SCALING_SOFT                0x1998
+
+/* Ambisonic layouts */
+#define AL_FUMA_SOFT                             0x0000
+#define AL_ACN_SOFT                              0x0001
+
+/* Ambisonic scalings (normalization) */
+/*#define AL_FUMA_SOFT*/
+#define AL_SN3D_SOFT                             0x0001
+#define AL_N3D_SOFT                              0x0002
+#endif
+
+#ifndef ALC_SOFT_loopback_bformat
+#define ALC_SOFT_loopback_bformat 1
+#define ALC_AMBISONIC_LAYOUT_SOFT                0x1997
+#define ALC_AMBISONIC_SCALING_SOFT               0x1998
+#define ALC_AMBISONIC_ORDER_SOFT                 0x1999
+#define ALC_MAX_AMBISONIC_ORDER_SOFT             0x199B
+
+#define ALC_BFORMAT3D_SOFT                       0x1507
+
+/* Ambisonic layouts */
+#define ALC_FUMA_SOFT                            0x0000
+#define ALC_ACN_SOFT                             0x0001
+
+/* Ambisonic scalings (normalization) */
+/*#define ALC_FUMA_SOFT*/
+#define ALC_SN3D_SOFT                            0x0001
+#define ALC_N3D_SOFT                             0x0002
+#endif
+
+#ifndef AL_SOFT_effect_target
+#define AL_SOFT_effect_target
+#define AL_EFFECTSLOT_TARGET_SOFT                0x199C
+#endif
+
+#ifndef AL_SOFT_events
+#define AL_SOFT_events 1
+#define AL_EVENT_CALLBACK_FUNCTION_SOFT          0x19A2
+#define AL_EVENT_CALLBACK_USER_PARAM_SOFT        0x19A3
+#define AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT      0x19A4
+#define AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT  0x19A5
+#define AL_EVENT_TYPE_DISCONNECTED_SOFT          0x19A6
+typedef void (AL_APIENTRY*ALEVENTPROCSOFT)(ALenum eventType, ALuint object, ALuint param,
+                                           ALsizei length, const ALchar *message,
+                                           void *userParam);
+typedef void (AL_APIENTRY*LPALEVENTCONTROLSOFT)(ALsizei count, const ALenum *types, ALboolean enable);
+typedef void (AL_APIENTRY*LPALEVENTCALLBACKSOFT)(ALEVENTPROCSOFT callback, void *userParam);
+typedef void* (AL_APIENTRY*LPALGETPOINTERSOFT)(ALenum pname);
+typedef void (AL_APIENTRY*LPALGETPOINTERVSOFT)(ALenum pname, void **values);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alEventControlSOFT(ALsizei count, const ALenum *types, ALboolean enable);
+AL_API void AL_APIENTRY alEventCallbackSOFT(ALEVENTPROCSOFT callback, void *userParam);
+AL_API void* AL_APIENTRY alGetPointerSOFT(ALenum pname);
+AL_API void AL_APIENTRY alGetPointervSOFT(ALenum pname, void **values);
+#endif
+#endif
+
+#ifndef ALC_SOFT_reopen_device
+#define ALC_SOFT_reopen_device
+typedef ALCboolean (ALC_APIENTRY*LPALCREOPENDEVICESOFT)(ALCdevice *device,
+    const ALCchar *deviceName, const ALCint *attribs);
+#ifdef AL_ALEXT_PROTOTYPES
+ALCboolean ALC_APIENTRY alcReopenDeviceSOFT(ALCdevice *device, const ALCchar *deviceName,
+    const ALCint *attribs);
+#endif
+#endif
+
+#ifndef AL_SOFT_callback_buffer
+#define AL_SOFT_callback_buffer
+#define AL_BUFFER_CALLBACK_FUNCTION_SOFT         0x19A0
+#define AL_BUFFER_CALLBACK_USER_PARAM_SOFT       0x19A1
+typedef ALsizei (AL_APIENTRY*ALBUFFERCALLBACKTYPESOFT)(ALvoid *userptr, ALvoid *sampledata, ALsizei numbytes);
+typedef void (AL_APIENTRY*LPALBUFFERCALLBACKSOFT)(ALuint buffer, ALenum format, ALsizei freq, ALBUFFERCALLBACKTYPESOFT callback, ALvoid *userptr);
+typedef void (AL_APIENTRY*LPALGETBUFFERPTRSOFT)(ALuint buffer, ALenum param, ALvoid **value);
+typedef void (AL_APIENTRY*LPALGETBUFFER3PTRSOFT)(ALuint buffer, ALenum param, ALvoid **value1, ALvoid **value2, ALvoid **value3);
+typedef void (AL_APIENTRY*LPALGETBUFFERPTRVSOFT)(ALuint buffer, ALenum param, ALvoid **values);
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alBufferCallbackSOFT(ALuint buffer, ALenum format, ALsizei freq, ALBUFFERCALLBACKTYPESOFT callback, ALvoid *userptr);
+AL_API void AL_APIENTRY alGetBufferPtrSOFT(ALuint buffer, ALenum param, ALvoid **ptr);
+AL_API void AL_APIENTRY alGetBuffer3PtrSOFT(ALuint buffer, ALenum param, ALvoid **ptr0, ALvoid **ptr1, ALvoid **ptr2);
+AL_API void AL_APIENTRY alGetBufferPtrvSOFT(ALuint buffer, ALenum param, ALvoid **ptr);
+#endif
+#endif
+
+#ifndef AL_SOFT_UHJ
+#define AL_SOFT_UHJ
+#define AL_FORMAT_UHJ2CHN8_SOFT                  0x19A2
+#define AL_FORMAT_UHJ2CHN16_SOFT                 0x19A3
+#define AL_FORMAT_UHJ2CHN_FLOAT32_SOFT           0x19A4
+#define AL_FORMAT_UHJ3CHN8_SOFT                  0x19A5
+#define AL_FORMAT_UHJ3CHN16_SOFT                 0x19A6
+#define AL_FORMAT_UHJ3CHN_FLOAT32_SOFT           0x19A7
+#define AL_FORMAT_UHJ4CHN8_SOFT                  0x19A8
+#define AL_FORMAT_UHJ4CHN16_SOFT                 0x19A9
+#define AL_FORMAT_UHJ4CHN_FLOAT32_SOFT           0x19AA
+
+#define AL_STEREO_MODE_SOFT                      0x19B0
+#define AL_NORMAL_SOFT                           0x0000
+#define AL_SUPER_STEREO_SOFT                     0x0001
+#define AL_SUPER_STEREO_WIDTH_SOFT               0x19B1
+#endif
+
+#ifndef AL_SOFT_UHJ_ex
+#define AL_SOFT_UHJ_ex
+#define AL_FORMAT_UHJ2CHN_MULAW_SOFT             0x19B3
+#define AL_FORMAT_UHJ2CHN_ALAW_SOFT              0x19B4
+#define AL_FORMAT_UHJ2CHN_IMA4_SOFT              0x19B5
+#define AL_FORMAT_UHJ2CHN_MSADPCM_SOFT           0x19B6
+#define AL_FORMAT_UHJ3CHN_MULAW_SOFT             0x19B7
+#define AL_FORMAT_UHJ3CHN_ALAW_SOFT              0x19B8
+#define AL_FORMAT_UHJ4CHN_MULAW_SOFT             0x19B9
+#define AL_FORMAT_UHJ4CHN_ALAW_SOFT              0x19BA
+#endif
+
+#ifndef ALC_SOFT_output_mode
+#define ALC_SOFT_output_mode
+#define ALC_OUTPUT_MODE_SOFT                     0x19AC
+#define ALC_ANY_SOFT                             0x19AD
+/*#define ALC_MONO_SOFT                            0x1500*/
+/*#define ALC_STEREO_SOFT                          0x1501*/
+#define ALC_STEREO_BASIC_SOFT                    0x19AE
+#define ALC_STEREO_UHJ_SOFT                      0x19AF
+#define ALC_STEREO_HRTF_SOFT                     0x19B2
+/*#define ALC_QUAD_SOFT                            0x1503*/
+#define ALC_SURROUND_5_1_SOFT                    0x1504
+#define ALC_SURROUND_6_1_SOFT                    0x1505
+#define ALC_SURROUND_7_1_SOFT                    0x1506
+#endif
+
+#ifndef AL_SOFT_source_start_delay
+#define AL_SOFT_source_start_delay
+typedef void (AL_APIENTRY*LPALSOURCEPLAYATTIMESOFT)(ALuint source, ALint64SOFT start_time);
+typedef void (AL_APIENTRY*LPALSOURCEPLAYATTIMEVSOFT)(ALsizei n, const ALuint *sources, ALint64SOFT start_time);
+#ifdef AL_ALEXT_PROTOTYPES
+void AL_APIENTRY alSourcePlayAtTimeSOFT(ALuint source, ALint64SOFT start_time);
+void AL_APIENTRY alSourcePlayAtTimevSOFT(ALsizei n, const ALuint *sources, ALint64SOFT start_time);
+#endif
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/AL/efx-creative.h b/include/AL/efx-creative.h
new file mode 100644 (file)
index 0000000..0a04c98
--- /dev/null
@@ -0,0 +1,3 @@
+/* The tokens that would be defined here are already defined in efx.h. This
+ * empty file is here to provide compatibility with Windows-based projects
+ * that would include it. */
diff --git a/include/AL/efx-presets.h b/include/AL/efx-presets.h
new file mode 100644 (file)
index 0000000..8539fd5
--- /dev/null
@@ -0,0 +1,402 @@
+/* Reverb presets for EFX */
+
+#ifndef EFX_PRESETS_H
+#define EFX_PRESETS_H
+
+#ifndef EFXEAXREVERBPROPERTIES_DEFINED
+#define EFXEAXREVERBPROPERTIES_DEFINED
+typedef struct {
+    float flDensity;
+    float flDiffusion;
+    float flGain;
+    float flGainHF;
+    float flGainLF;
+    float flDecayTime;
+    float flDecayHFRatio;
+    float flDecayLFRatio;
+    float flReflectionsGain;
+    float flReflectionsDelay;
+    float flReflectionsPan[3];
+    float flLateReverbGain;
+    float flLateReverbDelay;
+    float flLateReverbPan[3];
+    float flEchoTime;
+    float flEchoDepth;
+    float flModulationTime;
+    float flModulationDepth;
+    float flAirAbsorptionGainHF;
+    float flHFReference;
+    float flLFReference;
+    float flRoomRolloffFactor;
+    int   iDecayHFLimit;
+} EFXEAXREVERBPROPERTIES, *LPEFXEAXREVERBPROPERTIES;
+#endif
+
+/* Default Presets */
+
+#define EFX_REVERB_PRESET_GENERIC \
+    { 1.0000f, 1.0000f, 0.3162f, 0.8913f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0500f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_PADDEDCELL \
+    { 0.1715f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.1700f, 0.1000f, 1.0000f, 0.2500f, 0.0010f, { 0.0000f, 0.0000f, 0.0000f }, 1.2691f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ROOM \
+    { 0.4287f, 1.0000f, 0.3162f, 0.5929f, 1.0000f, 0.4000f, 0.8300f, 1.0000f, 0.1503f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 1.0629f, 0.0030f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_BATHROOM \
+    { 0.1715f, 1.0000f, 0.3162f, 0.2512f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.6531f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 3.2734f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_LIVINGROOM \
+    { 0.9766f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.5000f, 0.1000f, 1.0000f, 0.2051f, 0.0030f, { 0.0000f, 0.0000f, 0.0000f }, 0.2805f, 0.0040f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_STONEROOM \
+    { 1.0000f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 2.3100f, 0.6400f, 1.0000f, 0.4411f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1003f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_AUDITORIUM \
+    { 1.0000f, 1.0000f, 0.3162f, 0.5781f, 1.0000f, 4.3200f, 0.5900f, 1.0000f, 0.4032f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.7170f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CONCERTHALL \
+    { 1.0000f, 1.0000f, 0.3162f, 0.5623f, 1.0000f, 3.9200f, 0.7000f, 1.0000f, 0.2427f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.9977f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CAVE \
+    { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 2.9100f, 1.3000f, 1.0000f, 0.5000f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.7063f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_ARENA \
+    { 1.0000f, 1.0000f, 0.3162f, 0.4477f, 1.0000f, 7.2400f, 0.3300f, 1.0000f, 0.2612f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.0186f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_HANGAR \
+    { 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 10.0500f, 0.2300f, 1.0000f, 0.5000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2560f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CARPETEDHALLWAY \
+    { 0.4287f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 0.3000f, 0.1000f, 1.0000f, 0.1215f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 0.1531f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_HALLWAY \
+    { 0.3645f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 1.4900f, 0.5900f, 1.0000f, 0.2458f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.6615f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_STONECORRIDOR \
+    { 1.0000f, 1.0000f, 0.3162f, 0.7612f, 1.0000f, 2.7000f, 0.7900f, 1.0000f, 0.2472f, 0.0130f, { 0.0000f, 0.0000f, 0.0000f }, 1.5758f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ALLEY \
+    { 1.0000f, 0.3000f, 0.3162f, 0.7328f, 1.0000f, 1.4900f, 0.8600f, 1.0000f, 0.2500f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.9954f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.9500f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FOREST \
+    { 1.0000f, 0.3000f, 0.3162f, 0.0224f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.0525f, 0.1620f, { 0.0000f, 0.0000f, 0.0000f }, 0.7682f, 0.0880f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CITY \
+    { 1.0000f, 0.5000f, 0.3162f, 0.3981f, 1.0000f, 1.4900f, 0.6700f, 1.0000f, 0.0730f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.1427f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_MOUNTAINS \
+    { 1.0000f, 0.2700f, 0.3162f, 0.0562f, 1.0000f, 1.4900f, 0.2100f, 1.0000f, 0.0407f, 0.3000f, { 0.0000f, 0.0000f, 0.0000f }, 0.1919f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_QUARRY \
+    { 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0000f, 0.0610f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.7000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_PLAIN \
+    { 1.0000f, 0.2100f, 0.3162f, 0.1000f, 1.0000f, 1.4900f, 0.5000f, 1.0000f, 0.0585f, 0.1790f, { 0.0000f, 0.0000f, 0.0000f }, 0.1089f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_PARKINGLOT \
+    { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 1.6500f, 1.5000f, 1.0000f, 0.2082f, 0.0080f, { 0.0000f, 0.0000f, 0.0000f }, 0.2652f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_SEWERPIPE \
+    { 0.3071f, 0.8000f, 0.3162f, 0.3162f, 1.0000f, 2.8100f, 0.1400f, 1.0000f, 1.6387f, 0.0140f, { 0.0000f, 0.0000f, 0.0000f }, 3.2471f, 0.0210f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_UNDERWATER \
+    { 0.3645f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 1.4900f, 0.1000f, 1.0000f, 0.5963f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 7.0795f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 1.1800f, 0.3480f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_DRUGGED \
+    { 0.4287f, 0.5000f, 0.3162f, 1.0000f, 1.0000f, 8.3900f, 1.3900f, 1.0000f, 0.8760f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 3.1081f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_DIZZY \
+    { 0.3645f, 0.6000f, 0.3162f, 0.6310f, 1.0000f, 17.2300f, 0.5600f, 1.0000f, 0.1392f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.4937f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.8100f, 0.3100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_PSYCHOTIC \
+    { 0.0625f, 0.5000f, 0.3162f, 0.8404f, 1.0000f, 7.5600f, 0.9100f, 1.0000f, 0.4864f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 2.4378f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 4.0000f, 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+/* Castle Presets */
+
+#define EFX_REVERB_PRESET_CASTLE_SMALLROOM \
+    { 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 1.2200f, 0.8300f, 0.3100f, 0.8913f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CASTLE_SHORTPASSAGE \
+    { 1.0000f, 0.8900f, 0.3162f, 0.3162f, 0.1000f, 2.3200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CASTLE_MEDIUMROOM \
+    { 1.0000f, 0.9300f, 0.3162f, 0.2818f, 0.1000f, 2.0400f, 0.8300f, 0.4600f, 0.6310f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1550f, 0.0300f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CASTLE_LARGEROOM \
+    { 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.1259f, 2.5300f, 0.8300f, 0.5000f, 0.4467f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1850f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CASTLE_LONGPASSAGE \
+    { 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 3.4200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CASTLE_HALL \
+    { 1.0000f, 0.8100f, 0.3162f, 0.2818f, 0.1778f, 3.1400f, 0.7900f, 0.6200f, 0.1778f, 0.0560f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CASTLE_CUPBOARD \
+    { 1.0000f, 0.8900f, 0.3162f, 0.2818f, 0.1000f, 0.6700f, 0.8700f, 0.3100f, 1.4125f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 3.5481f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CASTLE_COURTYARD \
+    { 1.0000f, 0.4200f, 0.3162f, 0.4467f, 0.1995f, 2.1300f, 0.6100f, 0.2300f, 0.2239f, 0.1600f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3700f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_CASTLE_ALCOVE \
+    { 1.0000f, 0.8900f, 0.3162f, 0.5012f, 0.1000f, 1.6400f, 0.8700f, 0.3100f, 1.0000f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 }
+
+/* Factory Presets */
+
+#define EFX_REVERB_PRESET_FACTORY_SMALLROOM \
+    { 0.3645f, 0.8200f, 0.3162f, 0.7943f, 0.5012f, 1.7200f, 0.6500f, 1.3100f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.1190f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_SHORTPASSAGE \
+    { 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 2.5300f, 0.6500f, 1.3100f, 1.0000f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_MEDIUMROOM \
+    { 0.4287f, 0.8200f, 0.2512f, 0.7943f, 0.5012f, 2.7600f, 0.6500f, 1.3100f, 0.2818f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1740f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_LARGEROOM \
+    { 0.4287f, 0.7500f, 0.2512f, 0.7079f, 0.6310f, 4.2400f, 0.5100f, 1.3100f, 0.1778f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2310f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_LONGPASSAGE \
+    { 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 4.0600f, 0.6500f, 1.3100f, 1.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_HALL \
+    { 0.4287f, 0.7500f, 0.3162f, 0.7079f, 0.6310f, 7.4300f, 0.5100f, 1.3100f, 0.0631f, 0.0730f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_CUPBOARD \
+    { 0.3071f, 0.6300f, 0.2512f, 0.7943f, 0.5012f, 0.4900f, 0.6500f, 1.3100f, 1.2589f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.1070f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_COURTYARD \
+    { 0.3071f, 0.5700f, 0.3162f, 0.3162f, 0.6310f, 2.3200f, 0.2900f, 0.5600f, 0.2239f, 0.1400f, { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2900f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_FACTORY_ALCOVE \
+    { 0.3645f, 0.5900f, 0.2512f, 0.7943f, 0.5012f, 3.1400f, 0.6500f, 1.3100f, 1.4125f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1140f, 0.1000f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 }
+
+/* Ice Palace Presets */
+
+#define EFX_REVERB_PRESET_ICEPALACE_SMALLROOM \
+    { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 1.5100f, 1.5300f, 0.2700f, 0.8913f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1640f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_SHORTPASSAGE \
+    { 1.0000f, 0.7500f, 0.3162f, 0.5623f, 0.2818f, 1.7900f, 1.4600f, 0.2800f, 0.5012f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.0900f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_MEDIUMROOM \
+    { 1.0000f, 0.8700f, 0.3162f, 0.5623f, 0.4467f, 2.2200f, 1.5300f, 0.3200f, 0.3981f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.1200f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_LARGEROOM \
+    { 1.0000f, 0.8100f, 0.3162f, 0.5623f, 0.4467f, 3.1400f, 1.5300f, 0.3200f, 0.2512f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_LONGPASSAGE \
+    { 1.0000f, 0.7700f, 0.3162f, 0.5623f, 0.3981f, 3.0100f, 1.4600f, 0.2800f, 0.7943f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.0400f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_HALL \
+    { 1.0000f, 0.7600f, 0.3162f, 0.4467f, 0.5623f, 5.4900f, 1.5300f, 0.3800f, 0.1122f, 0.0540f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0520f, { 0.0000f, 0.0000f, 0.0000f }, 0.2260f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_CUPBOARD \
+    { 1.0000f, 0.8300f, 0.3162f, 0.5012f, 0.2239f, 0.7600f, 1.5300f, 0.2600f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1430f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_COURTYARD \
+    { 1.0000f, 0.5900f, 0.3162f, 0.2818f, 0.3162f, 2.0400f, 1.2000f, 0.3800f, 0.3162f, 0.1730f, { 0.0000f, 0.0000f, 0.0000f }, 0.3162f, 0.0430f, { 0.0000f, 0.0000f, 0.0000f }, 0.2350f, 0.4800f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_ICEPALACE_ALCOVE \
+    { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 2.7600f, 1.4600f, 0.2800f, 1.1220f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1610f, 0.0900f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 }
+
+/* Space Station Presets */
+
+#define EFX_REVERB_PRESET_SPACESTATION_SMALLROOM \
+    { 0.2109f, 0.7000f, 0.3162f, 0.7079f, 0.8913f, 1.7200f, 0.8200f, 0.5500f, 0.7943f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0130f, { 0.0000f, 0.0000f, 0.0000f }, 0.1880f, 0.2600f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPACESTATION_SHORTPASSAGE \
+    { 0.2109f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 3.5700f, 0.5000f, 0.5500f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1720f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPACESTATION_MEDIUMROOM \
+    { 0.2109f, 0.7500f, 0.3162f, 0.6310f, 0.8913f, 3.0100f, 0.5000f, 0.5500f, 0.3981f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2090f, 0.3100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPACESTATION_LARGEROOM \
+    { 0.3645f, 0.8100f, 0.3162f, 0.6310f, 0.8913f, 3.8900f, 0.3800f, 0.6100f, 0.3162f, 0.0560f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2330f, 0.2800f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPACESTATION_LONGPASSAGE \
+    { 0.4287f, 0.8200f, 0.3162f, 0.6310f, 0.8913f, 4.6200f, 0.6200f, 0.5500f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPACESTATION_HALL \
+    { 0.4287f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 7.1100f, 0.3800f, 0.6100f, 0.1778f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2500f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPACESTATION_CUPBOARD \
+    { 0.1715f, 0.5600f, 0.3162f, 0.7079f, 0.8913f, 0.7900f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1810f, 0.3100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPACESTATION_ALCOVE \
+    { 0.2109f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.1600f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1920f, 0.2100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 }
+
+/* Wooden Galleon Presets */
+
+#define EFX_REVERB_PRESET_WOODEN_SMALLROOM \
+    { 1.0000f, 1.0000f, 0.3162f, 0.1122f, 0.3162f, 0.7900f, 0.3200f, 0.8700f, 1.0000f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_SHORTPASSAGE \
+    { 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.7500f, 0.5000f, 0.8700f, 0.8913f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_MEDIUMROOM \
+    { 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.2818f, 1.4700f, 0.4200f, 0.8200f, 0.8913f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_LARGEROOM \
+    { 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.2818f, 2.6500f, 0.3300f, 0.8200f, 0.8913f, 0.0660f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_LONGPASSAGE \
+    { 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.3162f, 1.9900f, 0.4000f, 0.7900f, 1.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.4467f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_HALL \
+    { 1.0000f, 1.0000f, 0.3162f, 0.0794f, 0.2818f, 3.4500f, 0.3000f, 0.8200f, 0.8913f, 0.0880f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_CUPBOARD \
+    { 1.0000f, 1.0000f, 0.3162f, 0.1413f, 0.3162f, 0.5600f, 0.4600f, 0.9100f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_COURTYARD \
+    { 1.0000f, 0.6500f, 0.3162f, 0.0794f, 0.3162f, 1.7900f, 0.3500f, 0.7900f, 0.5623f, 0.1230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1000f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_WOODEN_ALCOVE \
+    { 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.2200f, 0.6200f, 0.9100f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 }
+
+/* Sports Presets */
+
+#define EFX_REVERB_PRESET_SPORT_EMPTYSTADIUM \
+    { 1.0000f, 1.0000f, 0.3162f, 0.4467f, 0.7943f, 6.2600f, 0.5100f, 1.1000f, 0.0631f, 0.1830f, { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPORT_SQUASHCOURT \
+    { 1.0000f, 0.7500f, 0.3162f, 0.3162f, 0.7943f, 2.2200f, 0.9100f, 1.1600f, 0.4467f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1260f, 0.1900f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPORT_SMALLSWIMMINGPOOL \
+    { 1.0000f, 0.7000f, 0.3162f, 0.7943f, 0.8913f, 2.7600f, 1.2500f, 1.1400f, 0.6310f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, 0.1900f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_SPORT_LARGESWIMMINGPOOL \
+    { 1.0000f, 0.8200f, 0.3162f, 0.7943f, 1.0000f, 5.4900f, 1.3100f, 1.1400f, 0.4467f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2220f, 0.5500f, 1.1590f, 0.2100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_SPORT_GYMNASIUM \
+    { 1.0000f, 0.8100f, 0.3162f, 0.4467f, 0.8913f, 3.1400f, 1.0600f, 1.3500f, 0.3981f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0450f, { 0.0000f, 0.0000f, 0.0000f }, 0.1460f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPORT_FULLSTADIUM \
+    { 1.0000f, 1.0000f, 0.3162f, 0.0708f, 0.7943f, 5.2500f, 0.1700f, 0.8000f, 0.1000f, 0.1880f, { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SPORT_STADIUMTANNOY \
+    { 1.0000f, 0.7800f, 0.3162f, 0.5623f, 0.5012f, 2.5300f, 0.8800f, 0.6800f, 0.2818f, 0.2300f, { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+/* Prefab Presets */
+
+#define EFX_REVERB_PRESET_PREFAB_WORKSHOP \
+    { 0.4287f, 1.0000f, 0.3162f, 0.1413f, 0.3981f, 0.7600f, 1.0000f, 1.0000f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_PREFAB_SCHOOLROOM \
+    { 0.4022f, 0.6900f, 0.3162f, 0.6310f, 0.5012f, 0.9800f, 0.4500f, 0.1800f, 1.4125f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_PREFAB_PRACTISEROOM \
+    { 0.4022f, 0.8700f, 0.3162f, 0.3981f, 0.5012f, 1.1200f, 0.5600f, 0.1800f, 1.2589f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_PREFAB_OUTHOUSE \
+    { 1.0000f, 0.8200f, 0.3162f, 0.1122f, 0.1585f, 1.3800f, 0.3800f, 0.3500f, 0.8913f, 0.0240f, { 0.0000f, 0.0000f, -0.0000f }, 0.6310f, 0.0440f, { 0.0000f, 0.0000f, 0.0000f }, 0.1210f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_PREFAB_CARAVAN \
+    { 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.1259f, 0.4300f, 1.5000f, 1.0000f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+/* Dome and Pipe Presets */
+
+#define EFX_REVERB_PRESET_DOME_TOMB \
+    { 1.0000f, 0.7900f, 0.3162f, 0.3548f, 0.2239f, 4.1800f, 0.2100f, 0.1000f, 0.3868f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 1.6788f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.1900f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_PIPE_SMALL \
+    { 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 5.0400f, 0.1000f, 0.1000f, 0.5012f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 2.5119f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_DOME_SAINTPAULS \
+    { 1.0000f, 0.8700f, 0.3162f, 0.3548f, 0.2239f, 10.4800f, 0.1900f, 0.1000f, 0.1778f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0420f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1200f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_PIPE_LONGTHIN \
+    { 0.2560f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 9.2100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_PIPE_LARGE \
+    { 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 8.4500f, 0.1000f, 0.1000f, 0.3981f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_PIPE_RESONANT \
+    { 0.1373f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 6.8100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 }
+
+/* Outdoors Presets */
+
+#define EFX_REVERB_PRESET_OUTDOORS_BACKYARD \
+    { 1.0000f, 0.4500f, 0.3162f, 0.2512f, 0.5012f, 1.1200f, 0.3400f, 0.4600f, 0.4467f, 0.0690f, { 0.0000f, 0.0000f, -0.0000f }, 0.7079f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_OUTDOORS_ROLLINGPLAINS \
+    { 1.0000f, 0.0000f, 0.3162f, 0.0112f, 0.6310f, 2.1300f, 0.2100f, 0.4600f, 0.1778f, 0.3000f, { 0.0000f, 0.0000f, -0.0000f }, 0.4467f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_OUTDOORS_DEEPCANYON \
+    { 1.0000f, 0.7400f, 0.3162f, 0.1778f, 0.6310f, 3.8900f, 0.2100f, 0.4600f, 0.3162f, 0.2230f, { 0.0000f, 0.0000f, -0.0000f }, 0.3548f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_OUTDOORS_CREEK \
+    { 1.0000f, 0.3500f, 0.3162f, 0.1778f, 0.5012f, 2.1300f, 0.2100f, 0.4600f, 0.3981f, 0.1150f, { 0.0000f, 0.0000f, -0.0000f }, 0.1995f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_OUTDOORS_VALLEY \
+    { 1.0000f, 0.2800f, 0.3162f, 0.0282f, 0.1585f, 2.8800f, 0.2600f, 0.3500f, 0.1413f, 0.2630f, { 0.0000f, 0.0000f, -0.0000f }, 0.3981f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 }
+
+/* Mood Presets */
+
+#define EFX_REVERB_PRESET_MOOD_HEAVEN \
+    { 1.0000f, 0.9400f, 0.3162f, 0.7943f, 0.4467f, 5.0400f, 1.1200f, 0.5600f, 0.2427f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0800f, 2.7420f, 0.0500f, 0.9977f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_MOOD_HELL \
+    { 1.0000f, 0.5700f, 0.3162f, 0.3548f, 0.4467f, 3.5700f, 0.4900f, 2.0000f, 0.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1100f, 0.0400f, 2.1090f, 0.5200f, 0.9943f, 5000.0000f, 139.5000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_MOOD_MEMORY \
+    { 1.0000f, 0.8500f, 0.3162f, 0.6310f, 0.3548f, 4.0600f, 0.8200f, 0.5600f, 0.0398f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.4740f, 0.4500f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+/* Driving Presets */
+
+#define EFX_REVERB_PRESET_DRIVING_COMMENTATOR \
+    { 1.0000f, 0.0000f, 0.3162f, 0.5623f, 0.5012f, 2.4200f, 0.8800f, 0.6800f, 0.1995f, 0.0930f, { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_DRIVING_PITGARAGE \
+    { 0.4287f, 0.5900f, 0.3162f, 0.7079f, 0.5623f, 1.7200f, 0.9300f, 0.8700f, 0.5623f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_DRIVING_INCAR_RACER \
+    { 0.0832f, 0.8000f, 0.3162f, 1.0000f, 0.7943f, 0.1700f, 2.0000f, 0.4100f, 1.7783f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_DRIVING_INCAR_SPORTS \
+    { 0.0832f, 0.8000f, 0.3162f, 0.6310f, 1.0000f, 0.1700f, 0.7500f, 0.4100f, 1.0000f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_DRIVING_INCAR_LUXURY \
+    { 0.2560f, 1.0000f, 0.3162f, 0.1000f, 0.5012f, 0.1300f, 0.4100f, 0.4600f, 0.7943f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_DRIVING_FULLGRANDSTAND \
+    { 1.0000f, 1.0000f, 0.3162f, 0.2818f, 0.6310f, 3.0100f, 1.3700f, 1.2800f, 0.3548f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 0.1778f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_DRIVING_EMPTYGRANDSTAND \
+    { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 0.7943f, 4.6200f, 1.7500f, 1.4000f, 0.2082f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_DRIVING_TUNNEL \
+    { 1.0000f, 0.8100f, 0.3162f, 0.3981f, 0.8913f, 3.4200f, 0.9400f, 1.3100f, 0.7079f, 0.0510f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.0500f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 155.3000f, 0.0000f, 0x1 }
+
+/* City Presets */
+
+#define EFX_REVERB_PRESET_CITY_STREETS \
+    { 1.0000f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.7900f, 1.1200f, 0.9100f, 0.2818f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 0.1995f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CITY_SUBWAY \
+    { 1.0000f, 0.7400f, 0.3162f, 0.7079f, 0.8913f, 3.0100f, 1.2300f, 0.9100f, 0.7079f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.2100f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CITY_MUSEUM \
+    { 1.0000f, 0.8200f, 0.3162f, 0.1778f, 0.1778f, 3.2800f, 1.4000f, 0.5700f, 0.2512f, 0.0390f, { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_CITY_LIBRARY \
+    { 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.0891f, 2.7600f, 0.8900f, 0.4100f, 0.3548f, 0.0290f, { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 }
+
+#define EFX_REVERB_PRESET_CITY_UNDERPASS \
+    { 1.0000f, 0.8200f, 0.3162f, 0.4467f, 0.8913f, 3.5700f, 1.1200f, 0.9100f, 0.3981f, 0.0590f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1400f, 0.2500f, 0.0000f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CITY_ABANDONED \
+    { 1.0000f, 0.6900f, 0.3162f, 0.7943f, 0.8913f, 3.2800f, 1.1700f, 0.9100f, 0.4467f, 0.0440f, { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9966f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+/* Misc. Presets */
+
+#define EFX_REVERB_PRESET_DUSTYROOM \
+    { 0.3645f, 0.5600f, 0.3162f, 0.7943f, 0.7079f, 1.7900f, 0.3800f, 0.2100f, 0.5012f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0060f, { 0.0000f, 0.0000f, 0.0000f }, 0.2020f, 0.0500f, 0.2500f, 0.0000f, 0.9886f, 13046.0000f, 163.3000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_CHAPEL \
+    { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 1.0000f, 4.6200f, 0.6400f, 1.2300f, 0.4467f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.1100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 }
+
+#define EFX_REVERB_PRESET_SMALLWATERROOM \
+    { 1.0000f, 0.7000f, 0.3162f, 0.4477f, 1.0000f, 1.5100f, 1.2500f, 1.1400f, 0.8913f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, 0.1900f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x0 }
+
+#endif /* EFX_PRESETS_H */
diff --git a/include/AL/efx.h b/include/AL/efx.h
new file mode 100644 (file)
index 0000000..5ab64a6
--- /dev/null
@@ -0,0 +1,762 @@
+#ifndef AL_EFX_H
+#define AL_EFX_H
+
+#include <float.h>
+
+#include "alc.h"
+#include "al.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define ALC_EXT_EFX_NAME                         "ALC_EXT_EFX"
+
+#define ALC_EFX_MAJOR_VERSION                    0x20001
+#define ALC_EFX_MINOR_VERSION                    0x20002
+#define ALC_MAX_AUXILIARY_SENDS                  0x20003
+
+
+/* Listener properties. */
+#define AL_METERS_PER_UNIT                       0x20004
+
+/* Source properties. */
+#define AL_DIRECT_FILTER                         0x20005
+#define AL_AUXILIARY_SEND_FILTER                 0x20006
+#define AL_AIR_ABSORPTION_FACTOR                 0x20007
+#define AL_ROOM_ROLLOFF_FACTOR                   0x20008
+#define AL_CONE_OUTER_GAINHF                     0x20009
+#define AL_DIRECT_FILTER_GAINHF_AUTO             0x2000A
+#define AL_AUXILIARY_SEND_FILTER_GAIN_AUTO       0x2000B
+#define AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO     0x2000C
+
+
+/* Effect properties. */
+
+/* Reverb effect parameters */
+#define AL_REVERB_DENSITY                        0x0001
+#define AL_REVERB_DIFFUSION                      0x0002
+#define AL_REVERB_GAIN                           0x0003
+#define AL_REVERB_GAINHF                         0x0004
+#define AL_REVERB_DECAY_TIME                     0x0005
+#define AL_REVERB_DECAY_HFRATIO                  0x0006
+#define AL_REVERB_REFLECTIONS_GAIN               0x0007
+#define AL_REVERB_REFLECTIONS_DELAY              0x0008
+#define AL_REVERB_LATE_REVERB_GAIN               0x0009
+#define AL_REVERB_LATE_REVERB_DELAY              0x000A
+#define AL_REVERB_AIR_ABSORPTION_GAINHF          0x000B
+#define AL_REVERB_ROOM_ROLLOFF_FACTOR            0x000C
+#define AL_REVERB_DECAY_HFLIMIT                  0x000D
+
+/* EAX Reverb effect parameters */
+#define AL_EAXREVERB_DENSITY                     0x0001
+#define AL_EAXREVERB_DIFFUSION                   0x0002
+#define AL_EAXREVERB_GAIN                        0x0003
+#define AL_EAXREVERB_GAINHF                      0x0004
+#define AL_EAXREVERB_GAINLF                      0x0005
+#define AL_EAXREVERB_DECAY_TIME                  0x0006
+#define AL_EAXREVERB_DECAY_HFRATIO               0x0007
+#define AL_EAXREVERB_DECAY_LFRATIO               0x0008
+#define AL_EAXREVERB_REFLECTIONS_GAIN            0x0009
+#define AL_EAXREVERB_REFLECTIONS_DELAY           0x000A
+#define AL_EAXREVERB_REFLECTIONS_PAN             0x000B
+#define AL_EAXREVERB_LATE_REVERB_GAIN            0x000C
+#define AL_EAXREVERB_LATE_REVERB_DELAY           0x000D
+#define AL_EAXREVERB_LATE_REVERB_PAN             0x000E
+#define AL_EAXREVERB_ECHO_TIME                   0x000F
+#define AL_EAXREVERB_ECHO_DEPTH                  0x0010
+#define AL_EAXREVERB_MODULATION_TIME             0x0011
+#define AL_EAXREVERB_MODULATION_DEPTH            0x0012
+#define AL_EAXREVERB_AIR_ABSORPTION_GAINHF       0x0013
+#define AL_EAXREVERB_HFREFERENCE                 0x0014
+#define AL_EAXREVERB_LFREFERENCE                 0x0015
+#define AL_EAXREVERB_ROOM_ROLLOFF_FACTOR         0x0016
+#define AL_EAXREVERB_DECAY_HFLIMIT               0x0017
+
+/* Chorus effect parameters */
+#define AL_CHORUS_WAVEFORM                       0x0001
+#define AL_CHORUS_PHASE                          0x0002
+#define AL_CHORUS_RATE                           0x0003
+#define AL_CHORUS_DEPTH                          0x0004
+#define AL_CHORUS_FEEDBACK                       0x0005
+#define AL_CHORUS_DELAY                          0x0006
+
+/* Distortion effect parameters */
+#define AL_DISTORTION_EDGE                       0x0001
+#define AL_DISTORTION_GAIN                       0x0002
+#define AL_DISTORTION_LOWPASS_CUTOFF             0x0003
+#define AL_DISTORTION_EQCENTER                   0x0004
+#define AL_DISTORTION_EQBANDWIDTH                0x0005
+
+/* Echo effect parameters */
+#define AL_ECHO_DELAY                            0x0001
+#define AL_ECHO_LRDELAY                          0x0002
+#define AL_ECHO_DAMPING                          0x0003
+#define AL_ECHO_FEEDBACK                         0x0004
+#define AL_ECHO_SPREAD                           0x0005
+
+/* Flanger effect parameters */
+#define AL_FLANGER_WAVEFORM                      0x0001
+#define AL_FLANGER_PHASE                         0x0002
+#define AL_FLANGER_RATE                          0x0003
+#define AL_FLANGER_DEPTH                         0x0004
+#define AL_FLANGER_FEEDBACK                      0x0005
+#define AL_FLANGER_DELAY                         0x0006
+
+/* Frequency shifter effect parameters */
+#define AL_FREQUENCY_SHIFTER_FREQUENCY           0x0001
+#define AL_FREQUENCY_SHIFTER_LEFT_DIRECTION      0x0002
+#define AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION     0x0003
+
+/* Vocal morpher effect parameters */
+#define AL_VOCAL_MORPHER_PHONEMEA                0x0001
+#define AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING  0x0002
+#define AL_VOCAL_MORPHER_PHONEMEB                0x0003
+#define AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING  0x0004
+#define AL_VOCAL_MORPHER_WAVEFORM                0x0005
+#define AL_VOCAL_MORPHER_RATE                    0x0006
+
+/* Pitchshifter effect parameters */
+#define AL_PITCH_SHIFTER_COARSE_TUNE             0x0001
+#define AL_PITCH_SHIFTER_FINE_TUNE               0x0002
+
+/* Ringmodulator effect parameters */
+#define AL_RING_MODULATOR_FREQUENCY              0x0001
+#define AL_RING_MODULATOR_HIGHPASS_CUTOFF        0x0002
+#define AL_RING_MODULATOR_WAVEFORM               0x0003
+
+/* Autowah effect parameters */
+#define AL_AUTOWAH_ATTACK_TIME                   0x0001
+#define AL_AUTOWAH_RELEASE_TIME                  0x0002
+#define AL_AUTOWAH_RESONANCE                     0x0003
+#define AL_AUTOWAH_PEAK_GAIN                     0x0004
+
+/* Compressor effect parameters */
+#define AL_COMPRESSOR_ONOFF                      0x0001
+
+/* Equalizer effect parameters */
+#define AL_EQUALIZER_LOW_GAIN                    0x0001
+#define AL_EQUALIZER_LOW_CUTOFF                  0x0002
+#define AL_EQUALIZER_MID1_GAIN                   0x0003
+#define AL_EQUALIZER_MID1_CENTER                 0x0004
+#define AL_EQUALIZER_MID1_WIDTH                  0x0005
+#define AL_EQUALIZER_MID2_GAIN                   0x0006
+#define AL_EQUALIZER_MID2_CENTER                 0x0007
+#define AL_EQUALIZER_MID2_WIDTH                  0x0008
+#define AL_EQUALIZER_HIGH_GAIN                   0x0009
+#define AL_EQUALIZER_HIGH_CUTOFF                 0x000A
+
+/* Effect type */
+#define AL_EFFECT_FIRST_PARAMETER                0x0000
+#define AL_EFFECT_LAST_PARAMETER                 0x8000
+#define AL_EFFECT_TYPE                           0x8001
+
+/* Effect types, used with the AL_EFFECT_TYPE property */
+#define AL_EFFECT_NULL                           0x0000
+#define AL_EFFECT_REVERB                         0x0001
+#define AL_EFFECT_CHORUS                         0x0002
+#define AL_EFFECT_DISTORTION                     0x0003
+#define AL_EFFECT_ECHO                           0x0004
+#define AL_EFFECT_FLANGER                        0x0005
+#define AL_EFFECT_FREQUENCY_SHIFTER              0x0006
+#define AL_EFFECT_VOCAL_MORPHER                  0x0007
+#define AL_EFFECT_PITCH_SHIFTER                  0x0008
+#define AL_EFFECT_RING_MODULATOR                 0x0009
+#define AL_EFFECT_AUTOWAH                        0x000A
+#define AL_EFFECT_COMPRESSOR                     0x000B
+#define AL_EFFECT_EQUALIZER                      0x000C
+#define AL_EFFECT_EAXREVERB                      0x8000
+
+/* Auxiliary Effect Slot properties. */
+#define AL_EFFECTSLOT_EFFECT                     0x0001
+#define AL_EFFECTSLOT_GAIN                       0x0002
+#define AL_EFFECTSLOT_AUXILIARY_SEND_AUTO        0x0003
+
+/* NULL Auxiliary Slot ID to disable a source send. */
+#define AL_EFFECTSLOT_NULL                       0x0000
+
+
+/* Filter properties. */
+
+/* Lowpass filter parameters */
+#define AL_LOWPASS_GAIN                          0x0001
+#define AL_LOWPASS_GAINHF                        0x0002
+
+/* Highpass filter parameters */
+#define AL_HIGHPASS_GAIN                         0x0001
+#define AL_HIGHPASS_GAINLF                       0x0002
+
+/* Bandpass filter parameters */
+#define AL_BANDPASS_GAIN                         0x0001
+#define AL_BANDPASS_GAINLF                       0x0002
+#define AL_BANDPASS_GAINHF                       0x0003
+
+/* Filter type */
+#define AL_FILTER_FIRST_PARAMETER                0x0000
+#define AL_FILTER_LAST_PARAMETER                 0x8000
+#define AL_FILTER_TYPE                           0x8001
+
+/* Filter types, used with the AL_FILTER_TYPE property */
+#define AL_FILTER_NULL                           0x0000
+#define AL_FILTER_LOWPASS                        0x0001
+#define AL_FILTER_HIGHPASS                       0x0002
+#define AL_FILTER_BANDPASS                       0x0003
+
+
+/* Effect object function types. */
+typedef void (AL_APIENTRY *LPALGENEFFECTS)(ALsizei, ALuint*);
+typedef void (AL_APIENTRY *LPALDELETEEFFECTS)(ALsizei, const ALuint*);
+typedef ALboolean (AL_APIENTRY *LPALISEFFECT)(ALuint);
+typedef void (AL_APIENTRY *LPALEFFECTI)(ALuint, ALenum, ALint);
+typedef void (AL_APIENTRY *LPALEFFECTIV)(ALuint, ALenum, const ALint*);
+typedef void (AL_APIENTRY *LPALEFFECTF)(ALuint, ALenum, ALfloat);
+typedef void (AL_APIENTRY *LPALEFFECTFV)(ALuint, ALenum, const ALfloat*);
+typedef void (AL_APIENTRY *LPALGETEFFECTI)(ALuint, ALenum, ALint*);
+typedef void (AL_APIENTRY *LPALGETEFFECTIV)(ALuint, ALenum, ALint*);
+typedef void (AL_APIENTRY *LPALGETEFFECTF)(ALuint, ALenum, ALfloat*);
+typedef void (AL_APIENTRY *LPALGETEFFECTFV)(ALuint, ALenum, ALfloat*);
+
+/* Filter object function types. */
+typedef void (AL_APIENTRY *LPALGENFILTERS)(ALsizei, ALuint*);
+typedef void (AL_APIENTRY *LPALDELETEFILTERS)(ALsizei, const ALuint*);
+typedef ALboolean (AL_APIENTRY *LPALISFILTER)(ALuint);
+typedef void (AL_APIENTRY *LPALFILTERI)(ALuint, ALenum, ALint);
+typedef void (AL_APIENTRY *LPALFILTERIV)(ALuint, ALenum, const ALint*);
+typedef void (AL_APIENTRY *LPALFILTERF)(ALuint, ALenum, ALfloat);
+typedef void (AL_APIENTRY *LPALFILTERFV)(ALuint, ALenum, const ALfloat*);
+typedef void (AL_APIENTRY *LPALGETFILTERI)(ALuint, ALenum, ALint*);
+typedef void (AL_APIENTRY *LPALGETFILTERIV)(ALuint, ALenum, ALint*);
+typedef void (AL_APIENTRY *LPALGETFILTERF)(ALuint, ALenum, ALfloat*);
+typedef void (AL_APIENTRY *LPALGETFILTERFV)(ALuint, ALenum, ALfloat*);
+
+/* Auxiliary Effect Slot object function types. */
+typedef void (AL_APIENTRY *LPALGENAUXILIARYEFFECTSLOTS)(ALsizei, ALuint*);
+typedef void (AL_APIENTRY *LPALDELETEAUXILIARYEFFECTSLOTS)(ALsizei, const ALuint*);
+typedef ALboolean (AL_APIENTRY *LPALISAUXILIARYEFFECTSLOT)(ALuint);
+typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint);
+typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, const ALint*);
+typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat);
+typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, const ALfloat*);
+typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint*);
+typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, ALint*);
+typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat*);
+typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, ALfloat*);
+
+#ifdef AL_ALEXT_PROTOTYPES
+AL_API void AL_APIENTRY alGenEffects(ALsizei n, ALuint *effects);
+AL_API void AL_APIENTRY alDeleteEffects(ALsizei n, const ALuint *effects);
+AL_API ALboolean AL_APIENTRY alIsEffect(ALuint effect);
+AL_API void AL_APIENTRY alEffecti(ALuint effect, ALenum param, ALint iValue);
+AL_API void AL_APIENTRY alEffectiv(ALuint effect, ALenum param, const ALint *piValues);
+AL_API void AL_APIENTRY alEffectf(ALuint effect, ALenum param, ALfloat flValue);
+AL_API void AL_APIENTRY alEffectfv(ALuint effect, ALenum param, const ALfloat *pflValues);
+AL_API void AL_APIENTRY alGetEffecti(ALuint effect, ALenum param, ALint *piValue);
+AL_API void AL_APIENTRY alGetEffectiv(ALuint effect, ALenum param, ALint *piValues);
+AL_API void AL_APIENTRY alGetEffectf(ALuint effect, ALenum param, ALfloat *pflValue);
+AL_API void AL_APIENTRY alGetEffectfv(ALuint effect, ALenum param, ALfloat *pflValues);
+
+AL_API void AL_APIENTRY alGenFilters(ALsizei n, ALuint *filters);
+AL_API void AL_APIENTRY alDeleteFilters(ALsizei n, const ALuint *filters);
+AL_API ALboolean AL_APIENTRY alIsFilter(ALuint filter);
+AL_API void AL_APIENTRY alFilteri(ALuint filter, ALenum param, ALint iValue);
+AL_API void AL_APIENTRY alFilteriv(ALuint filter, ALenum param, const ALint *piValues);
+AL_API void AL_APIENTRY alFilterf(ALuint filter, ALenum param, ALfloat flValue);
+AL_API void AL_APIENTRY alFilterfv(ALuint filter, ALenum param, const ALfloat *pflValues);
+AL_API void AL_APIENTRY alGetFilteri(ALuint filter, ALenum param, ALint *piValue);
+AL_API void AL_APIENTRY alGetFilteriv(ALuint filter, ALenum param, ALint *piValues);
+AL_API void AL_APIENTRY alGetFilterf(ALuint filter, ALenum param, ALfloat *pflValue);
+AL_API void AL_APIENTRY alGetFilterfv(ALuint filter, ALenum param, ALfloat *pflValues);
+
+AL_API void AL_APIENTRY alGenAuxiliaryEffectSlots(ALsizei n, ALuint *effectslots);
+AL_API void AL_APIENTRY alDeleteAuxiliaryEffectSlots(ALsizei n, const ALuint *effectslots);
+AL_API ALboolean AL_APIENTRY alIsAuxiliaryEffectSlot(ALuint effectslot);
+AL_API void AL_APIENTRY alAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint iValue);
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, const ALint *piValues);
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat flValue);
+AL_API void AL_APIENTRY alAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, const ALfloat *pflValues);
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint *piValue);
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, ALint *piValues);
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat *pflValue);
+AL_API void AL_APIENTRY alGetAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, ALfloat *pflValues);
+#endif
+
+/* Filter ranges and defaults. */
+
+/* Lowpass filter */
+#define AL_LOWPASS_MIN_GAIN                      (0.0f)
+#define AL_LOWPASS_MAX_GAIN                      (1.0f)
+#define AL_LOWPASS_DEFAULT_GAIN                  (1.0f)
+
+#define AL_LOWPASS_MIN_GAINHF                    (0.0f)
+#define AL_LOWPASS_MAX_GAINHF                    (1.0f)
+#define AL_LOWPASS_DEFAULT_GAINHF                (1.0f)
+
+/* Highpass filter */
+#define AL_HIGHPASS_MIN_GAIN                     (0.0f)
+#define AL_HIGHPASS_MAX_GAIN                     (1.0f)
+#define AL_HIGHPASS_DEFAULT_GAIN                 (1.0f)
+
+#define AL_HIGHPASS_MIN_GAINLF                   (0.0f)
+#define AL_HIGHPASS_MAX_GAINLF                   (1.0f)
+#define AL_HIGHPASS_DEFAULT_GAINLF               (1.0f)
+
+/* Bandpass filter */
+#define AL_BANDPASS_MIN_GAIN                     (0.0f)
+#define AL_BANDPASS_MAX_GAIN                     (1.0f)
+#define AL_BANDPASS_DEFAULT_GAIN                 (1.0f)
+
+#define AL_BANDPASS_MIN_GAINHF                   (0.0f)
+#define AL_BANDPASS_MAX_GAINHF                   (1.0f)
+#define AL_BANDPASS_DEFAULT_GAINHF               (1.0f)
+
+#define AL_BANDPASS_MIN_GAINLF                   (0.0f)
+#define AL_BANDPASS_MAX_GAINLF                   (1.0f)
+#define AL_BANDPASS_DEFAULT_GAINLF               (1.0f)
+
+
+/* Effect parameter ranges and defaults. */
+
+/* Standard reverb effect */
+#define AL_REVERB_MIN_DENSITY                    (0.0f)
+#define AL_REVERB_MAX_DENSITY                    (1.0f)
+#define AL_REVERB_DEFAULT_DENSITY                (1.0f)
+
+#define AL_REVERB_MIN_DIFFUSION                  (0.0f)
+#define AL_REVERB_MAX_DIFFUSION                  (1.0f)
+#define AL_REVERB_DEFAULT_DIFFUSION              (1.0f)
+
+#define AL_REVERB_MIN_GAIN                       (0.0f)
+#define AL_REVERB_MAX_GAIN                       (1.0f)
+#define AL_REVERB_DEFAULT_GAIN                   (0.32f)
+
+#define AL_REVERB_MIN_GAINHF                     (0.0f)
+#define AL_REVERB_MAX_GAINHF                     (1.0f)
+#define AL_REVERB_DEFAULT_GAINHF                 (0.89f)
+
+#define AL_REVERB_MIN_DECAY_TIME                 (0.1f)
+#define AL_REVERB_MAX_DECAY_TIME                 (20.0f)
+#define AL_REVERB_DEFAULT_DECAY_TIME             (1.49f)
+
+#define AL_REVERB_MIN_DECAY_HFRATIO              (0.1f)
+#define AL_REVERB_MAX_DECAY_HFRATIO              (2.0f)
+#define AL_REVERB_DEFAULT_DECAY_HFRATIO          (0.83f)
+
+#define AL_REVERB_MIN_REFLECTIONS_GAIN           (0.0f)
+#define AL_REVERB_MAX_REFLECTIONS_GAIN           (3.16f)
+#define AL_REVERB_DEFAULT_REFLECTIONS_GAIN       (0.05f)
+
+#define AL_REVERB_MIN_REFLECTIONS_DELAY          (0.0f)
+#define AL_REVERB_MAX_REFLECTIONS_DELAY          (0.3f)
+#define AL_REVERB_DEFAULT_REFLECTIONS_DELAY      (0.007f)
+
+#define AL_REVERB_MIN_LATE_REVERB_GAIN           (0.0f)
+#define AL_REVERB_MAX_LATE_REVERB_GAIN           (10.0f)
+#define AL_REVERB_DEFAULT_LATE_REVERB_GAIN       (1.26f)
+
+#define AL_REVERB_MIN_LATE_REVERB_DELAY          (0.0f)
+#define AL_REVERB_MAX_LATE_REVERB_DELAY          (0.1f)
+#define AL_REVERB_DEFAULT_LATE_REVERB_DELAY      (0.011f)
+
+#define AL_REVERB_MIN_AIR_ABSORPTION_GAINHF      (0.892f)
+#define AL_REVERB_MAX_AIR_ABSORPTION_GAINHF      (1.0f)
+#define AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF  (0.994f)
+
+#define AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR        (0.0f)
+#define AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR        (10.0f)
+#define AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR    (0.0f)
+
+#define AL_REVERB_MIN_DECAY_HFLIMIT              AL_FALSE
+#define AL_REVERB_MAX_DECAY_HFLIMIT              AL_TRUE
+#define AL_REVERB_DEFAULT_DECAY_HFLIMIT          AL_TRUE
+
+/* EAX reverb effect */
+#define AL_EAXREVERB_MIN_DENSITY                 (0.0f)
+#define AL_EAXREVERB_MAX_DENSITY                 (1.0f)
+#define AL_EAXREVERB_DEFAULT_DENSITY             (1.0f)
+
+#define AL_EAXREVERB_MIN_DIFFUSION               (0.0f)
+#define AL_EAXREVERB_MAX_DIFFUSION               (1.0f)
+#define AL_EAXREVERB_DEFAULT_DIFFUSION           (1.0f)
+
+#define AL_EAXREVERB_MIN_GAIN                    (0.0f)
+#define AL_EAXREVERB_MAX_GAIN                    (1.0f)
+#define AL_EAXREVERB_DEFAULT_GAIN                (0.32f)
+
+#define AL_EAXREVERB_MIN_GAINHF                  (0.0f)
+#define AL_EAXREVERB_MAX_GAINHF                  (1.0f)
+#define AL_EAXREVERB_DEFAULT_GAINHF              (0.89f)
+
+#define AL_EAXREVERB_MIN_GAINLF                  (0.0f)
+#define AL_EAXREVERB_MAX_GAINLF                  (1.0f)
+#define AL_EAXREVERB_DEFAULT_GAINLF              (1.0f)
+
+#define AL_EAXREVERB_MIN_DECAY_TIME              (0.1f)
+#define AL_EAXREVERB_MAX_DECAY_TIME              (20.0f)
+#define AL_EAXREVERB_DEFAULT_DECAY_TIME          (1.49f)
+
+#define AL_EAXREVERB_MIN_DECAY_HFRATIO           (0.1f)
+#define AL_EAXREVERB_MAX_DECAY_HFRATIO           (2.0f)
+#define AL_EAXREVERB_DEFAULT_DECAY_HFRATIO       (0.83f)
+
+#define AL_EAXREVERB_MIN_DECAY_LFRATIO           (0.1f)
+#define AL_EAXREVERB_MAX_DECAY_LFRATIO           (2.0f)
+#define AL_EAXREVERB_DEFAULT_DECAY_LFRATIO       (1.0f)
+
+#define AL_EAXREVERB_MIN_REFLECTIONS_GAIN        (0.0f)
+#define AL_EAXREVERB_MAX_REFLECTIONS_GAIN        (3.16f)
+#define AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN    (0.05f)
+
+#define AL_EAXREVERB_MIN_REFLECTIONS_DELAY       (0.0f)
+#define AL_EAXREVERB_MAX_REFLECTIONS_DELAY       (0.3f)
+#define AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY   (0.007f)
+
+#define AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ (0.0f)
+
+#define AL_EAXREVERB_MIN_LATE_REVERB_GAIN        (0.0f)
+#define AL_EAXREVERB_MAX_LATE_REVERB_GAIN        (10.0f)
+#define AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN    (1.26f)
+
+#define AL_EAXREVERB_MIN_LATE_REVERB_DELAY       (0.0f)
+#define AL_EAXREVERB_MAX_LATE_REVERB_DELAY       (0.1f)
+#define AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY   (0.011f)
+
+#define AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ (0.0f)
+
+#define AL_EAXREVERB_MIN_ECHO_TIME               (0.075f)
+#define AL_EAXREVERB_MAX_ECHO_TIME               (0.25f)
+#define AL_EAXREVERB_DEFAULT_ECHO_TIME           (0.25f)
+
+#define AL_EAXREVERB_MIN_ECHO_DEPTH              (0.0f)
+#define AL_EAXREVERB_MAX_ECHO_DEPTH              (1.0f)
+#define AL_EAXREVERB_DEFAULT_ECHO_DEPTH          (0.0f)
+
+#define AL_EAXREVERB_MIN_MODULATION_TIME         (0.04f)
+#define AL_EAXREVERB_MAX_MODULATION_TIME         (4.0f)
+#define AL_EAXREVERB_DEFAULT_MODULATION_TIME     (0.25f)
+
+#define AL_EAXREVERB_MIN_MODULATION_DEPTH        (0.0f)
+#define AL_EAXREVERB_MAX_MODULATION_DEPTH        (1.0f)
+#define AL_EAXREVERB_DEFAULT_MODULATION_DEPTH    (0.0f)
+
+#define AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF   (0.892f)
+#define AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF   (1.0f)
+#define AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f)
+
+#define AL_EAXREVERB_MIN_HFREFERENCE             (1000.0f)
+#define AL_EAXREVERB_MAX_HFREFERENCE             (20000.0f)
+#define AL_EAXREVERB_DEFAULT_HFREFERENCE         (5000.0f)
+
+#define AL_EAXREVERB_MIN_LFREFERENCE             (20.0f)
+#define AL_EAXREVERB_MAX_LFREFERENCE             (1000.0f)
+#define AL_EAXREVERB_DEFAULT_LFREFERENCE         (250.0f)
+
+#define AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR     (0.0f)
+#define AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR     (10.0f)
+#define AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f)
+
+#define AL_EAXREVERB_MIN_DECAY_HFLIMIT           AL_FALSE
+#define AL_EAXREVERB_MAX_DECAY_HFLIMIT           AL_TRUE
+#define AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT       AL_TRUE
+
+/* Chorus effect */
+#define AL_CHORUS_WAVEFORM_SINUSOID              (0)
+#define AL_CHORUS_WAVEFORM_TRIANGLE              (1)
+
+#define AL_CHORUS_MIN_WAVEFORM                   (0)
+#define AL_CHORUS_MAX_WAVEFORM                   (1)
+#define AL_CHORUS_DEFAULT_WAVEFORM               (1)
+
+#define AL_CHORUS_MIN_PHASE                      (-180)
+#define AL_CHORUS_MAX_PHASE                      (180)
+#define AL_CHORUS_DEFAULT_PHASE                  (90)
+
+#define AL_CHORUS_MIN_RATE                       (0.0f)
+#define AL_CHORUS_MAX_RATE                       (10.0f)
+#define AL_CHORUS_DEFAULT_RATE                   (1.1f)
+
+#define AL_CHORUS_MIN_DEPTH                      (0.0f)
+#define AL_CHORUS_MAX_DEPTH                      (1.0f)
+#define AL_CHORUS_DEFAULT_DEPTH                  (0.1f)
+
+#define AL_CHORUS_MIN_FEEDBACK                   (-1.0f)
+#define AL_CHORUS_MAX_FEEDBACK                   (1.0f)
+#define AL_CHORUS_DEFAULT_FEEDBACK               (0.25f)
+
+#define AL_CHORUS_MIN_DELAY                      (0.0f)
+#define AL_CHORUS_MAX_DELAY                      (0.016f)
+#define AL_CHORUS_DEFAULT_DELAY                  (0.016f)
+
+/* Distortion effect */
+#define AL_DISTORTION_MIN_EDGE                   (0.0f)
+#define AL_DISTORTION_MAX_EDGE                   (1.0f)
+#define AL_DISTORTION_DEFAULT_EDGE               (0.2f)
+
+#define AL_DISTORTION_MIN_GAIN                   (0.01f)
+#define AL_DISTORTION_MAX_GAIN                   (1.0f)
+#define AL_DISTORTION_DEFAULT_GAIN               (0.05f)
+
+#define AL_DISTORTION_MIN_LOWPASS_CUTOFF         (80.0f)
+#define AL_DISTORTION_MAX_LOWPASS_CUTOFF         (24000.0f)
+#define AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF     (8000.0f)
+
+#define AL_DISTORTION_MIN_EQCENTER               (80.0f)
+#define AL_DISTORTION_MAX_EQCENTER               (24000.0f)
+#define AL_DISTORTION_DEFAULT_EQCENTER           (3600.0f)
+
+#define AL_DISTORTION_MIN_EQBANDWIDTH            (80.0f)
+#define AL_DISTORTION_MAX_EQBANDWIDTH            (24000.0f)
+#define AL_DISTORTION_DEFAULT_EQBANDWIDTH        (3600.0f)
+
+/* Echo effect */
+#define AL_ECHO_MIN_DELAY                        (0.0f)
+#define AL_ECHO_MAX_DELAY                        (0.207f)
+#define AL_ECHO_DEFAULT_DELAY                    (0.1f)
+
+#define AL_ECHO_MIN_LRDELAY                      (0.0f)
+#define AL_ECHO_MAX_LRDELAY                      (0.404f)
+#define AL_ECHO_DEFAULT_LRDELAY                  (0.1f)
+
+#define AL_ECHO_MIN_DAMPING                      (0.0f)
+#define AL_ECHO_MAX_DAMPING                      (0.99f)
+#define AL_ECHO_DEFAULT_DAMPING                  (0.5f)
+
+#define AL_ECHO_MIN_FEEDBACK                     (0.0f)
+#define AL_ECHO_MAX_FEEDBACK                     (1.0f)
+#define AL_ECHO_DEFAULT_FEEDBACK                 (0.5f)
+
+#define AL_ECHO_MIN_SPREAD                       (-1.0f)
+#define AL_ECHO_MAX_SPREAD                       (1.0f)
+#define AL_ECHO_DEFAULT_SPREAD                   (-1.0f)
+
+/* Flanger effect */
+#define AL_FLANGER_WAVEFORM_SINUSOID             (0)
+#define AL_FLANGER_WAVEFORM_TRIANGLE             (1)
+
+#define AL_FLANGER_MIN_WAVEFORM                  (0)
+#define AL_FLANGER_MAX_WAVEFORM                  (1)
+#define AL_FLANGER_DEFAULT_WAVEFORM              (1)
+
+#define AL_FLANGER_MIN_PHASE                     (-180)
+#define AL_FLANGER_MAX_PHASE                     (180)
+#define AL_FLANGER_DEFAULT_PHASE                 (0)
+
+#define AL_FLANGER_MIN_RATE                      (0.0f)
+#define AL_FLANGER_MAX_RATE                      (10.0f)
+#define AL_FLANGER_DEFAULT_RATE                  (0.27f)
+
+#define AL_FLANGER_MIN_DEPTH                     (0.0f)
+#define AL_FLANGER_MAX_DEPTH                     (1.0f)
+#define AL_FLANGER_DEFAULT_DEPTH                 (1.0f)
+
+#define AL_FLANGER_MIN_FEEDBACK                  (-1.0f)
+#define AL_FLANGER_MAX_FEEDBACK                  (1.0f)
+#define AL_FLANGER_DEFAULT_FEEDBACK              (-0.5f)
+
+#define AL_FLANGER_MIN_DELAY                     (0.0f)
+#define AL_FLANGER_MAX_DELAY                     (0.004f)
+#define AL_FLANGER_DEFAULT_DELAY                 (0.002f)
+
+/* Frequency shifter effect */
+#define AL_FREQUENCY_SHIFTER_MIN_FREQUENCY       (0.0f)
+#define AL_FREQUENCY_SHIFTER_MAX_FREQUENCY       (24000.0f)
+#define AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY   (0.0f)
+
+#define AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION  (0)
+#define AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION  (2)
+#define AL_FREQUENCY_SHIFTER_DEFAULT_LEFT_DIRECTION (0)
+
+#define AL_FREQUENCY_SHIFTER_DIRECTION_DOWN      (0)
+#define AL_FREQUENCY_SHIFTER_DIRECTION_UP        (1)
+#define AL_FREQUENCY_SHIFTER_DIRECTION_OFF       (2)
+
+#define AL_FREQUENCY_SHIFTER_MIN_RIGHT_DIRECTION (0)
+#define AL_FREQUENCY_SHIFTER_MAX_RIGHT_DIRECTION (2)
+#define AL_FREQUENCY_SHIFTER_DEFAULT_RIGHT_DIRECTION (0)
+
+/* Vocal morpher effect */
+#define AL_VOCAL_MORPHER_MIN_PHONEMEA            (0)
+#define AL_VOCAL_MORPHER_MAX_PHONEMEA            (29)
+#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA        (0)
+
+#define AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING (-24)
+#define AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING (24)
+#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA_COARSE_TUNING (0)
+
+#define AL_VOCAL_MORPHER_MIN_PHONEMEB            (0)
+#define AL_VOCAL_MORPHER_MAX_PHONEMEB            (29)
+#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB        (10)
+
+#define AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING (-24)
+#define AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING (24)
+#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB_COARSE_TUNING (0)
+
+#define AL_VOCAL_MORPHER_PHONEME_A               (0)
+#define AL_VOCAL_MORPHER_PHONEME_E               (1)
+#define AL_VOCAL_MORPHER_PHONEME_I               (2)
+#define AL_VOCAL_MORPHER_PHONEME_O               (3)
+#define AL_VOCAL_MORPHER_PHONEME_U               (4)
+#define AL_VOCAL_MORPHER_PHONEME_AA              (5)
+#define AL_VOCAL_MORPHER_PHONEME_AE              (6)
+#define AL_VOCAL_MORPHER_PHONEME_AH              (7)
+#define AL_VOCAL_MORPHER_PHONEME_AO              (8)
+#define AL_VOCAL_MORPHER_PHONEME_EH              (9)
+#define AL_VOCAL_MORPHER_PHONEME_ER              (10)
+#define AL_VOCAL_MORPHER_PHONEME_IH              (11)
+#define AL_VOCAL_MORPHER_PHONEME_IY              (12)
+#define AL_VOCAL_MORPHER_PHONEME_UH              (13)
+#define AL_VOCAL_MORPHER_PHONEME_UW              (14)
+#define AL_VOCAL_MORPHER_PHONEME_B               (15)
+#define AL_VOCAL_MORPHER_PHONEME_D               (16)
+#define AL_VOCAL_MORPHER_PHONEME_F               (17)
+#define AL_VOCAL_MORPHER_PHONEME_G               (18)
+#define AL_VOCAL_MORPHER_PHONEME_J               (19)
+#define AL_VOCAL_MORPHER_PHONEME_K               (20)
+#define AL_VOCAL_MORPHER_PHONEME_L               (21)
+#define AL_VOCAL_MORPHER_PHONEME_M               (22)
+#define AL_VOCAL_MORPHER_PHONEME_N               (23)
+#define AL_VOCAL_MORPHER_PHONEME_P               (24)
+#define AL_VOCAL_MORPHER_PHONEME_R               (25)
+#define AL_VOCAL_MORPHER_PHONEME_S               (26)
+#define AL_VOCAL_MORPHER_PHONEME_T               (27)
+#define AL_VOCAL_MORPHER_PHONEME_V               (28)
+#define AL_VOCAL_MORPHER_PHONEME_Z               (29)
+
+#define AL_VOCAL_MORPHER_WAVEFORM_SINUSOID       (0)
+#define AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE       (1)
+#define AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH       (2)
+
+#define AL_VOCAL_MORPHER_MIN_WAVEFORM            (0)
+#define AL_VOCAL_MORPHER_MAX_WAVEFORM            (2)
+#define AL_VOCAL_MORPHER_DEFAULT_WAVEFORM        (0)
+
+#define AL_VOCAL_MORPHER_MIN_RATE                (0.0f)
+#define AL_VOCAL_MORPHER_MAX_RATE                (10.0f)
+#define AL_VOCAL_MORPHER_DEFAULT_RATE            (1.41f)
+
+/* Pitch shifter effect */
+#define AL_PITCH_SHIFTER_MIN_COARSE_TUNE         (-12)
+#define AL_PITCH_SHIFTER_MAX_COARSE_TUNE         (12)
+#define AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE     (12)
+
+#define AL_PITCH_SHIFTER_MIN_FINE_TUNE           (-50)
+#define AL_PITCH_SHIFTER_MAX_FINE_TUNE           (50)
+#define AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE       (0)
+
+/* Ring modulator effect */
+#define AL_RING_MODULATOR_MIN_FREQUENCY          (0.0f)
+#define AL_RING_MODULATOR_MAX_FREQUENCY          (8000.0f)
+#define AL_RING_MODULATOR_DEFAULT_FREQUENCY      (440.0f)
+
+#define AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF    (0.0f)
+#define AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF    (24000.0f)
+#define AL_RING_MODULATOR_DEFAULT_HIGHPASS_CUTOFF (800.0f)
+
+#define AL_RING_MODULATOR_SINUSOID               (0)
+#define AL_RING_MODULATOR_SAWTOOTH               (1)
+#define AL_RING_MODULATOR_SQUARE                 (2)
+
+#define AL_RING_MODULATOR_MIN_WAVEFORM           (0)
+#define AL_RING_MODULATOR_MAX_WAVEFORM           (2)
+#define AL_RING_MODULATOR_DEFAULT_WAVEFORM       (0)
+
+/* Autowah effect */
+#define AL_AUTOWAH_MIN_ATTACK_TIME               (0.0001f)
+#define AL_AUTOWAH_MAX_ATTACK_TIME               (1.0f)
+#define AL_AUTOWAH_DEFAULT_ATTACK_TIME           (0.06f)
+
+#define AL_AUTOWAH_MIN_RELEASE_TIME              (0.0001f)
+#define AL_AUTOWAH_MAX_RELEASE_TIME              (1.0f)
+#define AL_AUTOWAH_DEFAULT_RELEASE_TIME          (0.06f)
+
+#define AL_AUTOWAH_MIN_RESONANCE                 (2.0f)
+#define AL_AUTOWAH_MAX_RESONANCE                 (1000.0f)
+#define AL_AUTOWAH_DEFAULT_RESONANCE             (1000.0f)
+
+#define AL_AUTOWAH_MIN_PEAK_GAIN                 (0.00003f)
+#define AL_AUTOWAH_MAX_PEAK_GAIN                 (31621.0f)
+#define AL_AUTOWAH_DEFAULT_PEAK_GAIN             (11.22f)
+
+/* Compressor effect */
+#define AL_COMPRESSOR_MIN_ONOFF                  (0)
+#define AL_COMPRESSOR_MAX_ONOFF                  (1)
+#define AL_COMPRESSOR_DEFAULT_ONOFF              (1)
+
+/* Equalizer effect */
+#define AL_EQUALIZER_MIN_LOW_GAIN                (0.126f)
+#define AL_EQUALIZER_MAX_LOW_GAIN                (7.943f)
+#define AL_EQUALIZER_DEFAULT_LOW_GAIN            (1.0f)
+
+#define AL_EQUALIZER_MIN_LOW_CUTOFF              (50.0f)
+#define AL_EQUALIZER_MAX_LOW_CUTOFF              (800.0f)
+#define AL_EQUALIZER_DEFAULT_LOW_CUTOFF          (200.0f)
+
+#define AL_EQUALIZER_MIN_MID1_GAIN               (0.126f)
+#define AL_EQUALIZER_MAX_MID1_GAIN               (7.943f)
+#define AL_EQUALIZER_DEFAULT_MID1_GAIN           (1.0f)
+
+#define AL_EQUALIZER_MIN_MID1_CENTER             (200.0f)
+#define AL_EQUALIZER_MAX_MID1_CENTER             (3000.0f)
+#define AL_EQUALIZER_DEFAULT_MID1_CENTER         (500.0f)
+
+#define AL_EQUALIZER_MIN_MID1_WIDTH              (0.01f)
+#define AL_EQUALIZER_MAX_MID1_WIDTH              (1.0f)
+#define AL_EQUALIZER_DEFAULT_MID1_WIDTH          (1.0f)
+
+#define AL_EQUALIZER_MIN_MID2_GAIN               (0.126f)
+#define AL_EQUALIZER_MAX_MID2_GAIN               (7.943f)
+#define AL_EQUALIZER_DEFAULT_MID2_GAIN           (1.0f)
+
+#define AL_EQUALIZER_MIN_MID2_CENTER             (1000.0f)
+#define AL_EQUALIZER_MAX_MID2_CENTER             (8000.0f)
+#define AL_EQUALIZER_DEFAULT_MID2_CENTER         (3000.0f)
+
+#define AL_EQUALIZER_MIN_MID2_WIDTH              (0.01f)
+#define AL_EQUALIZER_MAX_MID2_WIDTH              (1.0f)
+#define AL_EQUALIZER_DEFAULT_MID2_WIDTH          (1.0f)
+
+#define AL_EQUALIZER_MIN_HIGH_GAIN               (0.126f)
+#define AL_EQUALIZER_MAX_HIGH_GAIN               (7.943f)
+#define AL_EQUALIZER_DEFAULT_HIGH_GAIN           (1.0f)
+
+#define AL_EQUALIZER_MIN_HIGH_CUTOFF             (4000.0f)
+#define AL_EQUALIZER_MAX_HIGH_CUTOFF             (16000.0f)
+#define AL_EQUALIZER_DEFAULT_HIGH_CUTOFF         (6000.0f)
+
+
+/* Source parameter value ranges and defaults. */
+#define AL_MIN_AIR_ABSORPTION_FACTOR             (0.0f)
+#define AL_MAX_AIR_ABSORPTION_FACTOR             (10.0f)
+#define AL_DEFAULT_AIR_ABSORPTION_FACTOR         (0.0f)
+
+#define AL_MIN_ROOM_ROLLOFF_FACTOR               (0.0f)
+#define AL_MAX_ROOM_ROLLOFF_FACTOR               (10.0f)
+#define AL_DEFAULT_ROOM_ROLLOFF_FACTOR           (0.0f)
+
+#define AL_MIN_CONE_OUTER_GAINHF                 (0.0f)
+#define AL_MAX_CONE_OUTER_GAINHF                 (1.0f)
+#define AL_DEFAULT_CONE_OUTER_GAINHF             (1.0f)
+
+#define AL_MIN_DIRECT_FILTER_GAINHF_AUTO         AL_FALSE
+#define AL_MAX_DIRECT_FILTER_GAINHF_AUTO         AL_TRUE
+#define AL_DEFAULT_DIRECT_FILTER_GAINHF_AUTO     AL_TRUE
+
+#define AL_MIN_AUXILIARY_SEND_FILTER_GAIN_AUTO   AL_FALSE
+#define AL_MAX_AUXILIARY_SEND_FILTER_GAIN_AUTO   AL_TRUE
+#define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE
+
+#define AL_MIN_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_FALSE
+#define AL_MAX_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE
+#define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE
+
+
+/* Listener parameter value ranges and defaults. */
+#define AL_MIN_METERS_PER_UNIT                   FLT_MIN
+#define AL_MAX_METERS_PER_UNIT                   FLT_MAX
+#define AL_DEFAULT_METERS_PER_UNIT               (1.0f)
+
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif /* AL_EFX_H */
diff --git a/libopenal.version b/libopenal.version
new file mode 100644 (file)
index 0000000..dd998e1
--- /dev/null
@@ -0,0 +1,192 @@
+{
+global:
+alBuffer3f;
+alBuffer3i;
+alBufferData;
+alBufferf;
+alBufferfv;
+alBufferi;
+alBufferiv;
+alcCaptureCloseDevice;
+alcCaptureOpenDevice;
+alcCaptureSamples;
+alcCaptureStart;
+alcCaptureStop;
+alcCloseDevice;
+alcCreateContext;
+alcDestroyContext;
+alcGetContextsDevice;
+alcGetCurrentContext;
+alcGetEnumValue;
+alcGetError;
+alcGetIntegerv;
+alcGetProcAddress;
+alcGetString;
+alcIsExtensionPresent;
+alcMakeContextCurrent;
+alcOpenDevice;
+alcProcessContext;
+alcSuspendContext;
+alDeleteBuffers;
+alDeleteSources;
+alDisable;
+alDistanceModel;
+alDopplerFactor;
+alEnable;
+alGenBuffers;
+alGenSources;
+alGetBoolean;
+alGetBooleanv;
+alGetBuffer3f;
+alGetBuffer3i;
+alGetBufferf;
+alGetBufferfv;
+alGetBufferi;
+alGetBufferiv;
+alGetDouble;
+alGetDoublev;
+alGetEnumValue;
+alGetError;
+alGetFloat;
+alGetFloatv;
+alGetInteger;
+alGetIntegerv;
+alGetListener3f;
+alGetListener3i;
+alGetListenerf;
+alGetListenerfv;
+alGetListeneri;
+alGetListeneriv;
+alGetProcAddress;
+alGetSource3f;
+alGetSource3i;
+alGetSourcef;
+alGetSourcefv;
+alGetSourcei;
+alGetSourceiv;
+alGetString;
+alIsBuffer;
+alIsEnabled;
+alIsExtensionPresent;
+alIsSource;
+alListener3f;
+alListener3i;
+alListenerf;
+alListenerfv;
+alListeneri;
+alListeneriv;
+alSource3f;
+alSource3i;
+alSourcef;
+alSourcefv;
+alSourcei;
+alSourceiv;
+alSourcePause;
+alSourcePausev;
+alSourcePlay;
+alSourcePlayv;
+alSourceQueueBuffers;
+alSourceRewind;
+alSourceRewindv;
+alSourceStop;
+alSourceStopv;
+alSourceUnqueueBuffers;
+alSpeedOfSound;
+
+# Deprecated in AL 1.1, kept for compatibility.
+alDopplerVelocity;
+
+# EFX, effectively standard at this point.
+alAuxiliaryEffectSlotf;
+alAuxiliaryEffectSlotfv;
+alAuxiliaryEffectSloti;
+alAuxiliaryEffectSlotiv;
+alDeleteAuxiliaryEffectSlots;
+alDeleteEffects;
+alDeleteFilters;
+alEffectf;
+alEffectfv;
+alEffecti;
+alEffectiv;
+alFilterf;
+alFilterfv;
+alFilteri;
+alFilteriv;
+alGenAuxiliaryEffectSlots;
+alGenEffects;
+alGenFilters;
+alGetAuxiliaryEffectSlotf;
+alGetAuxiliaryEffectSlotfv;
+alGetAuxiliaryEffectSloti;
+alGetAuxiliaryEffectSlotiv;
+alGetEffectf;
+alGetEffectfv;
+alGetEffecti;
+alGetEffectiv;
+alGetFilterf;
+alGetFilterfv;
+alGetFilteri;
+alGetFilteriv;
+alIsAuxiliaryEffectSlot;
+alIsEffect;
+alIsFilter;
+
+# Non-standard
+alsoft_get_version;
+
+# These extension functions shouldn't be exported here, but they were exported
+# by mistake in previous releases, so need to stay for compatibility with apps
+# that may have directly linked to them. Remove them if it can be done without
+# breaking anything.
+alAuxiliaryEffectSlotPlaySOFT;
+alAuxiliaryEffectSlotPlayvSOFT;
+alAuxiliaryEffectSlotStopSOFT;
+alAuxiliaryEffectSlotStopvSOFT;
+alBufferCallbackSOFT;
+alBufferSamplesSOFT;
+alBufferStorageSOFT;
+alBufferSubDataSOFT;
+alBufferSubSamplesSOFT;
+alcDevicePauseSOFT;
+alcDeviceResumeSOFT;
+alcGetInteger64vSOFT;
+alcGetStringiSOFT;
+alcGetThreadContext;
+alcIsRenderFormatSupportedSOFT;
+alcLoopbackOpenDeviceSOFT;
+alcRenderSamplesSOFT;
+alcResetDeviceSOFT;
+alcSetThreadContext;
+alDeferUpdatesSOFT;
+alEventCallbackSOFT;
+alEventControlSOFT;
+alFlushMappedBufferSOFT;
+alGetBuffer3PtrSOFT;
+alGetBufferPtrSOFT;
+alGetBufferPtrvSOFT;
+alGetBufferSamplesSOFT;
+alGetInteger64SOFT;
+alGetInteger64vSOFT;
+alGetPointerSOFT;
+alGetPointervSOFT;
+alGetSource3dSOFT;
+alGetSource3i64SOFT;
+alGetSourcedSOFT;
+alGetSourcedvSOFT;
+alGetSourcei64SOFT;
+alGetSourcei64vSOFT;
+alGetStringiSOFT;
+alIsBufferFormatSupportedSOFT;
+alMapBufferSOFT;
+alProcessUpdatesSOFT;
+alSource3dSOFT;
+alSource3i64SOFT;
+alSourcedSOFT;
+alSourcedvSOFT;
+alSourcei64SOFT;
+alSourcei64vSOFT;
+alSourceQueueBufferLayersSOFT;
+alUnmapBufferSOFT;
+
+local: *;
+};
diff --git a/openal.pc.in b/openal.pc.in
new file mode 100644 (file)
index 0000000..dfa6f57
--- /dev/null
@@ -0,0 +1,12 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: OpenAL
+Description: OpenAL is a cross-platform 3D audio API
+Requires: @PKG_CONFIG_REQUIRES@
+Version: @PACKAGE_VERSION@
+Libs: -L${libdir} -l@LIBNAME@ @PKG_CONFIG_LIBS@
+Libs.private:@PKG_CONFIG_PRIVATE_LIBS@
+Cflags: -I${includedir} -I${includedir}/AL @PKG_CONFIG_CFLAGS@
diff --git a/presets/3D7.1.ambdec b/presets/3D7.1.ambdec
new file mode 100644 (file)
index 0000000..ec3b787
--- /dev/null
@@ -0,0 +1,66 @@
+# AmbDec configuration
+# Written by Ambisonic Decoder Toolbox, version 8.0
+
+# input channel order: W Y Z X
+
+/description     3D7-noCenter_1h1v_pinv_even_energy_rV_max_rE_2_band
+
+# In OpenAL Soft, 3D7.1 is a distinct configuration that uses the standard 5.1
+# channels (LF, RF, CE, LS, RS), plus two auxiliary channels (AUX0, AUX1) in
+# place of the rear speakers. AUX0 corresponds to the LB speaker (upper back
+# center), and AUX1 corresponds to the RB speaker (lower front center).
+
+# Similar to the the ITU-5.1-nocenter configuration, the front-center is
+# declared here so that an appropriate distance may be set (for proper delaying
+# or attenuating of dialog and such which feed it directly). It otherwise does
+# not contribute to positional sound output due to its irregular position.
+
+/version               3
+
+/dec/chan_mask         f
+/dec/freq_bands        2
+/dec/speakers          6
+/dec/coeff_scale       n3d
+
+/opt/input_scale       n3d
+/opt/nfeff_comp        input
+/opt/delay_comp        on
+/opt/level_comp        on
+/opt/xover_freq        400.000000
+/opt/xover_ratio       0.000000
+
+/speakers/{
+#              id       dist            azim            elev           conn
+#-----------------------------------------------------------------------
+add_spkr       LF       1.828800         51.000000      24.000000
+add_spkr       RF       1.828800        -51.000000      24.000000
+add_spkr       CE       1.828800          0.000000       0.000000
+add_spkr       AUX0     1.828800        180.000000      55.000000
+add_spkr       AUX1     1.828800          0.000000     -55.000000
+add_spkr       LS       1.828800        129.000000     -24.000000
+add_spkr       RS       1.828800       -129.000000     -24.000000
+/}
+
+/lfmatrix/{
+order_gain     1.00000000e+00  1.00000000e+00  0.000000        0.000000
+add_row         1.666666667e-01  2.033043281e-01  1.175581508e-01  1.678904388e-01
+add_row         1.666666667e-01 -2.033043281e-01  1.175581508e-01  1.678904388e-01
+add_row         0.000000000e+00  0.000000000e+00  0.000000000e+00  0.000000000e+00
+add_row         1.666666667e-01  0.000000000e+00  2.356640879e-01 -1.667265410e-01
+add_row         1.666666667e-01  0.000000000e+00 -2.356640879e-01  1.667265410e-01
+add_row         1.666666667e-01  2.033043281e-01 -1.175581508e-01 -1.678904388e-01
+add_row         1.666666667e-01 -2.033043281e-01 -1.175581508e-01 -1.678904388e-01
+/}
+
+/hfmatrix/{
+order_gain     1.73205081e+00  1.00000000e+00  0.000000        0.000000
+add_row         1.666666667e-01  2.033043281e-01  1.175581508e-01  1.678904388e-01
+add_row         1.666666667e-01 -2.033043281e-01  1.175581508e-01  1.678904388e-01
+add_row         0.000000000e+00  0.000000000e+00  0.000000000e+00  0.000000000e+00
+add_row         1.666666667e-01  0.000000000e+00  2.356640879e-01 -1.667265410e-01
+add_row         1.666666667e-01  0.000000000e+00 -2.356640879e-01  1.667265410e-01
+add_row         1.666666667e-01  2.033043281e-01 -1.175581508e-01 -1.678904388e-01
+add_row         1.666666667e-01 -2.033043281e-01 -1.175581508e-01 -1.678904388e-01
+/}
+
+/end
diff --git a/presets/hex-quad.ambdec b/presets/hex-quad.ambdec
new file mode 100644 (file)
index 0000000..ce6cf8b
--- /dev/null
@@ -0,0 +1,53 @@
+# AmbDec configuration
+# Written by Ambisonic Decoder Toolbox, version 8.0
+
+# input channel order: W Y Z X 
+
+/description     11_1_1h1v_allrad_5200_rE_max_1_band
+
+/version               3
+
+/dec/chan_mask         f
+/dec/freq_bands        1
+/dec/speakers          11
+/dec/coeff_scale       n3d
+
+/opt/input_scale       n3d
+/opt/nfeff_comp        output
+/opt/delay_comp        on
+/opt/level_comp        on
+/opt/xover_freq        400.000000
+/opt/xover_ratio       0.000000
+
+/speakers/{
+#              id       dist            azim            elev           conn
+#-----------------------------------------------------------------------
+add_spkr       LF       1.000000         30.000000       0.000000
+add_spkr       RF       1.000000        -30.000000       0.000000
+add_spkr       CE       1.000000          0.000000       0.000000
+add_spkr       LS       1.000000         90.000000       0.000000
+add_spkr       RS       1.000000        -90.000000       0.000000
+add_spkr       LB       1.000000        150.000000       0.000000
+add_spkr       RB       1.000000       -150.000000       0.000000
+add_spkr       LFT      1.000000         45.000000      35.000000
+add_spkr       RFT      1.000000        -45.000000      35.000000
+add_spkr       LBT      1.000000        135.000000      35.000000
+add_spkr       RBT      1.000000       -135.000000      35.000000
+/}
+
+/matrix/{
+order_gain     1.00000000e+00  1.00000000e+00  0.000000        0.000000
+add_row         1.27149251e-01  7.63047539e-02 -3.64373750e-02  1.59700680e-01
+add_row         1.07005418e-01 -7.67638760e-02 -4.92129762e-02  1.29012797e-01
+add_row         0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
+add_row         1.26400196e-01  1.77494694e-01 -3.71203389e-02  0.00000000e+00
+add_row         1.26396516e-01 -1.77488059e-01 -3.71297878e-02  0.00000000e+00
+add_row         1.06996956e-01  7.67615256e-02 -4.92166307e-02 -1.29001640e-01
+add_row         1.27145671e-01 -7.63003471e-02 -3.64353304e-02 -1.59697510e-01
+add_row         8.80919747e-02  7.48940670e-02  9.08786244e-02  6.22527183e-02
+add_row         1.57880745e-01 -7.28755272e-02  1.82364187e-01  8.74240284e-02
+add_row         1.57892225e-01  7.28944768e-02  1.82363474e-01 -8.74301086e-02
+add_row         8.80892603e-02 -7.48948724e-02  9.08779842e-02 -6.22480443e-02
+/}
+
+/end
diff --git a/presets/hexagon.ambdec b/presets/hexagon.ambdec
new file mode 100644 (file)
index 0000000..d45f273
--- /dev/null
@@ -0,0 +1,51 @@
+# AmbDec configuration
+# Written by Ambisonic Decoder Toolbox, version 8.0
+
+/description     Hexagon_2h0p_pinv_match_rV_max_rE_2_band
+
+/version               3
+
+/dec/chan_mask         11b
+/dec/freq_bands        2
+/dec/speakers          6
+/dec/coeff_scale       fuma
+
+/opt/input_scale       fuma
+/opt/nfeff_comp        input
+/opt/delay_comp        on
+/opt/level_comp        on
+/opt/xover_freq        400.000000
+/opt/xover_ratio       0.000000
+
+/speakers/{
+#            id      dist     azim     elev     conn
+#-----------------------------------------------------------------------
+add_spkr       LF       1.000000        30.000000       0.000000
+add_spkr       RF       1.000000       -30.000000       0.000000
+add_spkr       RS       1.000000       -90.000000       0.000000
+add_spkr       RB       1.000000       -150.000000      0.000000
+add_spkr       LB       1.000000        150.000000      0.000000
+add_spkr       LS       1.000000        90.000000       0.000000
+/}
+
+/lfmatrix/{
+order_gain     1.000000        1.000000        1.000000        0.000000
+add_row         0.235702        0.166667        0.288675        0.288675        0.166667
+add_row         0.235702       -0.166667        0.288675       -0.288675        0.166667
+add_row         0.235702       -0.333333        0.000000       -0.000000       -0.333333
+add_row         0.235702       -0.166667       -0.288675        0.288675        0.166667
+add_row         0.235702        0.166667       -0.288675       -0.288675        0.166667
+add_row         0.235702        0.333333        0.000000       -0.000000       -0.333333
+/}
+
+/hfmatrix/{
+order_gain     1.414214        1.224745        0.707107        0.000000
+add_row         0.235702        0.166667        0.288675        0.288675        0.166667
+add_row         0.235702       -0.166667        0.288675       -0.288675        0.166667
+add_row         0.235702       -0.333333        0.000000       -0.000000       -0.333333
+add_row         0.235702       -0.166667       -0.288675        0.288675        0.166667
+add_row         0.235702        0.166667       -0.288675       -0.288675        0.166667
+add_row         0.235702        0.333333        0.000000       -0.000000       -0.333333
+/}
+
+/end
diff --git a/presets/itu5.1-nocenter.ambdec b/presets/itu5.1-nocenter.ambdec
new file mode 100644 (file)
index 0000000..23839d0
--- /dev/null
@@ -0,0 +1,46 @@
+# AmbDec configuration
+# Written by Ambisonic Decoder Toolbox, version 8.0
+
+# input channel order: WYXVU
+
+/description     itu50-noCenter_2h0p_allrad_5200_rE_max_1_band
+
+# Although unused in this configuration, the front-center is declared here so
+# that an appropriate distance may be set (for proper delaying or attenuating
+# of dialog and such which feed it directly). It otherwise does not contribute
+# to positional sound output.
+
+/version            3
+
+/dec/chan_mask      11b
+/dec/freq_bands     1
+/dec/speakers       5
+/dec/coeff_scale    fuma
+
+/opt/input_scale    fuma
+/opt/nfeff_comp     input
+/opt/delay_comp     on
+/opt/level_comp     on
+/opt/xover_freq     400.000000
+/opt/xover_ratio    0.000000
+
+/speakers/{
+#           id  dist         azim      elev      conn
+#-----------------------------------------------------------------------
+add_spkr    LS  1.000000   110.000000  0.000000  system:playback_3
+add_spkr    LF  1.000000    30.000000  0.000000  system:playback_1
+add_spkr    CE  1.000000     0.000000  0.000000  system:playback_5
+add_spkr    RF  1.000000   -30.000000  0.000000  system:playback_2
+add_spkr    RS  1.000000  -110.000000  0.000000  system:playback_4
+/}
+
+/matrix/{
+order_gain  1.00000000e+00 8.66025404e-01 5.00000000e-01 0.000000
+add_row  4.70934222e-01  3.78169605e-01 -4.00084750e-01 -8.22264454e-02 -4.43765986e-02
+add_row  2.66639870e-01  2.55418584e-01  3.32591390e-01  2.82949132e-01  8.16816772e-02
+add_row  0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
+add_row  2.66634915e-01 -2.55421639e-01  3.32586482e-01 -2.82947688e-01  8.16782588e-02
+add_row  4.70935891e-01 -3.78173080e-01 -4.00080588e-01  8.22279700e-02 -4.43716394e-02
+/}
+
+/end
diff --git a/presets/itu5.1.ambdec b/presets/itu5.1.ambdec
new file mode 100644 (file)
index 0000000..8f4b14e
--- /dev/null
@@ -0,0 +1,47 @@
+# AmbDec configuration
+
+/description     itu50_2h0p_idhoa
+
+/version               3
+
+/dec/chan_mask         11b
+/dec/freq_bands        2
+/dec/speakers          5
+/dec/coeff_scale       fuma
+
+/opt/input_scale       fuma
+/opt/nfeff_comp        output
+/opt/delay_comp        on
+/opt/level_comp        on
+/opt/xover_freq        400.000000
+/opt/xover_ratio       0.000000
+
+/speakers/{
+#           id     dist         azim       elev     conn
+#-----------------------------------------------------------------------
+add_spkr       LS       1.000000        110.000000      0.000000
+add_spkr       LF       1.000000         30.000000      0.000000
+add_spkr       CE       1.000000          0.000000      0.000000
+add_spkr       RF       1.000000        -30.000000      0.000000
+add_spkr       RS       1.000000       -110.000000      0.000000
+/}
+
+/lfmatrix/{
+order_gain     1.000000        1.000000        1.000000        0.000000
+add_row         4.9010985e-1  3.7730501e-1 -3.7310699e-1 -1.2591453e-1  1.4513300e-2
+add_row         1.4908573e-1  3.0356168e-1  1.5329006e-1  2.4511248e-1 -1.5075313e-1
+add_row         1.3765492e-1  0.0000000e+0  4.4941794e-1  0.0000000e+0  2.5784407e-1
+add_row         1.4908573e-1 -3.0356168e-1  1.5329006e-1 -2.4511248e-1 -1.5075313e-1
+add_row         4.9010985e-1 -3.7730501e-1 -3.7310699e-1  1.2591453e-1  1.4513300e-2
+/}
+
+/hfmatrix/{
+order_gain     1.000000        1.000000        1.000000        0.000000
+add_row         5.6731600e-1  4.2292000e-1 -3.1549500e-1 -6.3449000e-2 -2.9238000e-2
+add_row         3.6858400e-1  2.7234900e-1  3.2161600e-1  1.9264500e-1  4.8260000e-2
+add_row         1.8357900e-1  0.0000000e+0  1.9958800e-1  0.0000000e+0  9.6282000e-2
+add_row         3.6858400e-1 -2.7234900e-1  3.2161600e-1 -1.9264500e-1  4.8260000e-2
+add_row         5.6731600e-1 -4.2292000e-1 -3.1549500e-1  6.3449000e-2 -2.9238000e-2
+/}
+
+/end
diff --git a/presets/presets.txt b/presets/presets.txt
new file mode 100644 (file)
index 0000000..f75a53e
--- /dev/null
@@ -0,0 +1,54 @@
+Ambisonic decoder configuration presets are provided here for common surround
+sound speaker layouts. The presets are prepared to work with OpenAL Soft's high
+quality decoder. By default all of the speaker distances within a preset are
+set to the same value, which results in no effect from distance compensation.
+If this doesn't match your physical speaker setup, it may be worth copying the
+preset and modifying the distance values to match (note that modifying the
+azimuth and elevation values in the presets will not have any effect; the
+specified angles do not change the decoder behavior).
+
+Details of the individual presets are as follows.
+
+square.ambdec
+Specifies a basic square speaker setup for Quadraphonic output, with identical
+width and depth. Front speakers are placed at +45 and -45 degrees, and back
+speakers are placed at +135 and -135 degrees.
+
+rectangle.ambdec
+Specifies a narrower speaker setup for Quadraphonic output, with a little less
+width but a little more depth over a basic square setup. Front speakers are
+placed at +30 and -30 degrees, providing a bit more compatibility for existing
+stereo content, with back speakers at +150 and -150 degrees.
+
+itu5.1.ambdec
+Specifies a standard ITU 5.0/5.1 setup for 5.1 Surround output. The front-
+center speaker is placed directly in front at 0 degrees, with the front-left
+and front-right at +30 and -30 degrees, and the surround speakers (side or
+back) at +110 and -110 degrees.
+
+hexagon.ambdec
+Specifies a flat-front hexagonal speaker setup for 7.1 Surround output. The
+front left and right speakers are placed at +30 and -30 degrees, the side
+speakers are placed at +90 and -90 degrees, and the back speakers are placed at
++150 and -150 degrees. Although this is for 7.1 output, no front-center speaker
+is defined for the decoder, meaning that speaker will be silent for 3D sound
+(however it may still be used with AL_SOFT_direct_channels or ALC_EXT_DEDICATED
+output). A "proper" 7.1 decoder may be provided in the future, but due to the
+nature of the speaker configuration will have trade-offs.
+
+hex-quad.ambdec
+Specifies a flat-front hexagonal speaker setup, plus an elevated quad speaker
+setup, for 7.1.4 Surround output. The front left and right speakers are placed
+at +30 and -30 degrees, the side speakers are placed at +90 and -90 degrees,
+and the back speakers are placed at +150 and -150 degrees. The elevated
+speakers are placed at an elevation of +35 degrees, with the top front left and
+right speakers placed at +45 and -45 degrees, and the top back left and right
+speakers placed at +135 and -135 degrees. Similar to 7.1, the front-center
+speaker is not used for 3D sound, but will be used as appropriate with
+AL_SOFT_direct_channels or ALC_EXT_DEDICATED.
+
+3D7.1.ambdec
+Specifies a 3D7.1 speaker setup for 3D7.1 Surround output. Please see
+docs/3D7.1.txt for information about speaker placement. Similar to 7.1, the
+front-center speaker is not used for 3D sound, but will be used as appropriate
+with AL_SOFT_direct_channels or ALC_EXT_DEDICATED.
diff --git a/presets/rectangle.ambdec b/presets/rectangle.ambdec
new file mode 100644 (file)
index 0000000..caf7231
--- /dev/null
@@ -0,0 +1,45 @@
+# AmbDec configuration
+# Written by Ambisonic Decoder Toolbox, version 8.0
+
+/description     Rectangle_1h0p_pinv_match_rV_max_rE_2_band
+
+/version               3
+
+/dec/chan_mask         b
+/dec/freq_bands        2
+/dec/speakers          4
+/dec/coeff_scale       fuma
+
+/opt/input_scale       fuma
+/opt/nfeff_comp        input
+/opt/delay_comp        on
+/opt/level_comp        on
+/opt/xover_freq        400.000000
+/opt/xover_ratio       0.000000
+
+/speakers/{
+#            id      dist     azim     elev     conn
+#-----------------------------------------------------------------------
+add_spkr       LF       1.000000        30.000000       0.000000
+add_spkr       RF       1.000000       -30.000000       0.000000
+add_spkr       RB       1.000000       -150.000000      0.000000
+add_spkr       LB       1.000000        150.000000      0.000000
+/}
+
+/lfmatrix/{
+order_gain     1.000000        1.000000        0.000000        0.000000
+add_row         0.353553        0.500000        0.288675
+add_row         0.353553       -0.500000        0.288675
+add_row         0.353553       -0.500000       -0.288675
+add_row         0.353553        0.500000       -0.288675
+/}
+
+/hfmatrix/{
+order_gain     1.414214        1.000000        0.000000        0.000000
+add_row         0.353553        0.500000        0.288675
+add_row         0.353553       -0.500000        0.288675
+add_row         0.353553       -0.500000       -0.288675
+add_row         0.353553        0.500000       -0.288675
+/}
+
+/end
diff --git a/presets/square.ambdec b/presets/square.ambdec
new file mode 100644 (file)
index 0000000..547ed36
--- /dev/null
@@ -0,0 +1,45 @@
+# AmbDec configuration
+# Written by Ambisonic Decoder Toolbox, version 8.0
+
+/description     Square_1h0p_pinv_match_rV_max_rE_2_band
+
+/version               3
+
+/dec/chan_mask         b
+/dec/freq_bands        2
+/dec/speakers          4
+/dec/coeff_scale       fuma
+
+/opt/input_scale       fuma
+/opt/nfeff_comp        input
+/opt/delay_comp        on
+/opt/level_comp        on
+/opt/xover_freq        400.000000
+/opt/xover_ratio       0.000000
+
+/speakers/{
+#            id      dist     azim     elev     conn
+#-----------------------------------------------------------------------
+add_spkr       LF       1.000000        45.000000       0.000000
+add_spkr       RF       1.000000       -45.000000       0.000000
+add_spkr       RB       1.000000       -135.000000      0.000000
+add_spkr       LB       1.000000        135.000000      0.000000
+/}
+
+/lfmatrix/{
+order_gain     1.000000        1.000000        0.000000        0.000000
+add_row         0.353553        0.353553        0.353553
+add_row         0.353553       -0.353553        0.353553
+add_row         0.353553       -0.353553       -0.353553
+add_row         0.353553        0.353553       -0.353553
+/}
+
+/hfmatrix/{
+order_gain     1.414214        1.000000        0.000000        0.000000
+add_row         0.353553        0.353553        0.353553
+add_row         0.353553       -0.353553        0.353553
+add_row         0.353553       -0.353553       -0.353553
+add_row         0.353553        0.353553       -0.353553
+/}
+
+/end
diff --git a/resources/openal32.rc b/resources/openal32.rc
new file mode 100644 (file)
index 0000000..1d7b9aa
--- /dev/null
@@ -0,0 +1,90 @@
+#pragma code_page(65001)
+// Microsoft Visual C++ generated resource script.
+//
+#include <windows.h>
+#include "resource.h"
+#include "version.h"
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE 
+BEGIN
+    "resource.h\0"
+END
+
+2 TEXTINCLUDE 
+BEGIN
+    "\0"
+END
+
+3 TEXTINCLUDE 
+BEGIN
+    "\r\n"
+    "\0"
+END
+
+#endif    // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION ALSOFT_VERSION_NUM
+ PRODUCTVERSION ALSOFT_VERSION_NUM
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS_NT_WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+                       VALUE "CompanyName", ""
+            VALUE "FileDescription", "Main implementation library"
+            VALUE "FileVersion", ALSOFT_VERSION
+            VALUE "InternalName", "OpenAL32.dll"
+            VALUE "LegalCopyright", "GNU LGPL - Version 2, June 1991"
+            VALUE "OriginalFilename", "OpenAL32.dll"
+            VALUE "ProductName", "OpenAL Soft"
+            VALUE "ProductVersion", ALSOFT_VERSION
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END
+
+#endif    // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif    // not APSTUDIO_INVOKED
+
diff --git a/resources/resource.h b/resources/resource.h
new file mode 100644 (file)
index 0000000..287c911
--- /dev/null
@@ -0,0 +1,15 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by openal32.rc, router.rc, soft_oal.rc
+//
+
+// Next default values for new objects
+// 
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE        101
+#define _APS_NEXT_COMMAND_VALUE         40001
+#define _APS_NEXT_CONTROL_VALUE         1000
+#define _APS_NEXT_SYMED_VALUE           101
+#endif
+#endif
diff --git a/resources/router.rc b/resources/router.rc
new file mode 100644 (file)
index 0000000..5292039
--- /dev/null
@@ -0,0 +1,90 @@
+#pragma code_page(65001)
+// Microsoft Visual C++ generated resource script.
+//
+#include <windows.h>
+#include "resource.h"
+#include "version.h"
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE 
+BEGIN
+    "resource.h\0"
+END
+
+2 TEXTINCLUDE 
+BEGIN
+    "\0"
+END
+
+3 TEXTINCLUDE 
+BEGIN
+    "\r\n"
+    "\0"
+END
+
+#endif    // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION ALSOFT_VERSION_NUM
+ PRODUCTVERSION ALSOFT_VERSION_NUM
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS_NT_WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+                       VALUE "CompanyName", ""
+            VALUE "FileDescription", "Router library"
+            VALUE "FileVersion", ALSOFT_VERSION
+            VALUE "InternalName", "OpenAL32.dll"
+            VALUE "LegalCopyright", "GNU LGPL - Version 2, June 1991"
+            VALUE "OriginalFilename", "OpenAL32.dll"
+            VALUE "ProductName", "OpenAL Soft"
+            VALUE "ProductVersion", ALSOFT_VERSION
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END
+
+#endif    // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif    // not APSTUDIO_INVOKED
+
diff --git a/resources/soft_oal.rc b/resources/soft_oal.rc
new file mode 100644 (file)
index 0000000..91e1750
--- /dev/null
@@ -0,0 +1,90 @@
+#pragma code_page(65001)
+// Microsoft Visual C++ generated resource script.
+//
+#include <windows.h>
+#include "resource.h"
+#include "version.h"
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE 
+BEGIN
+    "resource.h\0"
+END
+
+2 TEXTINCLUDE 
+BEGIN
+    "\0"
+END
+
+3 TEXTINCLUDE 
+BEGIN
+    "\r\n"
+    "\0"
+END
+
+#endif    // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION ALSOFT_VERSION_NUM
+ PRODUCTVERSION ALSOFT_VERSION_NUM
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS_NT_WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+                       VALUE "CompanyName", ""
+            VALUE "FileDescription", "Main implementation library"
+            VALUE "FileVersion", ALSOFT_VERSION
+            VALUE "InternalName", "soft_oal.dll"
+            VALUE "LegalCopyright", "GNU LGPL - Version 2, June 1991"
+            VALUE "OriginalFilename", "soft_oal.dll"
+            VALUE "ProductName", "OpenAL Soft"
+            VALUE "ProductVersion", ALSOFT_VERSION
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END
+
+#endif    // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif    // not APSTUDIO_INVOKED
+
diff --git a/router/al.cpp b/router/al.cpp
new file mode 100644 (file)
index 0000000..06c314e
--- /dev/null
@@ -0,0 +1,171 @@
+
+#include "config.h"
+
+#include <stddef.h>
+
+#include "AL/al.h"
+#include "router.h"
+
+
+std::atomic<DriverIface*> CurrentCtxDriver{nullptr};
+
+#define DECL_THUNK1(R,n,T1) AL_API R AL_APIENTRY n(T1 a)                      \
+{                                                                             \
+    DriverIface *iface = GetThreadDriver();                                   \
+    if(!iface) iface = CurrentCtxDriver.load(std::memory_order_acquire);      \
+    return iface->n(a);                                                       \
+}
+#define DECL_THUNK2(R,n,T1,T2) AL_API R AL_APIENTRY n(T1 a, T2 b) \
+{                                                                             \
+    DriverIface *iface = GetThreadDriver();                                   \
+    if(!iface) iface = CurrentCtxDriver.load(std::memory_order_acquire);      \
+    return iface->n(a, b);                                                    \
+}
+#define DECL_THUNK3(R,n,T1,T2,T3) AL_API R AL_APIENTRY n(T1 a, T2 b, T3 c) \
+{                                                                             \
+    DriverIface *iface = GetThreadDriver();                                   \
+    if(!iface) iface = CurrentCtxDriver.load(std::memory_order_acquire);      \
+    return iface->n(a, b, c);                                                 \
+}
+#define DECL_THUNK4(R,n,T1,T2,T3,T4) AL_API R AL_APIENTRY n(T1 a, T2 b, T3 c, T4 d) \
+{                                                                             \
+    DriverIface *iface = GetThreadDriver();                                   \
+    if(!iface) iface = CurrentCtxDriver.load(std::memory_order_acquire);      \
+    return iface->n(a, b, c, d);                                              \
+}
+#define DECL_THUNK5(R,n,T1,T2,T3,T4,T5) AL_API R AL_APIENTRY n(T1 a, T2 b, T3 c, T4 d, T5 e) \
+{                                                                             \
+    DriverIface *iface = GetThreadDriver();                                   \
+    if(!iface) iface = CurrentCtxDriver.load(std::memory_order_acquire);      \
+    return iface->n(a, b, c, d, e);                                           \
+}
+
+
+/* Ugly hack for some apps calling alGetError without a current context, and
+ * expecting it to be AL_NO_ERROR.
+ */
+AL_API ALenum AL_APIENTRY alGetError(void)
+{
+    DriverIface *iface = GetThreadDriver();
+    if(!iface) iface = CurrentCtxDriver.load(std::memory_order_acquire);
+    return iface ? iface->alGetError() : AL_NO_ERROR;
+}
+
+
+DECL_THUNK1(void, alDopplerFactor, ALfloat)
+DECL_THUNK1(void, alDopplerVelocity, ALfloat)
+DECL_THUNK1(void, alSpeedOfSound, ALfloat)
+DECL_THUNK1(void, alDistanceModel, ALenum)
+
+DECL_THUNK1(void, alEnable, ALenum)
+DECL_THUNK1(void, alDisable, ALenum)
+DECL_THUNK1(ALboolean, alIsEnabled, ALenum)
+
+DECL_THUNK1(const ALchar*, alGetString, ALenum)
+DECL_THUNK2(void, alGetBooleanv, ALenum, ALboolean*)
+DECL_THUNK2(void, alGetIntegerv, ALenum, ALint*)
+DECL_THUNK2(void, alGetFloatv, ALenum, ALfloat*)
+DECL_THUNK2(void, alGetDoublev, ALenum, ALdouble*)
+DECL_THUNK1(ALboolean, alGetBoolean, ALenum)
+DECL_THUNK1(ALint, alGetInteger, ALenum)
+DECL_THUNK1(ALfloat, alGetFloat, ALenum)
+DECL_THUNK1(ALdouble, alGetDouble, ALenum)
+
+DECL_THUNK1(ALboolean, alIsExtensionPresent, const ALchar*)
+DECL_THUNK1(void*, alGetProcAddress, const ALchar*)
+DECL_THUNK1(ALenum, alGetEnumValue, const ALchar*)
+
+DECL_THUNK2(void, alListenerf, ALenum, ALfloat)
+DECL_THUNK4(void, alListener3f, ALenum, ALfloat, ALfloat, ALfloat)
+DECL_THUNK2(void, alListenerfv, ALenum, const ALfloat*)
+DECL_THUNK2(void, alListeneri, ALenum, ALint)
+DECL_THUNK4(void, alListener3i, ALenum, ALint, ALint, ALint)
+DECL_THUNK2(void, alListeneriv, ALenum, const ALint*)
+DECL_THUNK2(void, alGetListenerf, ALenum, ALfloat*)
+DECL_THUNK4(void, alGetListener3f, ALenum, ALfloat*, ALfloat*, ALfloat*)
+DECL_THUNK2(void, alGetListenerfv, ALenum, ALfloat*)
+DECL_THUNK2(void, alGetListeneri, ALenum, ALint*)
+DECL_THUNK4(void, alGetListener3i, ALenum, ALint*, ALint*, ALint*)
+DECL_THUNK2(void, alGetListeneriv, ALenum, ALint*)
+
+DECL_THUNK2(void, alGenSources, ALsizei, ALuint*)
+DECL_THUNK2(void, alDeleteSources, ALsizei, const ALuint*)
+DECL_THUNK1(ALboolean, alIsSource, ALuint)
+DECL_THUNK3(void, alSourcef, ALuint, ALenum, ALfloat)
+DECL_THUNK5(void, alSource3f, ALuint, ALenum, ALfloat, ALfloat, ALfloat)
+DECL_THUNK3(void, alSourcefv, ALuint, ALenum, const ALfloat*)
+DECL_THUNK3(void, alSourcei, ALuint, ALenum, ALint)
+DECL_THUNK5(void, alSource3i, ALuint, ALenum, ALint, ALint, ALint)
+DECL_THUNK3(void, alSourceiv, ALuint, ALenum, const ALint*)
+DECL_THUNK3(void, alGetSourcef, ALuint, ALenum, ALfloat*)
+DECL_THUNK5(void, alGetSource3f, ALuint, ALenum, ALfloat*, ALfloat*, ALfloat*)
+DECL_THUNK3(void, alGetSourcefv, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetSourcei, ALuint, ALenum, ALint*)
+DECL_THUNK5(void, alGetSource3i, ALuint, ALenum, ALint*, ALint*, ALint*)
+DECL_THUNK3(void, alGetSourceiv, ALuint, ALenum, ALint*)
+DECL_THUNK2(void, alSourcePlayv, ALsizei, const ALuint*)
+DECL_THUNK2(void, alSourceStopv, ALsizei, const ALuint*)
+DECL_THUNK2(void, alSourceRewindv, ALsizei, const ALuint*)
+DECL_THUNK2(void, alSourcePausev, ALsizei, const ALuint*)
+DECL_THUNK1(void, alSourcePlay, ALuint)
+DECL_THUNK1(void, alSourceStop, ALuint)
+DECL_THUNK1(void, alSourceRewind, ALuint)
+DECL_THUNK1(void, alSourcePause, ALuint)
+DECL_THUNK3(void, alSourceQueueBuffers, ALuint, ALsizei, const ALuint*)
+DECL_THUNK3(void, alSourceUnqueueBuffers, ALuint, ALsizei, ALuint*)
+
+DECL_THUNK2(void, alGenBuffers, ALsizei, ALuint*)
+DECL_THUNK2(void, alDeleteBuffers, ALsizei, const ALuint*)
+DECL_THUNK1(ALboolean, alIsBuffer, ALuint)
+DECL_THUNK3(void, alBufferf, ALuint, ALenum, ALfloat)
+DECL_THUNK5(void, alBuffer3f, ALuint, ALenum, ALfloat, ALfloat, ALfloat)
+DECL_THUNK3(void, alBufferfv, ALuint, ALenum, const ALfloat*)
+DECL_THUNK3(void, alBufferi, ALuint, ALenum, ALint)
+DECL_THUNK5(void, alBuffer3i, ALuint, ALenum, ALint, ALint, ALint)
+DECL_THUNK3(void, alBufferiv, ALuint, ALenum, const ALint*)
+DECL_THUNK3(void, alGetBufferf, ALuint, ALenum, ALfloat*)
+DECL_THUNK5(void, alGetBuffer3f, ALuint, ALenum, ALfloat*, ALfloat*, ALfloat*)
+DECL_THUNK3(void, alGetBufferfv, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetBufferi, ALuint, ALenum, ALint*)
+DECL_THUNK5(void, alGetBuffer3i, ALuint, ALenum, ALint*, ALint*, ALint*)
+DECL_THUNK3(void, alGetBufferiv, ALuint, ALenum, ALint*)
+DECL_THUNK5(void, alBufferData, ALuint, ALenum, const ALvoid*, ALsizei, ALsizei)
+
+/* EFX 1.0. Required here to be exported from libOpenAL32.dll.a/OpenAL32.lib
+ * with the router enabled.
+ */
+DECL_THUNK2(void, alGenFilters, ALsizei, ALuint*)
+DECL_THUNK2(void, alDeleteFilters, ALsizei, const ALuint*)
+DECL_THUNK1(ALboolean, alIsFilter, ALuint)
+DECL_THUNK3(void, alFilterf, ALuint, ALenum, ALfloat)
+DECL_THUNK3(void, alFilterfv, ALuint, ALenum, const ALfloat*)
+DECL_THUNK3(void, alFilteri, ALuint, ALenum, ALint)
+DECL_THUNK3(void, alFilteriv, ALuint, ALenum, const ALint*)
+DECL_THUNK3(void, alGetFilterf, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetFilterfv, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetFilteri, ALuint, ALenum, ALint*)
+DECL_THUNK3(void, alGetFilteriv, ALuint, ALenum, ALint*)
+
+DECL_THUNK2(void, alGenEffects, ALsizei, ALuint*)
+DECL_THUNK2(void, alDeleteEffects, ALsizei, const ALuint*)
+DECL_THUNK1(ALboolean, alIsEffect, ALuint)
+DECL_THUNK3(void, alEffectf, ALuint, ALenum, ALfloat)
+DECL_THUNK3(void, alEffectfv, ALuint, ALenum, const ALfloat*)
+DECL_THUNK3(void, alEffecti, ALuint, ALenum, ALint)
+DECL_THUNK3(void, alEffectiv, ALuint, ALenum, const ALint*)
+DECL_THUNK3(void, alGetEffectf, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetEffectfv, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetEffecti, ALuint, ALenum, ALint*)
+DECL_THUNK3(void, alGetEffectiv, ALuint, ALenum, ALint*)
+
+DECL_THUNK2(void, alGenAuxiliaryEffectSlots, ALsizei, ALuint*)
+DECL_THUNK2(void, alDeleteAuxiliaryEffectSlots, ALsizei, const ALuint*)
+DECL_THUNK1(ALboolean, alIsAuxiliaryEffectSlot, ALuint)
+DECL_THUNK3(void, alAuxiliaryEffectSlotf, ALuint, ALenum, ALfloat)
+DECL_THUNK3(void, alAuxiliaryEffectSlotfv, ALuint, ALenum, const ALfloat*)
+DECL_THUNK3(void, alAuxiliaryEffectSloti, ALuint, ALenum, ALint)
+DECL_THUNK3(void, alAuxiliaryEffectSlotiv, ALuint, ALenum, const ALint*)
+DECL_THUNK3(void, alGetAuxiliaryEffectSlotf, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetAuxiliaryEffectSlotfv, ALuint, ALenum, ALfloat*)
+DECL_THUNK3(void, alGetAuxiliaryEffectSloti, ALuint, ALenum, ALint*)
+DECL_THUNK3(void, alGetAuxiliaryEffectSlotiv, ALuint, ALenum, ALint*)
diff --git a/router/alc.cpp b/router/alc.cpp
new file mode 100644 (file)
index 0000000..3aa3382
--- /dev/null
@@ -0,0 +1,1017 @@
+
+#include "config.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <mutex>
+#include <algorithm>
+#include <array>
+
+#include "AL/alc.h"
+#include "alstring.h"
+#include "router.h"
+
+
+#define DECL(x) { #x, reinterpret_cast<void*>(x) }
+struct FuncExportEntry {
+    const char *funcName;
+    void *address;
+};
+static const std::array<FuncExportEntry,128> alcFunctions{{
+    DECL(alcCreateContext),
+    DECL(alcMakeContextCurrent),
+    DECL(alcProcessContext),
+    DECL(alcSuspendContext),
+    DECL(alcDestroyContext),
+    DECL(alcGetCurrentContext),
+    DECL(alcGetContextsDevice),
+    DECL(alcOpenDevice),
+    DECL(alcCloseDevice),
+    DECL(alcGetError),
+    DECL(alcIsExtensionPresent),
+    DECL(alcGetProcAddress),
+    DECL(alcGetEnumValue),
+    DECL(alcGetString),
+    DECL(alcGetIntegerv),
+    DECL(alcCaptureOpenDevice),
+    DECL(alcCaptureCloseDevice),
+    DECL(alcCaptureStart),
+    DECL(alcCaptureStop),
+    DECL(alcCaptureSamples),
+
+    DECL(alcSetThreadContext),
+    DECL(alcGetThreadContext),
+
+    DECL(alEnable),
+    DECL(alDisable),
+    DECL(alIsEnabled),
+    DECL(alGetString),
+    DECL(alGetBooleanv),
+    DECL(alGetIntegerv),
+    DECL(alGetFloatv),
+    DECL(alGetDoublev),
+    DECL(alGetBoolean),
+    DECL(alGetInteger),
+    DECL(alGetFloat),
+    DECL(alGetDouble),
+    DECL(alGetError),
+    DECL(alIsExtensionPresent),
+    DECL(alGetProcAddress),
+    DECL(alGetEnumValue),
+    DECL(alListenerf),
+    DECL(alListener3f),
+    DECL(alListenerfv),
+    DECL(alListeneri),
+    DECL(alListener3i),
+    DECL(alListeneriv),
+    DECL(alGetListenerf),
+    DECL(alGetListener3f),
+    DECL(alGetListenerfv),
+    DECL(alGetListeneri),
+    DECL(alGetListener3i),
+    DECL(alGetListeneriv),
+    DECL(alGenSources),
+    DECL(alDeleteSources),
+    DECL(alIsSource),
+    DECL(alSourcef),
+    DECL(alSource3f),
+    DECL(alSourcefv),
+    DECL(alSourcei),
+    DECL(alSource3i),
+    DECL(alSourceiv),
+    DECL(alGetSourcef),
+    DECL(alGetSource3f),
+    DECL(alGetSourcefv),
+    DECL(alGetSourcei),
+    DECL(alGetSource3i),
+    DECL(alGetSourceiv),
+    DECL(alSourcePlayv),
+    DECL(alSourceStopv),
+    DECL(alSourceRewindv),
+    DECL(alSourcePausev),
+    DECL(alSourcePlay),
+    DECL(alSourceStop),
+    DECL(alSourceRewind),
+    DECL(alSourcePause),
+    DECL(alSourceQueueBuffers),
+    DECL(alSourceUnqueueBuffers),
+    DECL(alGenBuffers),
+    DECL(alDeleteBuffers),
+    DECL(alIsBuffer),
+    DECL(alBufferData),
+    DECL(alBufferf),
+    DECL(alBuffer3f),
+    DECL(alBufferfv),
+    DECL(alBufferi),
+    DECL(alBuffer3i),
+    DECL(alBufferiv),
+    DECL(alGetBufferf),
+    DECL(alGetBuffer3f),
+    DECL(alGetBufferfv),
+    DECL(alGetBufferi),
+    DECL(alGetBuffer3i),
+    DECL(alGetBufferiv),
+    DECL(alDopplerFactor),
+    DECL(alDopplerVelocity),
+    DECL(alSpeedOfSound),
+    DECL(alDistanceModel),
+
+    /* EFX 1.0 */
+    DECL(alGenFilters),
+    DECL(alDeleteFilters),
+    DECL(alIsFilter),
+    DECL(alFilterf),
+    DECL(alFilterfv),
+    DECL(alFilteri),
+    DECL(alFilteriv),
+    DECL(alGetFilterf),
+    DECL(alGetFilterfv),
+    DECL(alGetFilteri),
+    DECL(alGetFilteriv),
+    DECL(alGenEffects),
+    DECL(alDeleteEffects),
+    DECL(alIsEffect),
+    DECL(alEffectf),
+    DECL(alEffectfv),
+    DECL(alEffecti),
+    DECL(alEffectiv),
+    DECL(alGetEffectf),
+    DECL(alGetEffectfv),
+    DECL(alGetEffecti),
+    DECL(alGetEffectiv),
+    DECL(alGenAuxiliaryEffectSlots),
+    DECL(alDeleteAuxiliaryEffectSlots),
+    DECL(alIsAuxiliaryEffectSlot),
+    DECL(alAuxiliaryEffectSlotf),
+    DECL(alAuxiliaryEffectSlotfv),
+    DECL(alAuxiliaryEffectSloti),
+    DECL(alAuxiliaryEffectSlotiv),
+    DECL(alGetAuxiliaryEffectSlotf),
+    DECL(alGetAuxiliaryEffectSlotfv),
+    DECL(alGetAuxiliaryEffectSloti),
+    DECL(alGetAuxiliaryEffectSlotiv),
+}};
+#undef DECL
+
+#define DECL(x) { #x, (x) }
+struct EnumExportEntry {
+    const ALCchar *enumName;
+    ALCenum value;
+};
+static const std::array<EnumExportEntry,92> alcEnumerations{{
+    DECL(ALC_INVALID),
+    DECL(ALC_FALSE),
+    DECL(ALC_TRUE),
+
+    DECL(ALC_MAJOR_VERSION),
+    DECL(ALC_MINOR_VERSION),
+    DECL(ALC_ATTRIBUTES_SIZE),
+    DECL(ALC_ALL_ATTRIBUTES),
+    DECL(ALC_DEFAULT_DEVICE_SPECIFIER),
+    DECL(ALC_DEVICE_SPECIFIER),
+    DECL(ALC_ALL_DEVICES_SPECIFIER),
+    DECL(ALC_DEFAULT_ALL_DEVICES_SPECIFIER),
+    DECL(ALC_EXTENSIONS),
+    DECL(ALC_FREQUENCY),
+    DECL(ALC_REFRESH),
+    DECL(ALC_SYNC),
+    DECL(ALC_MONO_SOURCES),
+    DECL(ALC_STEREO_SOURCES),
+    DECL(ALC_CAPTURE_DEVICE_SPECIFIER),
+    DECL(ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER),
+    DECL(ALC_CAPTURE_SAMPLES),
+
+    DECL(ALC_NO_ERROR),
+    DECL(ALC_INVALID_DEVICE),
+    DECL(ALC_INVALID_CONTEXT),
+    DECL(ALC_INVALID_ENUM),
+    DECL(ALC_INVALID_VALUE),
+    DECL(ALC_OUT_OF_MEMORY),
+
+    DECL(AL_INVALID),
+    DECL(AL_NONE),
+    DECL(AL_FALSE),
+    DECL(AL_TRUE),
+
+    DECL(AL_SOURCE_RELATIVE),
+    DECL(AL_CONE_INNER_ANGLE),
+    DECL(AL_CONE_OUTER_ANGLE),
+    DECL(AL_PITCH),
+    DECL(AL_POSITION),
+    DECL(AL_DIRECTION),
+    DECL(AL_VELOCITY),
+    DECL(AL_LOOPING),
+    DECL(AL_BUFFER),
+    DECL(AL_GAIN),
+    DECL(AL_MIN_GAIN),
+    DECL(AL_MAX_GAIN),
+    DECL(AL_ORIENTATION),
+    DECL(AL_REFERENCE_DISTANCE),
+    DECL(AL_ROLLOFF_FACTOR),
+    DECL(AL_CONE_OUTER_GAIN),
+    DECL(AL_MAX_DISTANCE),
+    DECL(AL_SEC_OFFSET),
+    DECL(AL_SAMPLE_OFFSET),
+    DECL(AL_BYTE_OFFSET),
+    DECL(AL_SOURCE_TYPE),
+    DECL(AL_STATIC),
+    DECL(AL_STREAMING),
+    DECL(AL_UNDETERMINED),
+
+    DECL(AL_SOURCE_STATE),
+    DECL(AL_INITIAL),
+    DECL(AL_PLAYING),
+    DECL(AL_PAUSED),
+    DECL(AL_STOPPED),
+
+    DECL(AL_BUFFERS_QUEUED),
+    DECL(AL_BUFFERS_PROCESSED),
+
+    DECL(AL_FORMAT_MONO8),
+    DECL(AL_FORMAT_MONO16),
+    DECL(AL_FORMAT_STEREO8),
+    DECL(AL_FORMAT_STEREO16),
+
+    DECL(AL_FREQUENCY),
+    DECL(AL_BITS),
+    DECL(AL_CHANNELS),
+    DECL(AL_SIZE),
+
+    DECL(AL_UNUSED),
+    DECL(AL_PENDING),
+    DECL(AL_PROCESSED),
+
+    DECL(AL_NO_ERROR),
+    DECL(AL_INVALID_NAME),
+    DECL(AL_INVALID_ENUM),
+    DECL(AL_INVALID_VALUE),
+    DECL(AL_INVALID_OPERATION),
+    DECL(AL_OUT_OF_MEMORY),
+
+    DECL(AL_VENDOR),
+    DECL(AL_VERSION),
+    DECL(AL_RENDERER),
+    DECL(AL_EXTENSIONS),
+
+    DECL(AL_DOPPLER_FACTOR),
+    DECL(AL_DOPPLER_VELOCITY),
+    DECL(AL_DISTANCE_MODEL),
+    DECL(AL_SPEED_OF_SOUND),
+
+    DECL(AL_INVERSE_DISTANCE),
+    DECL(AL_INVERSE_DISTANCE_CLAMPED),
+    DECL(AL_LINEAR_DISTANCE),
+    DECL(AL_LINEAR_DISTANCE_CLAMPED),
+    DECL(AL_EXPONENT_DISTANCE),
+    DECL(AL_EXPONENT_DISTANCE_CLAMPED),
+}};
+#undef DECL
+
+static const ALCchar alcNoError[] = "No Error";
+static const ALCchar alcErrInvalidDevice[] = "Invalid Device";
+static const ALCchar alcErrInvalidContext[] = "Invalid Context";
+static const ALCchar alcErrInvalidEnum[] = "Invalid Enum";
+static const ALCchar alcErrInvalidValue[] = "Invalid Value";
+static const ALCchar alcErrOutOfMemory[] = "Out of Memory";
+static const ALCchar alcExtensionList[] =
+    "ALC_ENUMERATE_ALL_EXT ALC_ENUMERATION_EXT ALC_EXT_CAPTURE "
+    "ALC_EXT_thread_local_context";
+
+static const ALCint alcMajorVersion = 1;
+static const ALCint alcMinorVersion = 1;
+
+
+static std::recursive_mutex EnumerationLock;
+static std::mutex ContextSwitchLock;
+
+static std::atomic<ALCenum> LastError{ALC_NO_ERROR};
+static PtrIntMap DeviceIfaceMap;
+static PtrIntMap ContextIfaceMap;
+
+
+typedef struct EnumeratedList {
+    std::vector<ALCchar> Names;
+    std::vector<ALCint> Indicies;
+
+    void clear()
+    {
+        Names.clear();
+        Indicies.clear();
+    }
+} EnumeratedList;
+static EnumeratedList DevicesList;
+static EnumeratedList AllDevicesList;
+static EnumeratedList CaptureDevicesList;
+
+static void AppendDeviceList(EnumeratedList *list, const ALCchar *names, ALint idx)
+{
+    const ALCchar *name_end = names;
+    if(!name_end) return;
+
+    ALCsizei count = 0;
+    while(*name_end)
+    {
+        TRACE("Enumerated \"%s\", driver %d\n", name_end, idx);
+        ++count;
+        name_end += strlen(name_end)+1;
+    }
+    if(names == name_end)
+        return;
+
+    list->Names.reserve(list->Names.size() + (name_end - names) + 1);
+    list->Names.insert(list->Names.cend(), names, name_end);
+
+    list->Indicies.reserve(list->Indicies.size() + count);
+    list->Indicies.insert(list->Indicies.cend(), count, idx);
+}
+
+static ALint GetDriverIndexForName(const EnumeratedList *list, const ALCchar *name)
+{
+    const ALCchar *devnames = list->Names.data();
+    const ALCint *index = list->Indicies.data();
+
+    while(devnames && *devnames)
+    {
+        if(strcmp(name, devnames) == 0)
+            return *index;
+        devnames += strlen(devnames)+1;
+        index++;
+    }
+    return -1;
+}
+
+
+static void InitCtxFuncs(DriverIface &iface)
+{
+    ALCdevice *device{iface.alcGetContextsDevice(iface.alcGetCurrentContext())};
+
+#define LOAD_PROC(x) do {                                                     \
+    iface.x = reinterpret_cast<decltype(iface.x)>(iface.alGetProcAddress(#x));\
+    if(!iface.x)                                                              \
+        ERR("Failed to find entry point for %s in %ls\n", #x,                 \
+            iface.Name.c_str());                                              \
+} while(0)
+    if(iface.alcIsExtensionPresent(device, "ALC_EXT_EFX"))
+    {
+        LOAD_PROC(alGenFilters);
+        LOAD_PROC(alDeleteFilters);
+        LOAD_PROC(alIsFilter);
+        LOAD_PROC(alFilterf);
+        LOAD_PROC(alFilterfv);
+        LOAD_PROC(alFilteri);
+        LOAD_PROC(alFilteriv);
+        LOAD_PROC(alGetFilterf);
+        LOAD_PROC(alGetFilterfv);
+        LOAD_PROC(alGetFilteri);
+        LOAD_PROC(alGetFilteriv);
+        LOAD_PROC(alGenEffects);
+        LOAD_PROC(alDeleteEffects);
+        LOAD_PROC(alIsEffect);
+        LOAD_PROC(alEffectf);
+        LOAD_PROC(alEffectfv);
+        LOAD_PROC(alEffecti);
+        LOAD_PROC(alEffectiv);
+        LOAD_PROC(alGetEffectf);
+        LOAD_PROC(alGetEffectfv);
+        LOAD_PROC(alGetEffecti);
+        LOAD_PROC(alGetEffectiv);
+        LOAD_PROC(alGenAuxiliaryEffectSlots);
+        LOAD_PROC(alDeleteAuxiliaryEffectSlots);
+        LOAD_PROC(alIsAuxiliaryEffectSlot);
+        LOAD_PROC(alAuxiliaryEffectSlotf);
+        LOAD_PROC(alAuxiliaryEffectSlotfv);
+        LOAD_PROC(alAuxiliaryEffectSloti);
+        LOAD_PROC(alAuxiliaryEffectSlotiv);
+        LOAD_PROC(alGetAuxiliaryEffectSlotf);
+        LOAD_PROC(alGetAuxiliaryEffectSlotfv);
+        LOAD_PROC(alGetAuxiliaryEffectSloti);
+        LOAD_PROC(alGetAuxiliaryEffectSlotiv);
+    }
+#undef LOAD_PROC
+}
+
+
+ALC_API ALCdevice* ALC_APIENTRY alcOpenDevice(const ALCchar *devicename)
+{
+    ALCdevice *device = nullptr;
+    ALint idx = 0;
+
+    /* Prior to the enumeration extension, apps would hardcode these names as a
+     * quality hint for the wrapper driver. Ignore them since there's no sane
+     * way to map them.
+     */
+    if(devicename && (devicename[0] == '\0' ||
+                      strcmp(devicename, "DirectSound3D") == 0 ||
+                      strcmp(devicename, "DirectSound") == 0 ||
+                      strcmp(devicename, "MMSYSTEM") == 0))
+        devicename = nullptr;
+    if(devicename)
+    {
+        {
+            std::lock_guard<std::recursive_mutex> _{EnumerationLock};
+            if(DevicesList.Names.empty())
+                (void)alcGetString(nullptr, ALC_DEVICE_SPECIFIER);
+            idx = GetDriverIndexForName(&DevicesList, devicename);
+            if(idx < 0)
+            {
+                if(AllDevicesList.Names.empty())
+                    (void)alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
+                idx = GetDriverIndexForName(&AllDevicesList, devicename);
+            }
+        }
+
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_VALUE);
+            TRACE("Failed to find driver for name \"%s\"\n", devicename);
+            return nullptr;
+        }
+        TRACE("Found driver %d for name \"%s\"\n", idx, devicename);
+        device = DriverList[idx]->alcOpenDevice(devicename);
+    }
+    else
+    {
+        for(const auto &drv : DriverList)
+        {
+            if(drv->ALCVer >= MAKE_ALC_VER(1, 1)
+                || drv->alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT"))
+            {
+                TRACE("Using default device from driver %d\n", idx);
+                device = drv->alcOpenDevice(nullptr);
+                break;
+            }
+            ++idx;
+        }
+    }
+
+    if(device)
+    {
+        if(DeviceIfaceMap.insert(device, idx) != ALC_NO_ERROR)
+        {
+            DriverList[idx]->alcCloseDevice(device);
+            device = nullptr;
+        }
+    }
+
+    return device;
+}
+
+ALC_API ALCboolean ALC_APIENTRY alcCloseDevice(ALCdevice *device)
+{
+    ALint idx;
+
+    if(!device || (idx=DeviceIfaceMap.lookupByKey(device)) < 0)
+    {
+        LastError.store(ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+    if(!DriverList[idx]->alcCloseDevice(device))
+        return ALC_FALSE;
+    DeviceIfaceMap.removeByKey(device);
+    return ALC_TRUE;
+}
+
+
+ALC_API ALCcontext* ALC_APIENTRY alcCreateContext(ALCdevice *device, const ALCint *attrlist)
+{
+    ALCcontext *context;
+    ALint idx;
+
+    if(!device || (idx=DeviceIfaceMap.lookupByKey(device)) < 0)
+    {
+        LastError.store(ALC_INVALID_DEVICE);
+        return nullptr;
+    }
+    context = DriverList[idx]->alcCreateContext(device, attrlist);
+    if(context)
+    {
+        if(ContextIfaceMap.insert(context, idx) != ALC_NO_ERROR)
+        {
+            DriverList[idx]->alcDestroyContext(context);
+            context = nullptr;
+        }
+    }
+
+    return context;
+}
+
+ALC_API ALCboolean ALC_APIENTRY alcMakeContextCurrent(ALCcontext *context)
+{
+    ALint idx = -1;
+
+    std::lock_guard<std::mutex> _{ContextSwitchLock};
+    if(context)
+    {
+        idx = ContextIfaceMap.lookupByKey(context);
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_CONTEXT);
+            return ALC_FALSE;
+        }
+        if(!DriverList[idx]->alcMakeContextCurrent(context))
+            return ALC_FALSE;
+
+        auto do_init = [idx]() { InitCtxFuncs(*DriverList[idx]); };
+        std::call_once(DriverList[idx]->InitOnceCtx, do_init);
+    }
+
+    /* Unset the context from the old driver if it's different from the new
+     * current one.
+     */
+    if(idx < 0)
+    {
+        DriverIface *oldiface = GetThreadDriver();
+        if(oldiface) oldiface->alcSetThreadContext(nullptr);
+        oldiface = CurrentCtxDriver.exchange(nullptr);
+        if(oldiface) oldiface->alcMakeContextCurrent(nullptr);
+    }
+    else
+    {
+        DriverIface *oldiface = GetThreadDriver();
+        if(oldiface && oldiface != DriverList[idx].get())
+            oldiface->alcSetThreadContext(nullptr);
+        oldiface = CurrentCtxDriver.exchange(DriverList[idx].get());
+        if(oldiface && oldiface != DriverList[idx].get())
+            oldiface->alcMakeContextCurrent(nullptr);
+    }
+    SetThreadDriver(nullptr);
+
+    return ALC_TRUE;
+}
+
+ALC_API void ALC_APIENTRY alcProcessContext(ALCcontext *context)
+{
+    if(context)
+    {
+        ALint idx = ContextIfaceMap.lookupByKey(context);
+        if(idx >= 0)
+            return DriverList[idx]->alcProcessContext(context);
+    }
+    LastError.store(ALC_INVALID_CONTEXT);
+}
+
+ALC_API void ALC_APIENTRY alcSuspendContext(ALCcontext *context)
+{
+    if(context)
+    {
+        ALint idx = ContextIfaceMap.lookupByKey(context);
+        if(idx >= 0)
+            return DriverList[idx]->alcSuspendContext(context);
+    }
+    LastError.store(ALC_INVALID_CONTEXT);
+}
+
+ALC_API void ALC_APIENTRY alcDestroyContext(ALCcontext *context)
+{
+    ALint idx;
+
+    if(!context || (idx=ContextIfaceMap.lookupByKey(context)) < 0)
+    {
+        LastError.store(ALC_INVALID_CONTEXT);
+        return;
+    }
+
+    DriverList[idx]->alcDestroyContext(context);
+    ContextIfaceMap.removeByKey(context);
+}
+
+ALC_API ALCcontext* ALC_APIENTRY alcGetCurrentContext(void)
+{
+    DriverIface *iface = GetThreadDriver();
+    if(!iface) iface = CurrentCtxDriver.load();
+    return iface ? iface->alcGetCurrentContext() : nullptr;
+}
+
+ALC_API ALCdevice* ALC_APIENTRY alcGetContextsDevice(ALCcontext *context)
+{
+    if(context)
+    {
+        ALint idx = ContextIfaceMap.lookupByKey(context);
+        if(idx >= 0)
+            return DriverList[idx]->alcGetContextsDevice(context);
+    }
+    LastError.store(ALC_INVALID_CONTEXT);
+    return nullptr;
+}
+
+
+ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx < 0) return ALC_INVALID_DEVICE;
+        return DriverList[idx]->alcGetError(device);
+    }
+    return LastError.exchange(ALC_NO_ERROR);
+}
+
+ALC_API ALCboolean ALC_APIENTRY alcIsExtensionPresent(ALCdevice *device, const ALCchar *extname)
+{
+    const char *ptr;
+    size_t len;
+
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_DEVICE);
+            return ALC_FALSE;
+        }
+        return DriverList[idx]->alcIsExtensionPresent(device, extname);
+    }
+
+    len = strlen(extname);
+    ptr = alcExtensionList;
+    while(ptr && *ptr)
+    {
+        if(al::strncasecmp(ptr, extname, len) == 0 && (ptr[len] == '\0' || isspace(ptr[len])))
+            return ALC_TRUE;
+        if((ptr=strchr(ptr, ' ')) != nullptr)
+        {
+            do {
+                ++ptr;
+            } while(isspace(*ptr));
+        }
+    }
+    return ALC_FALSE;
+}
+
+ALC_API void* ALC_APIENTRY alcGetProcAddress(ALCdevice *device, const ALCchar *funcname)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_DEVICE);
+            return nullptr;
+        }
+        return DriverList[idx]->alcGetProcAddress(device, funcname);
+    }
+
+    auto iter = std::find_if(alcFunctions.cbegin(), alcFunctions.cend(),
+        [funcname](const FuncExportEntry &entry) -> bool
+        { return strcmp(funcname, entry.funcName) == 0; }
+    );
+    return (iter != alcFunctions.cend()) ? iter->address : nullptr;
+}
+
+ALC_API ALCenum ALC_APIENTRY alcGetEnumValue(ALCdevice *device, const ALCchar *enumname)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_DEVICE);
+            return 0;
+        }
+        return DriverList[idx]->alcGetEnumValue(device, enumname);
+    }
+
+    auto iter = std::find_if(alcEnumerations.cbegin(), alcEnumerations.cend(),
+        [enumname](const EnumExportEntry &entry) -> bool
+        { return strcmp(enumname, entry.enumName) == 0; }
+    );
+    return (iter != alcEnumerations.cend()) ? iter->value : 0;
+}
+
+ALC_API const ALCchar* ALC_APIENTRY alcGetString(ALCdevice *device, ALCenum param)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_DEVICE);
+            return nullptr;
+        }
+        return DriverList[idx]->alcGetString(device, param);
+    }
+
+    switch(param)
+    {
+    case ALC_NO_ERROR:
+        return alcNoError;
+    case ALC_INVALID_ENUM:
+        return alcErrInvalidEnum;
+    case ALC_INVALID_VALUE:
+        return alcErrInvalidValue;
+    case ALC_INVALID_DEVICE:
+        return alcErrInvalidDevice;
+    case ALC_INVALID_CONTEXT:
+        return alcErrInvalidContext;
+    case ALC_OUT_OF_MEMORY:
+        return alcErrOutOfMemory;
+    case ALC_EXTENSIONS:
+        return alcExtensionList;
+
+    case ALC_DEVICE_SPECIFIER:
+    {
+        std::lock_guard<std::recursive_mutex> _{EnumerationLock};
+        DevicesList.clear();
+        ALint idx{0};
+        for(const auto &drv : DriverList)
+        {
+            /* Only enumerate names from drivers that support it. */
+            if(drv->ALCVer >= MAKE_ALC_VER(1, 1)
+                || drv->alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT"))
+                AppendDeviceList(&DevicesList,
+                    drv->alcGetString(nullptr, ALC_DEVICE_SPECIFIER), idx);
+            ++idx;
+        }
+        /* Ensure the list is double-null termianted. */
+        if(DevicesList.Names.empty())
+            DevicesList.Names.emplace_back('\0');
+        DevicesList.Names.emplace_back('\0');
+        return DevicesList.Names.data();
+    }
+
+    case ALC_ALL_DEVICES_SPECIFIER:
+    {
+        std::lock_guard<std::recursive_mutex> _{EnumerationLock};
+        AllDevicesList.clear();
+        ALint idx{0};
+        for(const auto &drv : DriverList)
+        {
+            /* If the driver doesn't support ALC_ENUMERATE_ALL_EXT, substitute
+             * standard enumeration.
+             */
+            if(drv->alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"))
+                AppendDeviceList(&AllDevicesList,
+                    drv->alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER), idx);
+            else if(drv->ALCVer >= MAKE_ALC_VER(1, 1)
+                || drv->alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT"))
+                AppendDeviceList(&AllDevicesList,
+                    drv->alcGetString(nullptr, ALC_DEVICE_SPECIFIER), idx);
+            ++idx;
+        }
+        /* Ensure the list is double-null termianted. */
+        if(AllDevicesList.Names.empty())
+            AllDevicesList.Names.emplace_back('\0');
+        AllDevicesList.Names.emplace_back('\0');
+        return AllDevicesList.Names.data();
+    }
+
+    case ALC_CAPTURE_DEVICE_SPECIFIER:
+    {
+        std::lock_guard<std::recursive_mutex> _{EnumerationLock};
+        CaptureDevicesList.clear();
+        ALint idx{0};
+        for(const auto &drv : DriverList)
+        {
+            if(drv->ALCVer >= MAKE_ALC_VER(1, 1)
+                || drv->alcIsExtensionPresent(nullptr, "ALC_EXT_CAPTURE"))
+                AppendDeviceList(&CaptureDevicesList,
+                    drv->alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER), idx);
+            ++idx;
+        }
+        /* Ensure the list is double-null termianted. */
+        if(CaptureDevicesList.Names.empty())
+            CaptureDevicesList.Names.emplace_back('\0');
+        CaptureDevicesList.Names.emplace_back('\0');
+        return CaptureDevicesList.Names.data();
+    }
+
+    case ALC_DEFAULT_DEVICE_SPECIFIER:
+    {
+        for(const auto &drv : DriverList)
+        {
+            if(drv->ALCVer >= MAKE_ALC_VER(1, 1)
+                || drv->alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT"))
+                return drv->alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER);
+        }
+        return "";
+    }
+
+    case ALC_DEFAULT_ALL_DEVICES_SPECIFIER:
+    {
+        for(const auto &drv : DriverList)
+        {
+            if(drv->alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT") != ALC_FALSE)
+                return drv->alcGetString(nullptr, ALC_DEFAULT_ALL_DEVICES_SPECIFIER);
+        }
+        return "";
+    }
+
+    case ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER:
+    {
+        for(const auto &drv : DriverList)
+        {
+            if(drv->ALCVer >= MAKE_ALC_VER(1, 1)
+                || drv->alcIsExtensionPresent(nullptr, "ALC_EXT_CAPTURE"))
+                return drv->alcGetString(nullptr, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER);
+        }
+        return "";
+    }
+
+    default:
+        LastError.store(ALC_INVALID_ENUM);
+        break;
+    }
+    return nullptr;
+}
+
+ALC_API void ALC_APIENTRY alcGetIntegerv(ALCdevice *device, ALCenum param, ALCsizei size, ALCint *values)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_DEVICE);
+            return;
+        }
+        return DriverList[idx]->alcGetIntegerv(device, param, size, values);
+    }
+
+    if(size <= 0 || values == nullptr)
+    {
+        LastError.store(ALC_INVALID_VALUE);
+        return;
+    }
+
+    switch(param)
+    {
+        case ALC_MAJOR_VERSION:
+            if(size >= 1)
+            {
+                values[0] = alcMajorVersion;
+                return;
+            }
+            /*fall-through*/
+        case ALC_MINOR_VERSION:
+            if(size >= 1)
+            {
+                values[0] = alcMinorVersion;
+                return;
+            }
+            LastError.store(ALC_INVALID_VALUE);
+            return;
+
+        case ALC_ATTRIBUTES_SIZE:
+        case ALC_ALL_ATTRIBUTES:
+        case ALC_FREQUENCY:
+        case ALC_REFRESH:
+        case ALC_SYNC:
+        case ALC_MONO_SOURCES:
+        case ALC_STEREO_SOURCES:
+        case ALC_CAPTURE_SAMPLES:
+            LastError.store(ALC_INVALID_DEVICE);
+            return;
+
+        default:
+            LastError.store(ALC_INVALID_ENUM);
+            return;
+    }
+}
+
+
+ALC_API ALCdevice* ALC_APIENTRY alcCaptureOpenDevice(const ALCchar *devicename, ALCuint frequency, ALCenum format, ALCsizei buffersize)
+{
+    ALCdevice *device = nullptr;
+    ALint idx = 0;
+
+    if(devicename && devicename[0] == '\0')
+        devicename = nullptr;
+    if(devicename)
+    {
+        {
+            std::lock_guard<std::recursive_mutex> _{EnumerationLock};
+            if(CaptureDevicesList.Names.empty())
+                (void)alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER);
+            idx = GetDriverIndexForName(&CaptureDevicesList, devicename);
+        }
+
+        if(idx < 0)
+        {
+            LastError.store(ALC_INVALID_VALUE);
+            TRACE("Failed to find driver for name \"%s\"\n", devicename);
+            return nullptr;
+        }
+        TRACE("Found driver %d for name \"%s\"\n", idx, devicename);
+        device = DriverList[idx]->alcCaptureOpenDevice(devicename, frequency, format, buffersize);
+    }
+    else
+    {
+        for(const auto &drv : DriverList)
+        {
+            if(drv->ALCVer >= MAKE_ALC_VER(1, 1)
+                || drv->alcIsExtensionPresent(nullptr, "ALC_EXT_CAPTURE"))
+            {
+                TRACE("Using default capture device from driver %d\n", idx);
+                device = drv->alcCaptureOpenDevice(nullptr, frequency, format, buffersize);
+                break;
+            }
+            ++idx;
+        }
+    }
+
+    if(device)
+    {
+        if(DeviceIfaceMap.insert(device, idx) != ALC_NO_ERROR)
+        {
+            DriverList[idx]->alcCaptureCloseDevice(device);
+            device = nullptr;
+        }
+    }
+
+    return device;
+}
+
+ALC_API ALCboolean ALC_APIENTRY alcCaptureCloseDevice(ALCdevice *device)
+{
+    ALint idx;
+
+    if(!device || (idx=DeviceIfaceMap.lookupByKey(device)) < 0)
+    {
+        LastError.store(ALC_INVALID_DEVICE);
+        return ALC_FALSE;
+    }
+    if(!DriverList[idx]->alcCaptureCloseDevice(device))
+        return ALC_FALSE;
+    DeviceIfaceMap.removeByKey(device);
+    return ALC_TRUE;
+}
+
+ALC_API void ALC_APIENTRY alcCaptureStart(ALCdevice *device)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx >= 0)
+            return DriverList[idx]->alcCaptureStart(device);
+    }
+    LastError.store(ALC_INVALID_DEVICE);
+}
+
+ALC_API void ALC_APIENTRY alcCaptureStop(ALCdevice *device)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx >= 0)
+            return DriverList[idx]->alcCaptureStop(device);
+    }
+    LastError.store(ALC_INVALID_DEVICE);
+}
+
+ALC_API void ALC_APIENTRY alcCaptureSamples(ALCdevice *device, ALCvoid *buffer, ALCsizei samples)
+{
+    if(device)
+    {
+        ALint idx = DeviceIfaceMap.lookupByKey(device);
+        if(idx >= 0)
+            return DriverList[idx]->alcCaptureSamples(device, buffer, samples);
+    }
+    LastError.store(ALC_INVALID_DEVICE);
+}
+
+
+ALC_API ALCboolean ALC_APIENTRY alcSetThreadContext(ALCcontext *context)
+{
+    ALCenum err = ALC_INVALID_CONTEXT;
+    ALint idx;
+
+    if(!context)
+    {
+        DriverIface *oldiface = GetThreadDriver();
+        if(oldiface && !oldiface->alcSetThreadContext(nullptr))
+            return ALC_FALSE;
+        SetThreadDriver(nullptr);
+        return ALC_TRUE;
+    }
+
+    idx = ContextIfaceMap.lookupByKey(context);
+    if(idx >= 0)
+    {
+        if(DriverList[idx]->alcSetThreadContext(context))
+        {
+            auto do_init = [idx]() { InitCtxFuncs(*DriverList[idx]); };
+            std::call_once(DriverList[idx]->InitOnceCtx, do_init);
+
+            DriverIface *oldiface = GetThreadDriver();
+            if(oldiface != DriverList[idx].get())
+            {
+                SetThreadDriver(DriverList[idx].get());
+                if(oldiface) oldiface->alcSetThreadContext(nullptr);
+            }
+            return ALC_TRUE;
+        }
+        err = DriverList[idx]->alcGetError(nullptr);
+    }
+    LastError.store(err);
+    return ALC_FALSE;
+}
+
+ALC_API ALCcontext* ALC_APIENTRY alcGetThreadContext(void)
+{
+    DriverIface *iface = GetThreadDriver();
+    if(iface) return iface->alcGetThreadContext();
+    return nullptr;
+}
diff --git a/router/router.cpp b/router/router.cpp
new file mode 100644 (file)
index 0000000..3c89105
--- /dev/null
@@ -0,0 +1,457 @@
+
+#include "config.h"
+
+#include "router.h"
+
+#include <algorithm>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#include "AL/alc.h"
+#include "AL/al.h"
+
+#include "almalloc.h"
+#include "strutils.h"
+
+#include "version.h"
+
+
+std::vector<DriverIfacePtr> DriverList;
+
+thread_local DriverIface *ThreadCtxDriver;
+
+enum LogLevel LogLevel = LogLevel_Error;
+FILE *LogFile;
+
+#ifdef __MINGW32__
+DriverIface *GetThreadDriver() noexcept { return ThreadCtxDriver; }
+void SetThreadDriver(DriverIface *driver) noexcept { ThreadCtxDriver = driver; }
+#endif
+
+static void LoadDriverList(void);
+
+
+BOOL APIENTRY DllMain(HINSTANCE, DWORD reason, void*)
+{
+    switch(reason)
+    {
+    case DLL_PROCESS_ATTACH:
+        LogFile = stderr;
+        if(auto logfname = al::getenv("ALROUTER_LOGFILE"))
+        {
+            FILE *f = fopen(logfname->c_str(), "w");
+            if(f == nullptr)
+                ERR("Could not open log file: %s\n", logfname->c_str());
+            else
+                LogFile = f;
+        }
+        if(auto loglev = al::getenv("ALROUTER_LOGLEVEL"))
+        {
+            char *end = nullptr;
+            long l = strtol(loglev->c_str(), &end, 0);
+            if(!end || *end != '\0')
+                ERR("Invalid log level value: %s\n", loglev->c_str());
+            else if(l < LogLevel_None || l > LogLevel_Trace)
+                ERR("Log level out of range: %s\n", loglev->c_str());
+            else
+                LogLevel = static_cast<enum LogLevel>(l);
+        }
+        TRACE("Initializing router v0.1-%s %s\n", ALSOFT_GIT_COMMIT_HASH, ALSOFT_GIT_BRANCH);
+        LoadDriverList();
+
+        break;
+
+    case DLL_THREAD_ATTACH:
+        break;
+    case DLL_THREAD_DETACH:
+        break;
+
+    case DLL_PROCESS_DETACH:
+        DriverList.clear();
+
+        if(LogFile && LogFile != stderr)
+            fclose(LogFile);
+        LogFile = nullptr;
+
+        break;
+    }
+    return TRUE;
+}
+
+
+static void AddModule(HMODULE module, const WCHAR *name)
+{
+    for(auto &drv : DriverList)
+    {
+        if(drv->Module == module)
+        {
+            TRACE("Skipping already-loaded module %p\n", decltype(std::declval<void*>()){module});
+            FreeLibrary(module);
+            return;
+        }
+        if(drv->Name == name)
+        {
+            TRACE("Skipping similarly-named module %ls\n", name);
+            FreeLibrary(module);
+            return;
+        }
+    }
+
+    DriverList.emplace_back(std::make_unique<DriverIface>(name, module));
+    DriverIface &newdrv = *DriverList.back();
+
+    /* Load required functions. */
+    int err = 0;
+#define LOAD_PROC(x) do {                                                     \
+    newdrv.x = reinterpret_cast<decltype(newdrv.x)>(reinterpret_cast<void*>(  \
+        GetProcAddress(module, #x)));                                         \
+    if(!newdrv.x)                                                             \
+    {                                                                         \
+        ERR("Failed to find entry point for %s in %ls\n", #x, name);          \
+        err = 1;                                                              \
+    }                                                                         \
+} while(0)
+    LOAD_PROC(alcCreateContext);
+    LOAD_PROC(alcMakeContextCurrent);
+    LOAD_PROC(alcProcessContext);
+    LOAD_PROC(alcSuspendContext);
+    LOAD_PROC(alcDestroyContext);
+    LOAD_PROC(alcGetCurrentContext);
+    LOAD_PROC(alcGetContextsDevice);
+    LOAD_PROC(alcOpenDevice);
+    LOAD_PROC(alcCloseDevice);
+    LOAD_PROC(alcGetError);
+    LOAD_PROC(alcIsExtensionPresent);
+    LOAD_PROC(alcGetProcAddress);
+    LOAD_PROC(alcGetEnumValue);
+    LOAD_PROC(alcGetString);
+    LOAD_PROC(alcGetIntegerv);
+    LOAD_PROC(alcCaptureOpenDevice);
+    LOAD_PROC(alcCaptureCloseDevice);
+    LOAD_PROC(alcCaptureStart);
+    LOAD_PROC(alcCaptureStop);
+    LOAD_PROC(alcCaptureSamples);
+
+    LOAD_PROC(alEnable);
+    LOAD_PROC(alDisable);
+    LOAD_PROC(alIsEnabled);
+    LOAD_PROC(alGetString);
+    LOAD_PROC(alGetBooleanv);
+    LOAD_PROC(alGetIntegerv);
+    LOAD_PROC(alGetFloatv);
+    LOAD_PROC(alGetDoublev);
+    LOAD_PROC(alGetBoolean);
+    LOAD_PROC(alGetInteger);
+    LOAD_PROC(alGetFloat);
+    LOAD_PROC(alGetDouble);
+    LOAD_PROC(alGetError);
+    LOAD_PROC(alIsExtensionPresent);
+    LOAD_PROC(alGetProcAddress);
+    LOAD_PROC(alGetEnumValue);
+    LOAD_PROC(alListenerf);
+    LOAD_PROC(alListener3f);
+    LOAD_PROC(alListenerfv);
+    LOAD_PROC(alListeneri);
+    LOAD_PROC(alListener3i);
+    LOAD_PROC(alListeneriv);
+    LOAD_PROC(alGetListenerf);
+    LOAD_PROC(alGetListener3f);
+    LOAD_PROC(alGetListenerfv);
+    LOAD_PROC(alGetListeneri);
+    LOAD_PROC(alGetListener3i);
+    LOAD_PROC(alGetListeneriv);
+    LOAD_PROC(alGenSources);
+    LOAD_PROC(alDeleteSources);
+    LOAD_PROC(alIsSource);
+    LOAD_PROC(alSourcef);
+    LOAD_PROC(alSource3f);
+    LOAD_PROC(alSourcefv);
+    LOAD_PROC(alSourcei);
+    LOAD_PROC(alSource3i);
+    LOAD_PROC(alSourceiv);
+    LOAD_PROC(alGetSourcef);
+    LOAD_PROC(alGetSource3f);
+    LOAD_PROC(alGetSourcefv);
+    LOAD_PROC(alGetSourcei);
+    LOAD_PROC(alGetSource3i);
+    LOAD_PROC(alGetSourceiv);
+    LOAD_PROC(alSourcePlayv);
+    LOAD_PROC(alSourceStopv);
+    LOAD_PROC(alSourceRewindv);
+    LOAD_PROC(alSourcePausev);
+    LOAD_PROC(alSourcePlay);
+    LOAD_PROC(alSourceStop);
+    LOAD_PROC(alSourceRewind);
+    LOAD_PROC(alSourcePause);
+    LOAD_PROC(alSourceQueueBuffers);
+    LOAD_PROC(alSourceUnqueueBuffers);
+    LOAD_PROC(alGenBuffers);
+    LOAD_PROC(alDeleteBuffers);
+    LOAD_PROC(alIsBuffer);
+    LOAD_PROC(alBufferData);
+    LOAD_PROC(alDopplerFactor);
+    LOAD_PROC(alDopplerVelocity);
+    LOAD_PROC(alSpeedOfSound);
+    LOAD_PROC(alDistanceModel);
+    if(!err)
+    {
+        ALCint alc_ver[2] = { 0, 0 };
+        newdrv.alcGetIntegerv(nullptr, ALC_MAJOR_VERSION, 1, &alc_ver[0]);
+        newdrv.alcGetIntegerv(nullptr, ALC_MINOR_VERSION, 1, &alc_ver[1]);
+        if(newdrv.alcGetError(nullptr) == ALC_NO_ERROR)
+            newdrv.ALCVer = MAKE_ALC_VER(alc_ver[0], alc_ver[1]);
+        else
+        {
+            WARN("Failed to query ALC version for %ls, assuming 1.0\n", name);
+            newdrv.ALCVer = MAKE_ALC_VER(1, 0);
+        }
+
+#undef LOAD_PROC
+#define LOAD_PROC(x) do {                                                      \
+    newdrv.x = reinterpret_cast<decltype(newdrv.x)>(reinterpret_cast<void*>(   \
+        GetProcAddress(module, #x)));                                          \
+    if(!newdrv.x)                                                              \
+    {                                                                          \
+        WARN("Failed to find optional entry point for %s in %ls\n", #x, name); \
+    }                                                                          \
+} while(0)
+    LOAD_PROC(alBufferf);
+    LOAD_PROC(alBuffer3f);
+    LOAD_PROC(alBufferfv);
+    LOAD_PROC(alBufferi);
+    LOAD_PROC(alBuffer3i);
+    LOAD_PROC(alBufferiv);
+    LOAD_PROC(alGetBufferf);
+    LOAD_PROC(alGetBuffer3f);
+    LOAD_PROC(alGetBufferfv);
+    LOAD_PROC(alGetBufferi);
+    LOAD_PROC(alGetBuffer3i);
+    LOAD_PROC(alGetBufferiv);
+
+#undef LOAD_PROC
+#define LOAD_PROC(x) do {                                                     \
+    newdrv.x = reinterpret_cast<decltype(newdrv.x)>(                          \
+        newdrv.alcGetProcAddress(nullptr, #x));                               \
+    if(!newdrv.x)                                                             \
+    {                                                                         \
+        ERR("Failed to find entry point for %s in %ls\n", #x, name);          \
+        err = 1;                                                              \
+    }                                                                         \
+} while(0)
+        if(newdrv.alcIsExtensionPresent(nullptr, "ALC_EXT_thread_local_context"))
+        {
+            LOAD_PROC(alcSetThreadContext);
+            LOAD_PROC(alcGetThreadContext);
+        }
+    }
+
+    if(err)
+    {
+        DriverList.pop_back();
+        return;
+    }
+    TRACE("Loaded module %p, %ls, ALC %d.%d\n", decltype(std::declval<void*>()){module}, name,
+          newdrv.ALCVer>>8, newdrv.ALCVer&255);
+#undef LOAD_PROC
+}
+
+static void SearchDrivers(WCHAR *path)
+{
+    WIN32_FIND_DATAW fdata;
+
+    TRACE("Searching for drivers in %ls...\n", path);
+    std::wstring srchPath = path;
+    srchPath += L"\\*oal.dll";
+
+    HANDLE srchHdl = FindFirstFileW(srchPath.c_str(), &fdata);
+    if(srchHdl != INVALID_HANDLE_VALUE)
+    {
+        do {
+            HMODULE mod;
+
+            srchPath = path;
+            srchPath += L"\\";
+            srchPath += fdata.cFileName;
+            TRACE("Found %ls\n", srchPath.c_str());
+
+            mod = LoadLibraryW(srchPath.c_str());
+            if(!mod)
+                WARN("Could not load %ls\n", srchPath.c_str());
+            else
+                AddModule(mod, fdata.cFileName);
+        } while(FindNextFileW(srchHdl, &fdata));
+        FindClose(srchHdl);
+    }
+}
+
+static WCHAR *strrchrW(WCHAR *str, WCHAR ch)
+{
+    WCHAR *res = nullptr;
+    while(str && *str != '\0')
+    {
+        if(*str == ch)
+            res = str;
+        ++str;
+    }
+    return res;
+}
+
+static int GetLoadedModuleDirectory(const WCHAR *name, WCHAR *moddir, DWORD length)
+{
+    HMODULE module = nullptr;
+    WCHAR *sep0, *sep1;
+
+    if(name)
+    {
+        module = GetModuleHandleW(name);
+        if(!module) return 0;
+    }
+
+    if(GetModuleFileNameW(module, moddir, length) == 0)
+        return 0;
+
+    sep0 = strrchrW(moddir, '/');
+    if(sep0) sep1 = strrchrW(sep0+1, '\\');
+    else sep1 = strrchrW(moddir, '\\');
+
+    if(sep1) *sep1 = '\0';
+    else if(sep0) *sep0 = '\0';
+    else *moddir = '\0';
+
+    return 1;
+}
+
+void LoadDriverList(void)
+{
+    WCHAR dll_path[MAX_PATH+1] = L"";
+    WCHAR cwd_path[MAX_PATH+1] = L"";
+    WCHAR proc_path[MAX_PATH+1] = L"";
+    WCHAR sys_path[MAX_PATH+1] = L"";
+    int len;
+
+    if(GetLoadedModuleDirectory(L"OpenAL32.dll", dll_path, MAX_PATH))
+        TRACE("Got DLL path %ls\n", dll_path);
+
+    GetCurrentDirectoryW(MAX_PATH, cwd_path);
+    len = lstrlenW(cwd_path);
+    if(len > 0 && (cwd_path[len-1] == '\\' || cwd_path[len-1] == '/'))
+        cwd_path[len-1] = '\0';
+    TRACE("Got current working directory %ls\n", cwd_path);
+
+    if(GetLoadedModuleDirectory(nullptr, proc_path, MAX_PATH))
+        TRACE("Got proc path %ls\n", proc_path);
+
+    GetSystemDirectoryW(sys_path, MAX_PATH);
+    len = lstrlenW(sys_path);
+    if(len > 0 && (sys_path[len-1] == '\\' || sys_path[len-1] == '/'))
+        sys_path[len-1] = '\0';
+    TRACE("Got system path %ls\n", sys_path);
+
+    /* Don't search the DLL's path if it is the same as the current working
+     * directory, app's path, or system path (don't want to do duplicate
+     * searches, or increase the priority of the app or system path).
+     */
+    if(dll_path[0] &&
+       (!cwd_path[0] || wcscmp(dll_path, cwd_path) != 0) &&
+       (!proc_path[0] || wcscmp(dll_path, proc_path) != 0) &&
+       (!sys_path[0] || wcscmp(dll_path, sys_path) != 0))
+        SearchDrivers(dll_path);
+    if(cwd_path[0] &&
+       (!proc_path[0] || wcscmp(cwd_path, proc_path) != 0) &&
+       (!sys_path[0] || wcscmp(cwd_path, sys_path) != 0))
+        SearchDrivers(cwd_path);
+    if(proc_path[0] && (!sys_path[0] || wcscmp(proc_path, sys_path) != 0))
+        SearchDrivers(proc_path);
+    if(sys_path[0])
+        SearchDrivers(sys_path);
+}
+
+
+PtrIntMap::~PtrIntMap()
+{
+    std::lock_guard<std::mutex> maplock{mLock};
+    al_free(mKeys);
+    mKeys = nullptr;
+    mValues = nullptr;
+    mSize = 0;
+    mCapacity = 0;
+}
+
+ALenum PtrIntMap::insert(void *key, int value)
+{
+    std::lock_guard<std::mutex> maplock{mLock};
+    auto iter = std::lower_bound(mKeys, mKeys+mSize, key);
+    auto pos = static_cast<ALsizei>(std::distance(mKeys, iter));
+
+    if(pos == mSize || mKeys[pos] != key)
+    {
+        if(mSize == mCapacity)
+        {
+            void **newkeys{nullptr};
+            ALsizei newcap{mCapacity ? (mCapacity<<1) : 4};
+            if(newcap > mCapacity)
+                newkeys = static_cast<void**>(
+                    al_calloc(16, (sizeof(mKeys[0])+sizeof(mValues[0]))*newcap)
+                );
+            if(!newkeys)
+                return AL_OUT_OF_MEMORY;
+            auto newvalues = reinterpret_cast<int*>(&newkeys[newcap]);
+
+            if(mKeys)
+            {
+                std::copy_n(mKeys, mSize, newkeys);
+                std::copy_n(mValues, mSize, newvalues);
+            }
+            al_free(mKeys);
+            mKeys = newkeys;
+            mValues = newvalues;
+            mCapacity = newcap;
+        }
+
+        if(pos < mSize)
+        {
+            std::copy_backward(mKeys+pos, mKeys+mSize, mKeys+mSize+1);
+            std::copy_backward(mValues+pos, mValues+mSize, mValues+mSize+1);
+        }
+        mSize++;
+    }
+    mKeys[pos] = key;
+    mValues[pos] = value;
+
+    return AL_NO_ERROR;
+}
+
+int PtrIntMap::removeByKey(void *key)
+{
+    int ret = -1;
+
+    std::lock_guard<std::mutex> maplock{mLock};
+    auto iter = std::lower_bound(mKeys, mKeys+mSize, key);
+    auto pos = static_cast<ALsizei>(std::distance(mKeys, iter));
+    if(pos < mSize && mKeys[pos] == key)
+    {
+        ret = mValues[pos];
+        if(pos+1 < mSize)
+        {
+            std::copy(mKeys+pos+1, mKeys+mSize, mKeys+pos);
+            std::copy(mValues+pos+1, mValues+mSize, mValues+pos);
+        }
+        mSize--;
+    }
+
+    return ret;
+}
+
+int PtrIntMap::lookupByKey(void *key)
+{
+    int ret = -1;
+
+    std::lock_guard<std::mutex> maplock{mLock};
+    auto iter = std::lower_bound(mKeys, mKeys+mSize, key);
+    auto pos = static_cast<ALsizei>(std::distance(mKeys, iter));
+    if(pos < mSize && mKeys[pos] == key)
+        ret = mValues[pos];
+
+    return ret;
+}
diff --git a/router/router.h b/router/router.h
new file mode 100644 (file)
index 0000000..2a126d4
--- /dev/null
@@ -0,0 +1,243 @@
+#ifndef ROUTER_ROUTER_H
+#define ROUTER_ROUTER_H
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <winnt.h>
+
+#include <stdio.h>
+
+#include <atomic>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "AL/alc.h"
+#include "AL/al.h"
+#include "AL/alext.h"
+
+
+#define MAKE_ALC_VER(major, minor) (((major)<<8) | (minor))
+
+struct DriverIface {
+    std::wstring Name;
+    HMODULE Module{nullptr};
+    int ALCVer{0};
+    std::once_flag InitOnceCtx{};
+
+    LPALCCREATECONTEXT alcCreateContext{nullptr};
+    LPALCMAKECONTEXTCURRENT alcMakeContextCurrent{nullptr};
+    LPALCPROCESSCONTEXT alcProcessContext{nullptr};
+    LPALCSUSPENDCONTEXT alcSuspendContext{nullptr};
+    LPALCDESTROYCONTEXT alcDestroyContext{nullptr};
+    LPALCGETCURRENTCONTEXT alcGetCurrentContext{nullptr};
+    LPALCGETCONTEXTSDEVICE alcGetContextsDevice{nullptr};
+    LPALCOPENDEVICE alcOpenDevice{nullptr};
+    LPALCCLOSEDEVICE alcCloseDevice{nullptr};
+    LPALCGETERROR alcGetError{nullptr};
+    LPALCISEXTENSIONPRESENT alcIsExtensionPresent{nullptr};
+    LPALCGETPROCADDRESS alcGetProcAddress{nullptr};
+    LPALCGETENUMVALUE alcGetEnumValue{nullptr};
+    LPALCGETSTRING alcGetString{nullptr};
+    LPALCGETINTEGERV alcGetIntegerv{nullptr};
+    LPALCCAPTUREOPENDEVICE alcCaptureOpenDevice{nullptr};
+    LPALCCAPTURECLOSEDEVICE alcCaptureCloseDevice{nullptr};
+    LPALCCAPTURESTART alcCaptureStart{nullptr};
+    LPALCCAPTURESTOP alcCaptureStop{nullptr};
+    LPALCCAPTURESAMPLES alcCaptureSamples{nullptr};
+
+    PFNALCSETTHREADCONTEXTPROC alcSetThreadContext{nullptr};
+    PFNALCGETTHREADCONTEXTPROC alcGetThreadContext{nullptr};
+
+    LPALENABLE alEnable{nullptr};
+    LPALDISABLE alDisable{nullptr};
+    LPALISENABLED alIsEnabled{nullptr};
+    LPALGETSTRING alGetString{nullptr};
+    LPALGETBOOLEANV alGetBooleanv{nullptr};
+    LPALGETINTEGERV alGetIntegerv{nullptr};
+    LPALGETFLOATV alGetFloatv{nullptr};
+    LPALGETDOUBLEV alGetDoublev{nullptr};
+    LPALGETBOOLEAN alGetBoolean{nullptr};
+    LPALGETINTEGER alGetInteger{nullptr};
+    LPALGETFLOAT alGetFloat{nullptr};
+    LPALGETDOUBLE alGetDouble{nullptr};
+    LPALGETERROR alGetError{nullptr};
+    LPALISEXTENSIONPRESENT alIsExtensionPresent{nullptr};
+    LPALGETPROCADDRESS alGetProcAddress{nullptr};
+    LPALGETENUMVALUE alGetEnumValue{nullptr};
+    LPALLISTENERF alListenerf{nullptr};
+    LPALLISTENER3F alListener3f{nullptr};
+    LPALLISTENERFV alListenerfv{nullptr};
+    LPALLISTENERI alListeneri{nullptr};
+    LPALLISTENER3I alListener3i{nullptr};
+    LPALLISTENERIV alListeneriv{nullptr};
+    LPALGETLISTENERF alGetListenerf{nullptr};
+    LPALGETLISTENER3F alGetListener3f{nullptr};
+    LPALGETLISTENERFV alGetListenerfv{nullptr};
+    LPALGETLISTENERI alGetListeneri{nullptr};
+    LPALGETLISTENER3I alGetListener3i{nullptr};
+    LPALGETLISTENERIV alGetListeneriv{nullptr};
+    LPALGENSOURCES alGenSources{nullptr};
+    LPALDELETESOURCES alDeleteSources{nullptr};
+    LPALISSOURCE alIsSource{nullptr};
+    LPALSOURCEF alSourcef{nullptr};
+    LPALSOURCE3F alSource3f{nullptr};
+    LPALSOURCEFV alSourcefv{nullptr};
+    LPALSOURCEI alSourcei{nullptr};
+    LPALSOURCE3I alSource3i{nullptr};
+    LPALSOURCEIV alSourceiv{nullptr};
+    LPALGETSOURCEF alGetSourcef{nullptr};
+    LPALGETSOURCE3F alGetSource3f{nullptr};
+    LPALGETSOURCEFV alGetSourcefv{nullptr};
+    LPALGETSOURCEI alGetSourcei{nullptr};
+    LPALGETSOURCE3I alGetSource3i{nullptr};
+    LPALGETSOURCEIV alGetSourceiv{nullptr};
+    LPALSOURCEPLAYV alSourcePlayv{nullptr};
+    LPALSOURCESTOPV alSourceStopv{nullptr};
+    LPALSOURCEREWINDV alSourceRewindv{nullptr};
+    LPALSOURCEPAUSEV alSourcePausev{nullptr};
+    LPALSOURCEPLAY alSourcePlay{nullptr};
+    LPALSOURCESTOP alSourceStop{nullptr};
+    LPALSOURCEREWIND alSourceRewind{nullptr};
+    LPALSOURCEPAUSE alSourcePause{nullptr};
+    LPALSOURCEQUEUEBUFFERS alSourceQueueBuffers{nullptr};
+    LPALSOURCEUNQUEUEBUFFERS alSourceUnqueueBuffers{nullptr};
+    LPALGENBUFFERS alGenBuffers{nullptr};
+    LPALDELETEBUFFERS alDeleteBuffers{nullptr};
+    LPALISBUFFER alIsBuffer{nullptr};
+    LPALBUFFERF alBufferf{nullptr};
+    LPALBUFFER3F alBuffer3f{nullptr};
+    LPALBUFFERFV alBufferfv{nullptr};
+    LPALBUFFERI alBufferi{nullptr};
+    LPALBUFFER3I alBuffer3i{nullptr};
+    LPALBUFFERIV alBufferiv{nullptr};
+    LPALGETBUFFERF alGetBufferf{nullptr};
+    LPALGETBUFFER3F alGetBuffer3f{nullptr};
+    LPALGETBUFFERFV alGetBufferfv{nullptr};
+    LPALGETBUFFERI alGetBufferi{nullptr};
+    LPALGETBUFFER3I alGetBuffer3i{nullptr};
+    LPALGETBUFFERIV alGetBufferiv{nullptr};
+    LPALBUFFERDATA alBufferData{nullptr};
+    LPALDOPPLERFACTOR alDopplerFactor{nullptr};
+    LPALDOPPLERVELOCITY alDopplerVelocity{nullptr};
+    LPALSPEEDOFSOUND alSpeedOfSound{nullptr};
+    LPALDISTANCEMODEL alDistanceModel{nullptr};
+
+    /* Functions to load after first context creation. */
+    LPALGENFILTERS alGenFilters{nullptr};
+    LPALDELETEFILTERS alDeleteFilters{nullptr};
+    LPALISFILTER alIsFilter{nullptr};
+    LPALFILTERF alFilterf{nullptr};
+    LPALFILTERFV alFilterfv{nullptr};
+    LPALFILTERI alFilteri{nullptr};
+    LPALFILTERIV alFilteriv{nullptr};
+    LPALGETFILTERF alGetFilterf{nullptr};
+    LPALGETFILTERFV alGetFilterfv{nullptr};
+    LPALGETFILTERI alGetFilteri{nullptr};
+    LPALGETFILTERIV alGetFilteriv{nullptr};
+    LPALGENEFFECTS alGenEffects{nullptr};
+    LPALDELETEEFFECTS alDeleteEffects{nullptr};
+    LPALISEFFECT alIsEffect{nullptr};
+    LPALEFFECTF alEffectf{nullptr};
+    LPALEFFECTFV alEffectfv{nullptr};
+    LPALEFFECTI alEffecti{nullptr};
+    LPALEFFECTIV alEffectiv{nullptr};
+    LPALGETEFFECTF alGetEffectf{nullptr};
+    LPALGETEFFECTFV alGetEffectfv{nullptr};
+    LPALGETEFFECTI alGetEffecti{nullptr};
+    LPALGETEFFECTIV alGetEffectiv{nullptr};
+    LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots{nullptr};
+    LPALDELETEAUXILIARYEFFECTSLOTS alDeleteAuxiliaryEffectSlots{nullptr};
+    LPALISAUXILIARYEFFECTSLOT alIsAuxiliaryEffectSlot{nullptr};
+    LPALAUXILIARYEFFECTSLOTF alAuxiliaryEffectSlotf{nullptr};
+    LPALAUXILIARYEFFECTSLOTFV alAuxiliaryEffectSlotfv{nullptr};
+    LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti{nullptr};
+    LPALAUXILIARYEFFECTSLOTIV alAuxiliaryEffectSlotiv{nullptr};
+    LPALGETAUXILIARYEFFECTSLOTF alGetAuxiliaryEffectSlotf{nullptr};
+    LPALGETAUXILIARYEFFECTSLOTFV alGetAuxiliaryEffectSlotfv{nullptr};
+    LPALGETAUXILIARYEFFECTSLOTI alGetAuxiliaryEffectSloti{nullptr};
+    LPALGETAUXILIARYEFFECTSLOTIV alGetAuxiliaryEffectSlotiv{nullptr};
+
+    template<typename T>
+    DriverIface(T&& name, HMODULE mod)
+      : Name(std::forward<T>(name)), Module(mod)
+    { }
+    ~DriverIface()
+    {
+        if(Module)
+            FreeLibrary(Module);
+        Module = nullptr;
+    }
+};
+using DriverIfacePtr = std::unique_ptr<DriverIface>;
+
+extern std::vector<DriverIfacePtr> DriverList;
+
+extern thread_local DriverIface *ThreadCtxDriver;
+extern std::atomic<DriverIface*> CurrentCtxDriver;
+
+/* HACK: MinGW generates bad code when accessing an extern thread_local object.
+ * Add a wrapper function for it that only accesses it where it's defined.
+ */
+#ifdef __MINGW32__
+DriverIface *GetThreadDriver() noexcept;
+void SetThreadDriver(DriverIface *driver) noexcept;
+#else
+inline DriverIface *GetThreadDriver() noexcept { return ThreadCtxDriver; }
+inline void SetThreadDriver(DriverIface *driver) noexcept { ThreadCtxDriver = driver; }
+#endif
+
+
+class PtrIntMap {
+    void **mKeys{nullptr};
+    /* Shares memory with keys. */
+    int *mValues{nullptr};
+
+    ALsizei mSize{0};
+    ALsizei mCapacity{0};
+    std::mutex mLock;
+
+public:
+    PtrIntMap() = default;
+    ~PtrIntMap();
+
+    ALenum insert(void *key, int value);
+    int removeByKey(void *key);
+    int lookupByKey(void *key);
+};
+
+
+enum LogLevel {
+    LogLevel_None  = 0,
+    LogLevel_Error = 1,
+    LogLevel_Warn  = 2,
+    LogLevel_Trace = 3,
+};
+extern enum LogLevel LogLevel;
+extern FILE *LogFile;
+
+#define TRACE(...) do {                                   \
+    if(LogLevel >= LogLevel_Trace)                        \
+    {                                                     \
+        fprintf(LogFile, "AL Router (II): " __VA_ARGS__); \
+        fflush(LogFile);                                  \
+    }                                                     \
+} while(0)
+#define WARN(...) do {                                    \
+    if(LogLevel >= LogLevel_Warn)                         \
+    {                                                     \
+        fprintf(LogFile, "AL Router (WW): " __VA_ARGS__); \
+        fflush(LogFile);                                  \
+    }                                                     \
+} while(0)
+#define ERR(...) do {                                     \
+    if(LogLevel >= LogLevel_Error)                        \
+    {                                                     \
+        fprintf(LogFile, "AL Router (EE): " __VA_ARGS__); \
+        fflush(LogFile);                                  \
+    }                                                     \
+} while(0)
+
+#endif /* ROUTER_ROUTER_H */
diff --git a/utils/CIAIR.def b/utils/CIAIR.def
new file mode 100644 (file)
index 0000000..5fabdb3
--- /dev/null
@@ -0,0 +1,3958 @@
+# This is a makemhr HRIR definition file.  It is used to define the layout and
+# source data to be processed into an OpenAL Soft compatible HRTF.
+#
+# This definition is used to transform the left and right ear HRIRs from a
+# data set used in several papers and articles by Fumitada Itakura, Kazuya
+# Takeda, Mikio Ikeda, Shoji Kajita, and Takanori Nishino.
+#
+# The data (data02.tgz) can be obtained from The Database of Head Related
+# Transfer Functions hosted by the Takeda Laboratory at Nagoya University:
+#
+#  http://www.sp.m.is.nagoya-u.ac.jp/HRTF/database.html
+#
+# It is copyright 1999 by Itakura Laboratory and the Center for Integrated
+# Acoustic Information Research (CIAIR) of Nagoya University and provided
+# free of charge with no restrictions on use so long as the authors (above)
+# are cited.
+
+rate     = 44100
+
+# The CIAIR set is stereo because it provides both ear HRIRs.
+type     = stereo
+
+points   = 512
+
+# No head radius was provided.  Just use the average radius of 9 cm.
+radius   = 0.09
+
+# The CIAIR set is composed of a single field with an unknown distance
+# between the source and the listener, so a guess of 1.5 meters is used.
+distance = 1.5
+
+# This set has a uniform number of azimuths for all but the poles (-90 and 90
+# degree elevation).
+azimuths = 1, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 1
+
+# The CIAIR source azimuth is counter-clockwise, so it needs to be flipped.
+# The extension of the source data may be misleading, they're ASCII text
+# lists of floating point values (one per line).  Left and right ear HRIRs
+# (from the respective files) are used to create a stereo HRTF.
+[  9,  0 ] = ascii (fp) : "./hrtfs/elev-45/L-45e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e000a.dat right
+[  9,  1 ] = ascii (fp) : "./hrtfs/elev-45/L-45e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e355a.dat right
+[  9,  2 ] = ascii (fp) : "./hrtfs/elev-45/L-45e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e350a.dat right
+[  9,  3 ] = ascii (fp) : "./hrtfs/elev-45/L-45e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e345a.dat right
+[  9,  4 ] = ascii (fp) : "./hrtfs/elev-45/L-45e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e340a.dat right
+[  9,  5 ] = ascii (fp) : "./hrtfs/elev-45/L-45e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e335a.dat right
+[  9,  6 ] = ascii (fp) : "./hrtfs/elev-45/L-45e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e330a.dat right
+[  9,  7 ] = ascii (fp) : "./hrtfs/elev-45/L-45e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e325a.dat right
+[  9,  8 ] = ascii (fp) : "./hrtfs/elev-45/L-45e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e320a.dat right
+[  9,  9 ] = ascii (fp) : "./hrtfs/elev-45/L-45e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e315a.dat right
+[  9, 10 ] = ascii (fp) : "./hrtfs/elev-45/L-45e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e310a.dat right
+[  9, 11 ] = ascii (fp) : "./hrtfs/elev-45/L-45e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e305a.dat right
+[  9, 12 ] = ascii (fp) : "./hrtfs/elev-45/L-45e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e300a.dat right
+[  9, 13 ] = ascii (fp) : "./hrtfs/elev-45/L-45e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e295a.dat right
+[  9, 14 ] = ascii (fp) : "./hrtfs/elev-45/L-45e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e290a.dat right
+[  9, 15 ] = ascii (fp) : "./hrtfs/elev-45/L-45e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e285a.dat right
+[  9, 16 ] = ascii (fp) : "./hrtfs/elev-45/L-45e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e280a.dat right
+[  9, 17 ] = ascii (fp) : "./hrtfs/elev-45/L-45e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e275a.dat right
+[  9, 18 ] = ascii (fp) : "./hrtfs/elev-45/L-45e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e270a.dat right
+[  9, 19 ] = ascii (fp) : "./hrtfs/elev-45/L-45e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e265a.dat right
+[  9, 20 ] = ascii (fp) : "./hrtfs/elev-45/L-45e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e260a.dat right
+[  9, 21 ] = ascii (fp) : "./hrtfs/elev-45/L-45e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e255a.dat right
+[  9, 22 ] = ascii (fp) : "./hrtfs/elev-45/L-45e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e250a.dat right
+[  9, 23 ] = ascii (fp) : "./hrtfs/elev-45/L-45e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e245a.dat right
+[  9, 24 ] = ascii (fp) : "./hrtfs/elev-45/L-45e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e240a.dat right
+[  9, 25 ] = ascii (fp) : "./hrtfs/elev-45/L-45e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e235a.dat right
+[  9, 26 ] = ascii (fp) : "./hrtfs/elev-45/L-45e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e230a.dat right
+[  9, 27 ] = ascii (fp) : "./hrtfs/elev-45/L-45e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e225a.dat right
+[  9, 28 ] = ascii (fp) : "./hrtfs/elev-45/L-45e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e220a.dat right
+[  9, 29 ] = ascii (fp) : "./hrtfs/elev-45/L-45e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e215a.dat right
+[  9, 30 ] = ascii (fp) : "./hrtfs/elev-45/L-45e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e210a.dat right
+[  9, 31 ] = ascii (fp) : "./hrtfs/elev-45/L-45e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e205a.dat right
+[  9, 32 ] = ascii (fp) : "./hrtfs/elev-45/L-45e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e200a.dat right
+[  9, 33 ] = ascii (fp) : "./hrtfs/elev-45/L-45e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e195a.dat right
+[  9, 34 ] = ascii (fp) : "./hrtfs/elev-45/L-45e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e190a.dat right
+[  9, 35 ] = ascii (fp) : "./hrtfs/elev-45/L-45e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e185a.dat right
+[  9, 36 ] = ascii (fp) : "./hrtfs/elev-45/L-45e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e180a.dat right
+[  9, 37 ] = ascii (fp) : "./hrtfs/elev-45/L-45e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e175a.dat right
+[  9, 38 ] = ascii (fp) : "./hrtfs/elev-45/L-45e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e170a.dat right
+[  9, 39 ] = ascii (fp) : "./hrtfs/elev-45/L-45e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e165a.dat right
+[  9, 40 ] = ascii (fp) : "./hrtfs/elev-45/L-45e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e160a.dat right
+[  9, 41 ] = ascii (fp) : "./hrtfs/elev-45/L-45e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e155a.dat right
+[  9, 42 ] = ascii (fp) : "./hrtfs/elev-45/L-45e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e150a.dat right
+[  9, 43 ] = ascii (fp) : "./hrtfs/elev-45/L-45e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e145a.dat right
+[  9, 44 ] = ascii (fp) : "./hrtfs/elev-45/L-45e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e140a.dat right
+[  9, 45 ] = ascii (fp) : "./hrtfs/elev-45/L-45e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e135a.dat right
+[  9, 46 ] = ascii (fp) : "./hrtfs/elev-45/L-45e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e130a.dat right
+[  9, 47 ] = ascii (fp) : "./hrtfs/elev-45/L-45e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e125a.dat right
+[  9, 48 ] = ascii (fp) : "./hrtfs/elev-45/L-45e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e120a.dat right
+[  9, 49 ] = ascii (fp) : "./hrtfs/elev-45/L-45e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e115a.dat right
+[  9, 50 ] = ascii (fp) : "./hrtfs/elev-45/L-45e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e110a.dat right
+[  9, 51 ] = ascii (fp) : "./hrtfs/elev-45/L-45e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e105a.dat right
+[  9, 52 ] = ascii (fp) : "./hrtfs/elev-45/L-45e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e100a.dat right
+[  9, 53 ] = ascii (fp) : "./hrtfs/elev-45/L-45e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e095a.dat right
+[  9, 54 ] = ascii (fp) : "./hrtfs/elev-45/L-45e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e090a.dat right
+[  9, 55 ] = ascii (fp) : "./hrtfs/elev-45/L-45e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e085a.dat right
+[  9, 56 ] = ascii (fp) : "./hrtfs/elev-45/L-45e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e080a.dat right
+[  9, 57 ] = ascii (fp) : "./hrtfs/elev-45/L-45e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e075a.dat right
+[  9, 58 ] = ascii (fp) : "./hrtfs/elev-45/L-45e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e070a.dat right
+[  9, 59 ] = ascii (fp) : "./hrtfs/elev-45/L-45e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e065a.dat right
+[  9, 60 ] = ascii (fp) : "./hrtfs/elev-45/L-45e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e060a.dat right
+[  9, 61 ] = ascii (fp) : "./hrtfs/elev-45/L-45e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e055a.dat right
+[  9, 62 ] = ascii (fp) : "./hrtfs/elev-45/L-45e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e050a.dat right
+[  9, 63 ] = ascii (fp) : "./hrtfs/elev-45/L-45e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e045a.dat right
+[  9, 64 ] = ascii (fp) : "./hrtfs/elev-45/L-45e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e040a.dat right
+[  9, 65 ] = ascii (fp) : "./hrtfs/elev-45/L-45e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e035a.dat right
+[  9, 66 ] = ascii (fp) : "./hrtfs/elev-45/L-45e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e030a.dat right
+[  9, 67 ] = ascii (fp) : "./hrtfs/elev-45/L-45e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e025a.dat right
+[  9, 68 ] = ascii (fp) : "./hrtfs/elev-45/L-45e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e020a.dat right
+[  9, 69 ] = ascii (fp) : "./hrtfs/elev-45/L-45e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e015a.dat right
+[  9, 70 ] = ascii (fp) : "./hrtfs/elev-45/L-45e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e010a.dat right
+[  9, 71 ] = ascii (fp) : "./hrtfs/elev-45/L-45e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-45/R-45e005a.dat right
+
+[ 10,  0 ] = ascii (fp) : "./hrtfs/elev-40/L-40e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e000a.dat right
+[ 10,  1 ] = ascii (fp) : "./hrtfs/elev-40/L-40e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e355a.dat right
+[ 10,  2 ] = ascii (fp) : "./hrtfs/elev-40/L-40e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e350a.dat right
+[ 10,  3 ] = ascii (fp) : "./hrtfs/elev-40/L-40e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e345a.dat right
+[ 10,  4 ] = ascii (fp) : "./hrtfs/elev-40/L-40e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e340a.dat right
+[ 10,  5 ] = ascii (fp) : "./hrtfs/elev-40/L-40e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e335a.dat right
+[ 10,  6 ] = ascii (fp) : "./hrtfs/elev-40/L-40e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e330a.dat right
+[ 10,  7 ] = ascii (fp) : "./hrtfs/elev-40/L-40e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e325a.dat right
+[ 10,  8 ] = ascii (fp) : "./hrtfs/elev-40/L-40e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e320a.dat right
+[ 10,  9 ] = ascii (fp) : "./hrtfs/elev-40/L-40e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e315a.dat right
+[ 10, 10 ] = ascii (fp) : "./hrtfs/elev-40/L-40e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e310a.dat right
+[ 10, 11 ] = ascii (fp) : "./hrtfs/elev-40/L-40e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e305a.dat right
+[ 10, 12 ] = ascii (fp) : "./hrtfs/elev-40/L-40e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e300a.dat right
+[ 10, 13 ] = ascii (fp) : "./hrtfs/elev-40/L-40e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e295a.dat right
+[ 10, 14 ] = ascii (fp) : "./hrtfs/elev-40/L-40e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e290a.dat right
+[ 10, 15 ] = ascii (fp) : "./hrtfs/elev-40/L-40e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e285a.dat right
+[ 10, 16 ] = ascii (fp) : "./hrtfs/elev-40/L-40e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e280a.dat right
+[ 10, 17 ] = ascii (fp) : "./hrtfs/elev-40/L-40e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e275a.dat right
+[ 10, 18 ] = ascii (fp) : "./hrtfs/elev-40/L-40e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e270a.dat right
+[ 10, 19 ] = ascii (fp) : "./hrtfs/elev-40/L-40e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e265a.dat right
+[ 10, 20 ] = ascii (fp) : "./hrtfs/elev-40/L-40e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e260a.dat right
+[ 10, 21 ] = ascii (fp) : "./hrtfs/elev-40/L-40e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e255a.dat right
+[ 10, 22 ] = ascii (fp) : "./hrtfs/elev-40/L-40e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e250a.dat right
+[ 10, 23 ] = ascii (fp) : "./hrtfs/elev-40/L-40e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e245a.dat right
+[ 10, 24 ] = ascii (fp) : "./hrtfs/elev-40/L-40e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e240a.dat right
+[ 10, 25 ] = ascii (fp) : "./hrtfs/elev-40/L-40e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e235a.dat right
+[ 10, 26 ] = ascii (fp) : "./hrtfs/elev-40/L-40e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e230a.dat right
+[ 10, 27 ] = ascii (fp) : "./hrtfs/elev-40/L-40e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e225a.dat right
+[ 10, 28 ] = ascii (fp) : "./hrtfs/elev-40/L-40e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e220a.dat right
+[ 10, 29 ] = ascii (fp) : "./hrtfs/elev-40/L-40e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e215a.dat right
+[ 10, 30 ] = ascii (fp) : "./hrtfs/elev-40/L-40e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e210a.dat right
+[ 10, 31 ] = ascii (fp) : "./hrtfs/elev-40/L-40e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e205a.dat right
+[ 10, 32 ] = ascii (fp) : "./hrtfs/elev-40/L-40e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e200a.dat right
+[ 10, 33 ] = ascii (fp) : "./hrtfs/elev-40/L-40e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e195a.dat right
+[ 10, 34 ] = ascii (fp) : "./hrtfs/elev-40/L-40e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e190a.dat right
+[ 10, 35 ] = ascii (fp) : "./hrtfs/elev-40/L-40e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e185a.dat right
+[ 10, 36 ] = ascii (fp) : "./hrtfs/elev-40/L-40e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e180a.dat right
+[ 10, 37 ] = ascii (fp) : "./hrtfs/elev-40/L-40e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e175a.dat right
+[ 10, 38 ] = ascii (fp) : "./hrtfs/elev-40/L-40e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e170a.dat right
+[ 10, 39 ] = ascii (fp) : "./hrtfs/elev-40/L-40e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e165a.dat right
+[ 10, 40 ] = ascii (fp) : "./hrtfs/elev-40/L-40e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e160a.dat right
+[ 10, 41 ] = ascii (fp) : "./hrtfs/elev-40/L-40e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e155a.dat right
+[ 10, 42 ] = ascii (fp) : "./hrtfs/elev-40/L-40e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e150a.dat right
+[ 10, 43 ] = ascii (fp) : "./hrtfs/elev-40/L-40e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e145a.dat right
+[ 10, 44 ] = ascii (fp) : "./hrtfs/elev-40/L-40e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e140a.dat right
+[ 10, 45 ] = ascii (fp) : "./hrtfs/elev-40/L-40e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e135a.dat right
+[ 10, 46 ] = ascii (fp) : "./hrtfs/elev-40/L-40e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e130a.dat right
+[ 10, 47 ] = ascii (fp) : "./hrtfs/elev-40/L-40e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e125a.dat right
+[ 10, 48 ] = ascii (fp) : "./hrtfs/elev-40/L-40e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e120a.dat right
+[ 10, 49 ] = ascii (fp) : "./hrtfs/elev-40/L-40e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e115a.dat right
+[ 10, 50 ] = ascii (fp) : "./hrtfs/elev-40/L-40e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e110a.dat right
+[ 10, 51 ] = ascii (fp) : "./hrtfs/elev-40/L-40e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e105a.dat right
+[ 10, 52 ] = ascii (fp) : "./hrtfs/elev-40/L-40e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e100a.dat right
+[ 10, 53 ] = ascii (fp) : "./hrtfs/elev-40/L-40e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e095a.dat right
+[ 10, 54 ] = ascii (fp) : "./hrtfs/elev-40/L-40e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e090a.dat right
+[ 10, 55 ] = ascii (fp) : "./hrtfs/elev-40/L-40e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e085a.dat right
+[ 10, 56 ] = ascii (fp) : "./hrtfs/elev-40/L-40e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e080a.dat right
+[ 10, 57 ] = ascii (fp) : "./hrtfs/elev-40/L-40e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e075a.dat right
+[ 10, 58 ] = ascii (fp) : "./hrtfs/elev-40/L-40e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e070a.dat right
+[ 10, 59 ] = ascii (fp) : "./hrtfs/elev-40/L-40e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e065a.dat right
+[ 10, 60 ] = ascii (fp) : "./hrtfs/elev-40/L-40e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e060a.dat right
+[ 10, 61 ] = ascii (fp) : "./hrtfs/elev-40/L-40e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e055a.dat right
+[ 10, 62 ] = ascii (fp) : "./hrtfs/elev-40/L-40e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e050a.dat right
+[ 10, 63 ] = ascii (fp) : "./hrtfs/elev-40/L-40e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e045a.dat right
+[ 10, 64 ] = ascii (fp) : "./hrtfs/elev-40/L-40e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e040a.dat right
+[ 10, 65 ] = ascii (fp) : "./hrtfs/elev-40/L-40e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e035a.dat right
+[ 10, 66 ] = ascii (fp) : "./hrtfs/elev-40/L-40e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e030a.dat right
+[ 10, 67 ] = ascii (fp) : "./hrtfs/elev-40/L-40e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e025a.dat right
+[ 10, 68 ] = ascii (fp) : "./hrtfs/elev-40/L-40e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e020a.dat right
+[ 10, 69 ] = ascii (fp) : "./hrtfs/elev-40/L-40e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e015a.dat right
+[ 10, 70 ] = ascii (fp) : "./hrtfs/elev-40/L-40e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e010a.dat right
+[ 10, 71 ] = ascii (fp) : "./hrtfs/elev-40/L-40e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-40/R-40e005a.dat right
+
+[ 11,  0 ] = ascii (fp) : "./hrtfs/elev-35/L-35e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e000a.dat right
+[ 11,  1 ] = ascii (fp) : "./hrtfs/elev-35/L-35e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e355a.dat right
+[ 11,  2 ] = ascii (fp) : "./hrtfs/elev-35/L-35e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e350a.dat right
+[ 11,  3 ] = ascii (fp) : "./hrtfs/elev-35/L-35e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e345a.dat right
+[ 11,  4 ] = ascii (fp) : "./hrtfs/elev-35/L-35e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e340a.dat right
+[ 11,  5 ] = ascii (fp) : "./hrtfs/elev-35/L-35e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e335a.dat right
+[ 11,  6 ] = ascii (fp) : "./hrtfs/elev-35/L-35e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e330a.dat right
+[ 11,  7 ] = ascii (fp) : "./hrtfs/elev-35/L-35e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e325a.dat right
+[ 11,  8 ] = ascii (fp) : "./hrtfs/elev-35/L-35e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e320a.dat right
+[ 11,  9 ] = ascii (fp) : "./hrtfs/elev-35/L-35e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e315a.dat right
+[ 11, 10 ] = ascii (fp) : "./hrtfs/elev-35/L-35e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e310a.dat right
+[ 11, 11 ] = ascii (fp) : "./hrtfs/elev-35/L-35e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e305a.dat right
+[ 11, 12 ] = ascii (fp) : "./hrtfs/elev-35/L-35e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e300a.dat right
+[ 11, 13 ] = ascii (fp) : "./hrtfs/elev-35/L-35e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e295a.dat right
+[ 11, 14 ] = ascii (fp) : "./hrtfs/elev-35/L-35e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e290a.dat right
+[ 11, 15 ] = ascii (fp) : "./hrtfs/elev-35/L-35e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e285a.dat right
+[ 11, 16 ] = ascii (fp) : "./hrtfs/elev-35/L-35e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e280a.dat right
+[ 11, 17 ] = ascii (fp) : "./hrtfs/elev-35/L-35e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e275a.dat right
+[ 11, 18 ] = ascii (fp) : "./hrtfs/elev-35/L-35e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e270a.dat right
+[ 11, 19 ] = ascii (fp) : "./hrtfs/elev-35/L-35e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e265a.dat right
+[ 11, 20 ] = ascii (fp) : "./hrtfs/elev-35/L-35e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e260a.dat right
+[ 11, 21 ] = ascii (fp) : "./hrtfs/elev-35/L-35e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e255a.dat right
+[ 11, 22 ] = ascii (fp) : "./hrtfs/elev-35/L-35e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e250a.dat right
+[ 11, 23 ] = ascii (fp) : "./hrtfs/elev-35/L-35e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e245a.dat right
+[ 11, 24 ] = ascii (fp) : "./hrtfs/elev-35/L-35e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e240a.dat right
+[ 11, 25 ] = ascii (fp) : "./hrtfs/elev-35/L-35e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e235a.dat right
+[ 11, 26 ] = ascii (fp) : "./hrtfs/elev-35/L-35e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e230a.dat right
+[ 11, 27 ] = ascii (fp) : "./hrtfs/elev-35/L-35e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e225a.dat right
+[ 11, 28 ] = ascii (fp) : "./hrtfs/elev-35/L-35e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e220a.dat right
+[ 11, 29 ] = ascii (fp) : "./hrtfs/elev-35/L-35e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e215a.dat right
+[ 11, 30 ] = ascii (fp) : "./hrtfs/elev-35/L-35e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e210a.dat right
+[ 11, 31 ] = ascii (fp) : "./hrtfs/elev-35/L-35e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e205a.dat right
+[ 11, 32 ] = ascii (fp) : "./hrtfs/elev-35/L-35e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e200a.dat right
+[ 11, 33 ] = ascii (fp) : "./hrtfs/elev-35/L-35e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e195a.dat right
+[ 11, 34 ] = ascii (fp) : "./hrtfs/elev-35/L-35e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e190a.dat right
+[ 11, 35 ] = ascii (fp) : "./hrtfs/elev-35/L-35e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e185a.dat right
+[ 11, 36 ] = ascii (fp) : "./hrtfs/elev-35/L-35e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e180a.dat right
+[ 11, 37 ] = ascii (fp) : "./hrtfs/elev-35/L-35e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e175a.dat right
+[ 11, 38 ] = ascii (fp) : "./hrtfs/elev-35/L-35e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e170a.dat right
+[ 11, 39 ] = ascii (fp) : "./hrtfs/elev-35/L-35e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e165a.dat right
+[ 11, 40 ] = ascii (fp) : "./hrtfs/elev-35/L-35e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e160a.dat right
+[ 11, 41 ] = ascii (fp) : "./hrtfs/elev-35/L-35e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e155a.dat right
+[ 11, 42 ] = ascii (fp) : "./hrtfs/elev-35/L-35e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e150a.dat right
+[ 11, 43 ] = ascii (fp) : "./hrtfs/elev-35/L-35e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e145a.dat right
+[ 11, 44 ] = ascii (fp) : "./hrtfs/elev-35/L-35e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e140a.dat right
+[ 11, 45 ] = ascii (fp) : "./hrtfs/elev-35/L-35e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e135a.dat right
+[ 11, 46 ] = ascii (fp) : "./hrtfs/elev-35/L-35e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e130a.dat right
+[ 11, 47 ] = ascii (fp) : "./hrtfs/elev-35/L-35e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e125a.dat right
+[ 11, 48 ] = ascii (fp) : "./hrtfs/elev-35/L-35e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e120a.dat right
+[ 11, 49 ] = ascii (fp) : "./hrtfs/elev-35/L-35e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e115a.dat right
+[ 11, 50 ] = ascii (fp) : "./hrtfs/elev-35/L-35e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e110a.dat right
+[ 11, 51 ] = ascii (fp) : "./hrtfs/elev-35/L-35e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e105a.dat right
+[ 11, 52 ] = ascii (fp) : "./hrtfs/elev-35/L-35e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e100a.dat right
+[ 11, 53 ] = ascii (fp) : "./hrtfs/elev-35/L-35e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e095a.dat right
+[ 11, 54 ] = ascii (fp) : "./hrtfs/elev-35/L-35e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e090a.dat right
+[ 11, 55 ] = ascii (fp) : "./hrtfs/elev-35/L-35e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e085a.dat right
+[ 11, 56 ] = ascii (fp) : "./hrtfs/elev-35/L-35e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e080a.dat right
+[ 11, 57 ] = ascii (fp) : "./hrtfs/elev-35/L-35e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e075a.dat right
+[ 11, 58 ] = ascii (fp) : "./hrtfs/elev-35/L-35e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e070a.dat right
+[ 11, 59 ] = ascii (fp) : "./hrtfs/elev-35/L-35e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e065a.dat right
+[ 11, 60 ] = ascii (fp) : "./hrtfs/elev-35/L-35e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e060a.dat right
+[ 11, 61 ] = ascii (fp) : "./hrtfs/elev-35/L-35e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e055a.dat right
+[ 11, 62 ] = ascii (fp) : "./hrtfs/elev-35/L-35e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e050a.dat right
+[ 11, 63 ] = ascii (fp) : "./hrtfs/elev-35/L-35e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e045a.dat right
+[ 11, 64 ] = ascii (fp) : "./hrtfs/elev-35/L-35e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e040a.dat right
+[ 11, 65 ] = ascii (fp) : "./hrtfs/elev-35/L-35e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e035a.dat right
+[ 11, 66 ] = ascii (fp) : "./hrtfs/elev-35/L-35e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e030a.dat right
+[ 11, 67 ] = ascii (fp) : "./hrtfs/elev-35/L-35e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e025a.dat right
+[ 11, 68 ] = ascii (fp) : "./hrtfs/elev-35/L-35e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e020a.dat right
+[ 11, 69 ] = ascii (fp) : "./hrtfs/elev-35/L-35e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e015a.dat right
+[ 11, 70 ] = ascii (fp) : "./hrtfs/elev-35/L-35e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e010a.dat right
+[ 11, 71 ] = ascii (fp) : "./hrtfs/elev-35/L-35e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-35/R-35e005a.dat right
+
+[ 12,  0 ] = ascii (fp) : "./hrtfs/elev-30/L-30e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e000a.dat right
+[ 12,  1 ] = ascii (fp) : "./hrtfs/elev-30/L-30e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e355a.dat right
+[ 12,  2 ] = ascii (fp) : "./hrtfs/elev-30/L-30e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e350a.dat right
+[ 12,  3 ] = ascii (fp) : "./hrtfs/elev-30/L-30e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e345a.dat right
+[ 12,  4 ] = ascii (fp) : "./hrtfs/elev-30/L-30e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e340a.dat right
+[ 12,  5 ] = ascii (fp) : "./hrtfs/elev-30/L-30e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e335a.dat right
+[ 12,  6 ] = ascii (fp) : "./hrtfs/elev-30/L-30e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e330a.dat right
+[ 12,  7 ] = ascii (fp) : "./hrtfs/elev-30/L-30e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e325a.dat right
+[ 12,  8 ] = ascii (fp) : "./hrtfs/elev-30/L-30e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e320a.dat right
+[ 12,  9 ] = ascii (fp) : "./hrtfs/elev-30/L-30e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e315a.dat right
+[ 12, 10 ] = ascii (fp) : "./hrtfs/elev-30/L-30e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e310a.dat right
+[ 12, 11 ] = ascii (fp) : "./hrtfs/elev-30/L-30e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e305a.dat right
+[ 12, 12 ] = ascii (fp) : "./hrtfs/elev-30/L-30e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e300a.dat right
+[ 12, 13 ] = ascii (fp) : "./hrtfs/elev-30/L-30e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e295a.dat right
+[ 12, 14 ] = ascii (fp) : "./hrtfs/elev-30/L-30e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e290a.dat right
+[ 12, 15 ] = ascii (fp) : "./hrtfs/elev-30/L-30e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e285a.dat right
+[ 12, 16 ] = ascii (fp) : "./hrtfs/elev-30/L-30e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e280a.dat right
+[ 12, 17 ] = ascii (fp) : "./hrtfs/elev-30/L-30e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e275a.dat right
+[ 12, 18 ] = ascii (fp) : "./hrtfs/elev-30/L-30e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e270a.dat right
+[ 12, 19 ] = ascii (fp) : "./hrtfs/elev-30/L-30e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e265a.dat right
+[ 12, 20 ] = ascii (fp) : "./hrtfs/elev-30/L-30e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e260a.dat right
+[ 12, 21 ] = ascii (fp) : "./hrtfs/elev-30/L-30e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e255a.dat right
+[ 12, 22 ] = ascii (fp) : "./hrtfs/elev-30/L-30e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e250a.dat right
+[ 12, 23 ] = ascii (fp) : "./hrtfs/elev-30/L-30e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e245a.dat right
+[ 12, 24 ] = ascii (fp) : "./hrtfs/elev-30/L-30e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e240a.dat right
+[ 12, 25 ] = ascii (fp) : "./hrtfs/elev-30/L-30e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e235a.dat right
+[ 12, 26 ] = ascii (fp) : "./hrtfs/elev-30/L-30e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e230a.dat right
+[ 12, 27 ] = ascii (fp) : "./hrtfs/elev-30/L-30e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e225a.dat right
+[ 12, 28 ] = ascii (fp) : "./hrtfs/elev-30/L-30e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e220a.dat right
+[ 12, 29 ] = ascii (fp) : "./hrtfs/elev-30/L-30e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e215a.dat right
+[ 12, 30 ] = ascii (fp) : "./hrtfs/elev-30/L-30e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e210a.dat right
+[ 12, 31 ] = ascii (fp) : "./hrtfs/elev-30/L-30e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e205a.dat right
+[ 12, 32 ] = ascii (fp) : "./hrtfs/elev-30/L-30e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e200a.dat right
+[ 12, 33 ] = ascii (fp) : "./hrtfs/elev-30/L-30e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e195a.dat right
+[ 12, 34 ] = ascii (fp) : "./hrtfs/elev-30/L-30e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e190a.dat right
+[ 12, 35 ] = ascii (fp) : "./hrtfs/elev-30/L-30e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e185a.dat right
+[ 12, 36 ] = ascii (fp) : "./hrtfs/elev-30/L-30e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e180a.dat right
+[ 12, 37 ] = ascii (fp) : "./hrtfs/elev-30/L-30e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e175a.dat right
+[ 12, 38 ] = ascii (fp) : "./hrtfs/elev-30/L-30e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e170a.dat right
+[ 12, 39 ] = ascii (fp) : "./hrtfs/elev-30/L-30e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e165a.dat right
+[ 12, 40 ] = ascii (fp) : "./hrtfs/elev-30/L-30e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e160a.dat right
+[ 12, 41 ] = ascii (fp) : "./hrtfs/elev-30/L-30e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e155a.dat right
+[ 12, 42 ] = ascii (fp) : "./hrtfs/elev-30/L-30e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e150a.dat right
+[ 12, 43 ] = ascii (fp) : "./hrtfs/elev-30/L-30e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e145a.dat right
+[ 12, 44 ] = ascii (fp) : "./hrtfs/elev-30/L-30e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e140a.dat right
+[ 12, 45 ] = ascii (fp) : "./hrtfs/elev-30/L-30e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e135a.dat right
+[ 12, 46 ] = ascii (fp) : "./hrtfs/elev-30/L-30e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e130a.dat right
+[ 12, 47 ] = ascii (fp) : "./hrtfs/elev-30/L-30e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e125a.dat right
+[ 12, 48 ] = ascii (fp) : "./hrtfs/elev-30/L-30e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e120a.dat right
+[ 12, 49 ] = ascii (fp) : "./hrtfs/elev-30/L-30e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e115a.dat right
+[ 12, 50 ] = ascii (fp) : "./hrtfs/elev-30/L-30e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e110a.dat right
+[ 12, 51 ] = ascii (fp) : "./hrtfs/elev-30/L-30e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e105a.dat right
+[ 12, 52 ] = ascii (fp) : "./hrtfs/elev-30/L-30e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e100a.dat right
+[ 12, 53 ] = ascii (fp) : "./hrtfs/elev-30/L-30e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e095a.dat right
+[ 12, 54 ] = ascii (fp) : "./hrtfs/elev-30/L-30e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e090a.dat right
+[ 12, 55 ] = ascii (fp) : "./hrtfs/elev-30/L-30e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e085a.dat right
+[ 12, 56 ] = ascii (fp) : "./hrtfs/elev-30/L-30e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e080a.dat right
+[ 12, 57 ] = ascii (fp) : "./hrtfs/elev-30/L-30e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e075a.dat right
+[ 12, 58 ] = ascii (fp) : "./hrtfs/elev-30/L-30e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e070a.dat right
+[ 12, 59 ] = ascii (fp) : "./hrtfs/elev-30/L-30e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e065a.dat right
+[ 12, 60 ] = ascii (fp) : "./hrtfs/elev-30/L-30e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e060a.dat right
+[ 12, 61 ] = ascii (fp) : "./hrtfs/elev-30/L-30e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e055a.dat right
+[ 12, 62 ] = ascii (fp) : "./hrtfs/elev-30/L-30e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e050a.dat right
+[ 12, 63 ] = ascii (fp) : "./hrtfs/elev-30/L-30e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e045a.dat right
+[ 12, 64 ] = ascii (fp) : "./hrtfs/elev-30/L-30e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e040a.dat right
+[ 12, 65 ] = ascii (fp) : "./hrtfs/elev-30/L-30e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e035a.dat right
+[ 12, 66 ] = ascii (fp) : "./hrtfs/elev-30/L-30e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e030a.dat right
+[ 12, 67 ] = ascii (fp) : "./hrtfs/elev-30/L-30e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e025a.dat right
+[ 12, 68 ] = ascii (fp) : "./hrtfs/elev-30/L-30e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e020a.dat right
+[ 12, 69 ] = ascii (fp) : "./hrtfs/elev-30/L-30e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e015a.dat right
+[ 12, 70 ] = ascii (fp) : "./hrtfs/elev-30/L-30e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e010a.dat right
+[ 12, 71 ] = ascii (fp) : "./hrtfs/elev-30/L-30e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-30/R-30e005a.dat right
+
+[ 13,  0 ] = ascii (fp) : "./hrtfs/elev-25/L-25e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e000a.dat right
+[ 13,  1 ] = ascii (fp) : "./hrtfs/elev-25/L-25e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e355a.dat right
+[ 13,  2 ] = ascii (fp) : "./hrtfs/elev-25/L-25e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e350a.dat right
+[ 13,  3 ] = ascii (fp) : "./hrtfs/elev-25/L-25e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e345a.dat right
+[ 13,  4 ] = ascii (fp) : "./hrtfs/elev-25/L-25e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e340a.dat right
+[ 13,  5 ] = ascii (fp) : "./hrtfs/elev-25/L-25e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e335a.dat right
+[ 13,  6 ] = ascii (fp) : "./hrtfs/elev-25/L-25e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e330a.dat right
+[ 13,  7 ] = ascii (fp) : "./hrtfs/elev-25/L-25e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e325a.dat right
+[ 13,  8 ] = ascii (fp) : "./hrtfs/elev-25/L-25e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e320a.dat right
+[ 13,  9 ] = ascii (fp) : "./hrtfs/elev-25/L-25e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e315a.dat right
+[ 13, 10 ] = ascii (fp) : "./hrtfs/elev-25/L-25e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e310a.dat right
+[ 13, 11 ] = ascii (fp) : "./hrtfs/elev-25/L-25e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e305a.dat right
+[ 13, 12 ] = ascii (fp) : "./hrtfs/elev-25/L-25e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e300a.dat right
+[ 13, 13 ] = ascii (fp) : "./hrtfs/elev-25/L-25e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e295a.dat right
+[ 13, 14 ] = ascii (fp) : "./hrtfs/elev-25/L-25e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e290a.dat right
+[ 13, 15 ] = ascii (fp) : "./hrtfs/elev-25/L-25e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e285a.dat right
+[ 13, 16 ] = ascii (fp) : "./hrtfs/elev-25/L-25e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e280a.dat right
+[ 13, 17 ] = ascii (fp) : "./hrtfs/elev-25/L-25e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e275a.dat right
+[ 13, 18 ] = ascii (fp) : "./hrtfs/elev-25/L-25e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e270a.dat right
+[ 13, 19 ] = ascii (fp) : "./hrtfs/elev-25/L-25e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e265a.dat right
+[ 13, 20 ] = ascii (fp) : "./hrtfs/elev-25/L-25e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e260a.dat right
+[ 13, 21 ] = ascii (fp) : "./hrtfs/elev-25/L-25e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e255a.dat right
+[ 13, 22 ] = ascii (fp) : "./hrtfs/elev-25/L-25e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e250a.dat right
+[ 13, 23 ] = ascii (fp) : "./hrtfs/elev-25/L-25e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e245a.dat right
+[ 13, 24 ] = ascii (fp) : "./hrtfs/elev-25/L-25e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e240a.dat right
+[ 13, 25 ] = ascii (fp) : "./hrtfs/elev-25/L-25e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e235a.dat right
+[ 13, 26 ] = ascii (fp) : "./hrtfs/elev-25/L-25e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e230a.dat right
+[ 13, 27 ] = ascii (fp) : "./hrtfs/elev-25/L-25e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e225a.dat right
+[ 13, 28 ] = ascii (fp) : "./hrtfs/elev-25/L-25e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e220a.dat right
+[ 13, 29 ] = ascii (fp) : "./hrtfs/elev-25/L-25e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e215a.dat right
+[ 13, 30 ] = ascii (fp) : "./hrtfs/elev-25/L-25e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e210a.dat right
+[ 13, 31 ] = ascii (fp) : "./hrtfs/elev-25/L-25e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e205a.dat right
+[ 13, 32 ] = ascii (fp) : "./hrtfs/elev-25/L-25e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e200a.dat right
+[ 13, 33 ] = ascii (fp) : "./hrtfs/elev-25/L-25e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e195a.dat right
+[ 13, 34 ] = ascii (fp) : "./hrtfs/elev-25/L-25e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e190a.dat right
+[ 13, 35 ] = ascii (fp) : "./hrtfs/elev-25/L-25e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e185a.dat right
+[ 13, 36 ] = ascii (fp) : "./hrtfs/elev-25/L-25e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e180a.dat right
+[ 13, 37 ] = ascii (fp) : "./hrtfs/elev-25/L-25e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e175a.dat right
+[ 13, 38 ] = ascii (fp) : "./hrtfs/elev-25/L-25e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e170a.dat right
+[ 13, 39 ] = ascii (fp) : "./hrtfs/elev-25/L-25e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e165a.dat right
+[ 13, 40 ] = ascii (fp) : "./hrtfs/elev-25/L-25e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e160a.dat right
+[ 13, 41 ] = ascii (fp) : "./hrtfs/elev-25/L-25e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e155a.dat right
+[ 13, 42 ] = ascii (fp) : "./hrtfs/elev-25/L-25e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e150a.dat right
+[ 13, 43 ] = ascii (fp) : "./hrtfs/elev-25/L-25e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e145a.dat right
+[ 13, 44 ] = ascii (fp) : "./hrtfs/elev-25/L-25e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e140a.dat right
+[ 13, 45 ] = ascii (fp) : "./hrtfs/elev-25/L-25e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e135a.dat right
+[ 13, 46 ] = ascii (fp) : "./hrtfs/elev-25/L-25e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e130a.dat right
+[ 13, 47 ] = ascii (fp) : "./hrtfs/elev-25/L-25e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e125a.dat right
+[ 13, 48 ] = ascii (fp) : "./hrtfs/elev-25/L-25e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e120a.dat right
+[ 13, 49 ] = ascii (fp) : "./hrtfs/elev-25/L-25e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e115a.dat right
+[ 13, 50 ] = ascii (fp) : "./hrtfs/elev-25/L-25e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e110a.dat right
+[ 13, 51 ] = ascii (fp) : "./hrtfs/elev-25/L-25e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e105a.dat right
+[ 13, 52 ] = ascii (fp) : "./hrtfs/elev-25/L-25e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e100a.dat right
+[ 13, 53 ] = ascii (fp) : "./hrtfs/elev-25/L-25e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e095a.dat right
+[ 13, 54 ] = ascii (fp) : "./hrtfs/elev-25/L-25e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e090a.dat right
+[ 13, 55 ] = ascii (fp) : "./hrtfs/elev-25/L-25e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e085a.dat right
+[ 13, 56 ] = ascii (fp) : "./hrtfs/elev-25/L-25e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e080a.dat right
+[ 13, 57 ] = ascii (fp) : "./hrtfs/elev-25/L-25e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e075a.dat right
+[ 13, 58 ] = ascii (fp) : "./hrtfs/elev-25/L-25e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e070a.dat right
+[ 13, 59 ] = ascii (fp) : "./hrtfs/elev-25/L-25e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e065a.dat right
+[ 13, 60 ] = ascii (fp) : "./hrtfs/elev-25/L-25e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e060a.dat right
+[ 13, 61 ] = ascii (fp) : "./hrtfs/elev-25/L-25e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e055a.dat right
+[ 13, 62 ] = ascii (fp) : "./hrtfs/elev-25/L-25e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e050a.dat right
+[ 13, 63 ] = ascii (fp) : "./hrtfs/elev-25/L-25e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e045a.dat right
+[ 13, 64 ] = ascii (fp) : "./hrtfs/elev-25/L-25e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e040a.dat right
+[ 13, 65 ] = ascii (fp) : "./hrtfs/elev-25/L-25e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e035a.dat right
+[ 13, 66 ] = ascii (fp) : "./hrtfs/elev-25/L-25e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e030a.dat right
+[ 13, 67 ] = ascii (fp) : "./hrtfs/elev-25/L-25e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e025a.dat right
+[ 13, 68 ] = ascii (fp) : "./hrtfs/elev-25/L-25e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e020a.dat right
+[ 13, 69 ] = ascii (fp) : "./hrtfs/elev-25/L-25e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e015a.dat right
+[ 13, 70 ] = ascii (fp) : "./hrtfs/elev-25/L-25e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e010a.dat right
+[ 13, 71 ] = ascii (fp) : "./hrtfs/elev-25/L-25e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-25/R-25e005a.dat right
+
+[ 14,  0 ] = ascii (fp) : "./hrtfs/elev-20/L-20e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e000a.dat right
+[ 14,  1 ] = ascii (fp) : "./hrtfs/elev-20/L-20e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e355a.dat right
+[ 14,  2 ] = ascii (fp) : "./hrtfs/elev-20/L-20e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e350a.dat right
+[ 14,  3 ] = ascii (fp) : "./hrtfs/elev-20/L-20e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e345a.dat right
+[ 14,  4 ] = ascii (fp) : "./hrtfs/elev-20/L-20e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e340a.dat right
+[ 14,  5 ] = ascii (fp) : "./hrtfs/elev-20/L-20e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e335a.dat right
+[ 14,  6 ] = ascii (fp) : "./hrtfs/elev-20/L-20e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e330a.dat right
+[ 14,  7 ] = ascii (fp) : "./hrtfs/elev-20/L-20e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e325a.dat right
+[ 14,  8 ] = ascii (fp) : "./hrtfs/elev-20/L-20e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e320a.dat right
+[ 14,  9 ] = ascii (fp) : "./hrtfs/elev-20/L-20e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e315a.dat right
+[ 14, 10 ] = ascii (fp) : "./hrtfs/elev-20/L-20e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e310a.dat right
+[ 14, 11 ] = ascii (fp) : "./hrtfs/elev-20/L-20e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e305a.dat right
+[ 14, 12 ] = ascii (fp) : "./hrtfs/elev-20/L-20e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e300a.dat right
+[ 14, 13 ] = ascii (fp) : "./hrtfs/elev-20/L-20e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e295a.dat right
+[ 14, 14 ] = ascii (fp) : "./hrtfs/elev-20/L-20e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e290a.dat right
+[ 14, 15 ] = ascii (fp) : "./hrtfs/elev-20/L-20e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e285a.dat right
+[ 14, 16 ] = ascii (fp) : "./hrtfs/elev-20/L-20e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e280a.dat right
+[ 14, 17 ] = ascii (fp) : "./hrtfs/elev-20/L-20e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e275a.dat right
+[ 14, 18 ] = ascii (fp) : "./hrtfs/elev-20/L-20e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e270a.dat right
+[ 14, 19 ] = ascii (fp) : "./hrtfs/elev-20/L-20e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e265a.dat right
+[ 14, 20 ] = ascii (fp) : "./hrtfs/elev-20/L-20e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e260a.dat right
+[ 14, 21 ] = ascii (fp) : "./hrtfs/elev-20/L-20e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e255a.dat right
+[ 14, 22 ] = ascii (fp) : "./hrtfs/elev-20/L-20e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e250a.dat right
+[ 14, 23 ] = ascii (fp) : "./hrtfs/elev-20/L-20e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e245a.dat right
+[ 14, 24 ] = ascii (fp) : "./hrtfs/elev-20/L-20e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e240a.dat right
+[ 14, 25 ] = ascii (fp) : "./hrtfs/elev-20/L-20e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e235a.dat right
+[ 14, 26 ] = ascii (fp) : "./hrtfs/elev-20/L-20e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e230a.dat right
+[ 14, 27 ] = ascii (fp) : "./hrtfs/elev-20/L-20e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e225a.dat right
+[ 14, 28 ] = ascii (fp) : "./hrtfs/elev-20/L-20e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e220a.dat right
+[ 14, 29 ] = ascii (fp) : "./hrtfs/elev-20/L-20e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e215a.dat right
+[ 14, 30 ] = ascii (fp) : "./hrtfs/elev-20/L-20e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e210a.dat right
+[ 14, 31 ] = ascii (fp) : "./hrtfs/elev-20/L-20e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e205a.dat right
+[ 14, 32 ] = ascii (fp) : "./hrtfs/elev-20/L-20e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e200a.dat right
+[ 14, 33 ] = ascii (fp) : "./hrtfs/elev-20/L-20e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e195a.dat right
+[ 14, 34 ] = ascii (fp) : "./hrtfs/elev-20/L-20e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e190a.dat right
+[ 14, 35 ] = ascii (fp) : "./hrtfs/elev-20/L-20e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e185a.dat right
+[ 14, 36 ] = ascii (fp) : "./hrtfs/elev-20/L-20e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e180a.dat right
+[ 14, 37 ] = ascii (fp) : "./hrtfs/elev-20/L-20e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e175a.dat right
+[ 14, 38 ] = ascii (fp) : "./hrtfs/elev-20/L-20e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e170a.dat right
+[ 14, 39 ] = ascii (fp) : "./hrtfs/elev-20/L-20e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e165a.dat right
+[ 14, 40 ] = ascii (fp) : "./hrtfs/elev-20/L-20e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e160a.dat right
+[ 14, 41 ] = ascii (fp) : "./hrtfs/elev-20/L-20e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e155a.dat right
+[ 14, 42 ] = ascii (fp) : "./hrtfs/elev-20/L-20e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e150a.dat right
+[ 14, 43 ] = ascii (fp) : "./hrtfs/elev-20/L-20e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e145a.dat right
+[ 14, 44 ] = ascii (fp) : "./hrtfs/elev-20/L-20e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e140a.dat right
+[ 14, 45 ] = ascii (fp) : "./hrtfs/elev-20/L-20e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e135a.dat right
+[ 14, 46 ] = ascii (fp) : "./hrtfs/elev-20/L-20e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e130a.dat right
+[ 14, 47 ] = ascii (fp) : "./hrtfs/elev-20/L-20e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e125a.dat right
+[ 14, 48 ] = ascii (fp) : "./hrtfs/elev-20/L-20e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e120a.dat right
+[ 14, 49 ] = ascii (fp) : "./hrtfs/elev-20/L-20e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e115a.dat right
+[ 14, 50 ] = ascii (fp) : "./hrtfs/elev-20/L-20e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e110a.dat right
+[ 14, 51 ] = ascii (fp) : "./hrtfs/elev-20/L-20e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e105a.dat right
+[ 14, 52 ] = ascii (fp) : "./hrtfs/elev-20/L-20e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e100a.dat right
+[ 14, 53 ] = ascii (fp) : "./hrtfs/elev-20/L-20e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e095a.dat right
+[ 14, 54 ] = ascii (fp) : "./hrtfs/elev-20/L-20e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e090a.dat right
+[ 14, 55 ] = ascii (fp) : "./hrtfs/elev-20/L-20e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e085a.dat right
+[ 14, 56 ] = ascii (fp) : "./hrtfs/elev-20/L-20e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e080a.dat right
+[ 14, 57 ] = ascii (fp) : "./hrtfs/elev-20/L-20e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e075a.dat right
+[ 14, 58 ] = ascii (fp) : "./hrtfs/elev-20/L-20e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e070a.dat right
+[ 14, 59 ] = ascii (fp) : "./hrtfs/elev-20/L-20e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e065a.dat right
+[ 14, 60 ] = ascii (fp) : "./hrtfs/elev-20/L-20e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e060a.dat right
+[ 14, 61 ] = ascii (fp) : "./hrtfs/elev-20/L-20e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e055a.dat right
+[ 14, 62 ] = ascii (fp) : "./hrtfs/elev-20/L-20e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e050a.dat right
+[ 14, 63 ] = ascii (fp) : "./hrtfs/elev-20/L-20e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e045a.dat right
+[ 14, 64 ] = ascii (fp) : "./hrtfs/elev-20/L-20e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e040a.dat right
+[ 14, 65 ] = ascii (fp) : "./hrtfs/elev-20/L-20e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e035a.dat right
+[ 14, 66 ] = ascii (fp) : "./hrtfs/elev-20/L-20e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e030a.dat right
+[ 14, 67 ] = ascii (fp) : "./hrtfs/elev-20/L-20e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e025a.dat right
+[ 14, 68 ] = ascii (fp) : "./hrtfs/elev-20/L-20e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e020a.dat right
+[ 14, 69 ] = ascii (fp) : "./hrtfs/elev-20/L-20e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e015a.dat right
+[ 14, 70 ] = ascii (fp) : "./hrtfs/elev-20/L-20e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e010a.dat right
+[ 14, 71 ] = ascii (fp) : "./hrtfs/elev-20/L-20e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-20/R-20e005a.dat right
+
+[ 15,  0 ] = ascii (fp) : "./hrtfs/elev-15/L-15e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e000a.dat right
+[ 15,  1 ] = ascii (fp) : "./hrtfs/elev-15/L-15e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e355a.dat right
+[ 15,  2 ] = ascii (fp) : "./hrtfs/elev-15/L-15e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e350a.dat right
+[ 15,  3 ] = ascii (fp) : "./hrtfs/elev-15/L-15e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e345a.dat right
+[ 15,  4 ] = ascii (fp) : "./hrtfs/elev-15/L-15e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e340a.dat right
+[ 15,  5 ] = ascii (fp) : "./hrtfs/elev-15/L-15e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e335a.dat right
+[ 15,  6 ] = ascii (fp) : "./hrtfs/elev-15/L-15e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e330a.dat right
+[ 15,  7 ] = ascii (fp) : "./hrtfs/elev-15/L-15e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e325a.dat right
+[ 15,  8 ] = ascii (fp) : "./hrtfs/elev-15/L-15e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e320a.dat right
+[ 15,  9 ] = ascii (fp) : "./hrtfs/elev-15/L-15e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e315a.dat right
+[ 15, 10 ] = ascii (fp) : "./hrtfs/elev-15/L-15e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e310a.dat right
+[ 15, 11 ] = ascii (fp) : "./hrtfs/elev-15/L-15e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e305a.dat right
+[ 15, 12 ] = ascii (fp) : "./hrtfs/elev-15/L-15e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e300a.dat right
+[ 15, 13 ] = ascii (fp) : "./hrtfs/elev-15/L-15e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e295a.dat right
+[ 15, 14 ] = ascii (fp) : "./hrtfs/elev-15/L-15e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e290a.dat right
+[ 15, 15 ] = ascii (fp) : "./hrtfs/elev-15/L-15e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e285a.dat right
+[ 15, 16 ] = ascii (fp) : "./hrtfs/elev-15/L-15e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e280a.dat right
+[ 15, 17 ] = ascii (fp) : "./hrtfs/elev-15/L-15e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e275a.dat right
+[ 15, 18 ] = ascii (fp) : "./hrtfs/elev-15/L-15e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e270a.dat right
+[ 15, 19 ] = ascii (fp) : "./hrtfs/elev-15/L-15e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e265a.dat right
+[ 15, 20 ] = ascii (fp) : "./hrtfs/elev-15/L-15e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e260a.dat right
+[ 15, 21 ] = ascii (fp) : "./hrtfs/elev-15/L-15e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e255a.dat right
+[ 15, 22 ] = ascii (fp) : "./hrtfs/elev-15/L-15e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e250a.dat right
+[ 15, 23 ] = ascii (fp) : "./hrtfs/elev-15/L-15e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e245a.dat right
+[ 15, 24 ] = ascii (fp) : "./hrtfs/elev-15/L-15e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e240a.dat right
+[ 15, 25 ] = ascii (fp) : "./hrtfs/elev-15/L-15e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e235a.dat right
+[ 15, 26 ] = ascii (fp) : "./hrtfs/elev-15/L-15e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e230a.dat right
+[ 15, 27 ] = ascii (fp) : "./hrtfs/elev-15/L-15e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e225a.dat right
+[ 15, 28 ] = ascii (fp) : "./hrtfs/elev-15/L-15e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e220a.dat right
+[ 15, 29 ] = ascii (fp) : "./hrtfs/elev-15/L-15e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e215a.dat right
+[ 15, 30 ] = ascii (fp) : "./hrtfs/elev-15/L-15e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e210a.dat right
+[ 15, 31 ] = ascii (fp) : "./hrtfs/elev-15/L-15e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e205a.dat right
+[ 15, 32 ] = ascii (fp) : "./hrtfs/elev-15/L-15e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e200a.dat right
+[ 15, 33 ] = ascii (fp) : "./hrtfs/elev-15/L-15e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e195a.dat right
+[ 15, 34 ] = ascii (fp) : "./hrtfs/elev-15/L-15e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e190a.dat right
+[ 15, 35 ] = ascii (fp) : "./hrtfs/elev-15/L-15e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e185a.dat right
+[ 15, 36 ] = ascii (fp) : "./hrtfs/elev-15/L-15e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e180a.dat right
+[ 15, 37 ] = ascii (fp) : "./hrtfs/elev-15/L-15e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e175a.dat right
+[ 15, 38 ] = ascii (fp) : "./hrtfs/elev-15/L-15e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e170a.dat right
+[ 15, 39 ] = ascii (fp) : "./hrtfs/elev-15/L-15e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e165a.dat right
+[ 15, 40 ] = ascii (fp) : "./hrtfs/elev-15/L-15e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e160a.dat right
+[ 15, 41 ] = ascii (fp) : "./hrtfs/elev-15/L-15e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e155a.dat right
+[ 15, 42 ] = ascii (fp) : "./hrtfs/elev-15/L-15e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e150a.dat right
+[ 15, 43 ] = ascii (fp) : "./hrtfs/elev-15/L-15e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e145a.dat right
+[ 15, 44 ] = ascii (fp) : "./hrtfs/elev-15/L-15e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e140a.dat right
+[ 15, 45 ] = ascii (fp) : "./hrtfs/elev-15/L-15e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e135a.dat right
+[ 15, 46 ] = ascii (fp) : "./hrtfs/elev-15/L-15e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e130a.dat right
+[ 15, 47 ] = ascii (fp) : "./hrtfs/elev-15/L-15e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e125a.dat right
+[ 15, 48 ] = ascii (fp) : "./hrtfs/elev-15/L-15e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e120a.dat right
+[ 15, 49 ] = ascii (fp) : "./hrtfs/elev-15/L-15e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e115a.dat right
+[ 15, 50 ] = ascii (fp) : "./hrtfs/elev-15/L-15e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e110a.dat right
+[ 15, 51 ] = ascii (fp) : "./hrtfs/elev-15/L-15e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e105a.dat right
+[ 15, 52 ] = ascii (fp) : "./hrtfs/elev-15/L-15e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e100a.dat right
+[ 15, 53 ] = ascii (fp) : "./hrtfs/elev-15/L-15e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e095a.dat right
+[ 15, 54 ] = ascii (fp) : "./hrtfs/elev-15/L-15e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e090a.dat right
+[ 15, 55 ] = ascii (fp) : "./hrtfs/elev-15/L-15e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e085a.dat right
+[ 15, 56 ] = ascii (fp) : "./hrtfs/elev-15/L-15e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e080a.dat right
+[ 15, 57 ] = ascii (fp) : "./hrtfs/elev-15/L-15e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e075a.dat right
+[ 15, 58 ] = ascii (fp) : "./hrtfs/elev-15/L-15e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e070a.dat right
+[ 15, 59 ] = ascii (fp) : "./hrtfs/elev-15/L-15e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e065a.dat right
+[ 15, 60 ] = ascii (fp) : "./hrtfs/elev-15/L-15e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e060a.dat right
+[ 15, 61 ] = ascii (fp) : "./hrtfs/elev-15/L-15e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e055a.dat right
+[ 15, 62 ] = ascii (fp) : "./hrtfs/elev-15/L-15e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e050a.dat right
+[ 15, 63 ] = ascii (fp) : "./hrtfs/elev-15/L-15e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e045a.dat right
+[ 15, 64 ] = ascii (fp) : "./hrtfs/elev-15/L-15e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e040a.dat right
+[ 15, 65 ] = ascii (fp) : "./hrtfs/elev-15/L-15e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e035a.dat right
+[ 15, 66 ] = ascii (fp) : "./hrtfs/elev-15/L-15e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e030a.dat right
+[ 15, 67 ] = ascii (fp) : "./hrtfs/elev-15/L-15e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e025a.dat right
+[ 15, 68 ] = ascii (fp) : "./hrtfs/elev-15/L-15e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e020a.dat right
+[ 15, 69 ] = ascii (fp) : "./hrtfs/elev-15/L-15e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e015a.dat right
+[ 15, 70 ] = ascii (fp) : "./hrtfs/elev-15/L-15e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e010a.dat right
+[ 15, 71 ] = ascii (fp) : "./hrtfs/elev-15/L-15e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-15/R-15e005a.dat right
+
+[ 16,  0 ] = ascii (fp) : "./hrtfs/elev-10/L-10e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e000a.dat right
+[ 16,  1 ] = ascii (fp) : "./hrtfs/elev-10/L-10e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e355a.dat right
+[ 16,  2 ] = ascii (fp) : "./hrtfs/elev-10/L-10e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e350a.dat right
+[ 16,  3 ] = ascii (fp) : "./hrtfs/elev-10/L-10e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e345a.dat right
+[ 16,  4 ] = ascii (fp) : "./hrtfs/elev-10/L-10e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e340a.dat right
+[ 16,  5 ] = ascii (fp) : "./hrtfs/elev-10/L-10e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e335a.dat right
+[ 16,  6 ] = ascii (fp) : "./hrtfs/elev-10/L-10e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e330a.dat right
+[ 16,  7 ] = ascii (fp) : "./hrtfs/elev-10/L-10e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e325a.dat right
+[ 16,  8 ] = ascii (fp) : "./hrtfs/elev-10/L-10e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e320a.dat right
+[ 16,  9 ] = ascii (fp) : "./hrtfs/elev-10/L-10e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e315a.dat right
+[ 16, 10 ] = ascii (fp) : "./hrtfs/elev-10/L-10e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e310a.dat right
+[ 16, 11 ] = ascii (fp) : "./hrtfs/elev-10/L-10e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e305a.dat right
+[ 16, 12 ] = ascii (fp) : "./hrtfs/elev-10/L-10e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e300a.dat right
+[ 16, 13 ] = ascii (fp) : "./hrtfs/elev-10/L-10e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e295a.dat right
+[ 16, 14 ] = ascii (fp) : "./hrtfs/elev-10/L-10e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e290a.dat right
+[ 16, 15 ] = ascii (fp) : "./hrtfs/elev-10/L-10e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e285a.dat right
+[ 16, 16 ] = ascii (fp) : "./hrtfs/elev-10/L-10e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e280a.dat right
+[ 16, 17 ] = ascii (fp) : "./hrtfs/elev-10/L-10e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e275a.dat right
+[ 16, 18 ] = ascii (fp) : "./hrtfs/elev-10/L-10e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e270a.dat right
+[ 16, 19 ] = ascii (fp) : "./hrtfs/elev-10/L-10e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e265a.dat right
+[ 16, 20 ] = ascii (fp) : "./hrtfs/elev-10/L-10e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e260a.dat right
+[ 16, 21 ] = ascii (fp) : "./hrtfs/elev-10/L-10e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e255a.dat right
+[ 16, 22 ] = ascii (fp) : "./hrtfs/elev-10/L-10e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e250a.dat right
+[ 16, 23 ] = ascii (fp) : "./hrtfs/elev-10/L-10e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e245a.dat right
+[ 16, 24 ] = ascii (fp) : "./hrtfs/elev-10/L-10e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e240a.dat right
+[ 16, 25 ] = ascii (fp) : "./hrtfs/elev-10/L-10e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e235a.dat right
+[ 16, 26 ] = ascii (fp) : "./hrtfs/elev-10/L-10e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e230a.dat right
+[ 16, 27 ] = ascii (fp) : "./hrtfs/elev-10/L-10e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e225a.dat right
+[ 16, 28 ] = ascii (fp) : "./hrtfs/elev-10/L-10e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e220a.dat right
+[ 16, 29 ] = ascii (fp) : "./hrtfs/elev-10/L-10e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e215a.dat right
+[ 16, 30 ] = ascii (fp) : "./hrtfs/elev-10/L-10e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e210a.dat right
+[ 16, 31 ] = ascii (fp) : "./hrtfs/elev-10/L-10e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e205a.dat right
+[ 16, 32 ] = ascii (fp) : "./hrtfs/elev-10/L-10e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e200a.dat right
+[ 16, 33 ] = ascii (fp) : "./hrtfs/elev-10/L-10e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e195a.dat right
+[ 16, 34 ] = ascii (fp) : "./hrtfs/elev-10/L-10e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e190a.dat right
+[ 16, 35 ] = ascii (fp) : "./hrtfs/elev-10/L-10e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e185a.dat right
+[ 16, 36 ] = ascii (fp) : "./hrtfs/elev-10/L-10e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e180a.dat right
+[ 16, 37 ] = ascii (fp) : "./hrtfs/elev-10/L-10e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e175a.dat right
+[ 16, 38 ] = ascii (fp) : "./hrtfs/elev-10/L-10e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e170a.dat right
+[ 16, 39 ] = ascii (fp) : "./hrtfs/elev-10/L-10e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e165a.dat right
+[ 16, 40 ] = ascii (fp) : "./hrtfs/elev-10/L-10e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e160a.dat right
+[ 16, 41 ] = ascii (fp) : "./hrtfs/elev-10/L-10e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e155a.dat right
+[ 16, 42 ] = ascii (fp) : "./hrtfs/elev-10/L-10e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e150a.dat right
+[ 16, 43 ] = ascii (fp) : "./hrtfs/elev-10/L-10e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e145a.dat right
+[ 16, 44 ] = ascii (fp) : "./hrtfs/elev-10/L-10e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e140a.dat right
+[ 16, 45 ] = ascii (fp) : "./hrtfs/elev-10/L-10e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e135a.dat right
+[ 16, 46 ] = ascii (fp) : "./hrtfs/elev-10/L-10e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e130a.dat right
+[ 16, 47 ] = ascii (fp) : "./hrtfs/elev-10/L-10e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e125a.dat right
+[ 16, 48 ] = ascii (fp) : "./hrtfs/elev-10/L-10e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e120a.dat right
+[ 16, 49 ] = ascii (fp) : "./hrtfs/elev-10/L-10e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e115a.dat right
+[ 16, 50 ] = ascii (fp) : "./hrtfs/elev-10/L-10e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e110a.dat right
+[ 16, 51 ] = ascii (fp) : "./hrtfs/elev-10/L-10e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e105a.dat right
+[ 16, 52 ] = ascii (fp) : "./hrtfs/elev-10/L-10e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e100a.dat right
+[ 16, 53 ] = ascii (fp) : "./hrtfs/elev-10/L-10e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e095a.dat right
+[ 16, 54 ] = ascii (fp) : "./hrtfs/elev-10/L-10e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e090a.dat right
+[ 16, 55 ] = ascii (fp) : "./hrtfs/elev-10/L-10e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e085a.dat right
+[ 16, 56 ] = ascii (fp) : "./hrtfs/elev-10/L-10e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e080a.dat right
+[ 16, 57 ] = ascii (fp) : "./hrtfs/elev-10/L-10e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e075a.dat right
+[ 16, 58 ] = ascii (fp) : "./hrtfs/elev-10/L-10e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e070a.dat right
+[ 16, 59 ] = ascii (fp) : "./hrtfs/elev-10/L-10e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e065a.dat right
+[ 16, 60 ] = ascii (fp) : "./hrtfs/elev-10/L-10e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e060a.dat right
+[ 16, 61 ] = ascii (fp) : "./hrtfs/elev-10/L-10e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e055a.dat right
+[ 16, 62 ] = ascii (fp) : "./hrtfs/elev-10/L-10e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e050a.dat right
+[ 16, 63 ] = ascii (fp) : "./hrtfs/elev-10/L-10e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e045a.dat right
+[ 16, 64 ] = ascii (fp) : "./hrtfs/elev-10/L-10e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e040a.dat right
+[ 16, 65 ] = ascii (fp) : "./hrtfs/elev-10/L-10e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e035a.dat right
+[ 16, 66 ] = ascii (fp) : "./hrtfs/elev-10/L-10e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e030a.dat right
+[ 16, 67 ] = ascii (fp) : "./hrtfs/elev-10/L-10e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e025a.dat right
+[ 16, 68 ] = ascii (fp) : "./hrtfs/elev-10/L-10e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e020a.dat right
+[ 16, 69 ] = ascii (fp) : "./hrtfs/elev-10/L-10e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e015a.dat right
+[ 16, 70 ] = ascii (fp) : "./hrtfs/elev-10/L-10e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e010a.dat right
+[ 16, 71 ] = ascii (fp) : "./hrtfs/elev-10/L-10e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-10/R-10e005a.dat right
+
+[ 17,  0 ] = ascii (fp) : "./hrtfs/elev-5/L-5e000a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e000a.dat right
+[ 17,  1 ] = ascii (fp) : "./hrtfs/elev-5/L-5e355a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e355a.dat right
+[ 17,  2 ] = ascii (fp) : "./hrtfs/elev-5/L-5e350a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e350a.dat right
+[ 17,  3 ] = ascii (fp) : "./hrtfs/elev-5/L-5e345a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e345a.dat right
+[ 17,  4 ] = ascii (fp) : "./hrtfs/elev-5/L-5e340a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e340a.dat right
+[ 17,  5 ] = ascii (fp) : "./hrtfs/elev-5/L-5e335a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e335a.dat right
+[ 17,  6 ] = ascii (fp) : "./hrtfs/elev-5/L-5e330a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e330a.dat right
+[ 17,  7 ] = ascii (fp) : "./hrtfs/elev-5/L-5e325a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e325a.dat right
+[ 17,  8 ] = ascii (fp) : "./hrtfs/elev-5/L-5e320a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e320a.dat right
+[ 17,  9 ] = ascii (fp) : "./hrtfs/elev-5/L-5e315a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e315a.dat right
+[ 17, 10 ] = ascii (fp) : "./hrtfs/elev-5/L-5e310a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e310a.dat right
+[ 17, 11 ] = ascii (fp) : "./hrtfs/elev-5/L-5e305a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e305a.dat right
+[ 17, 12 ] = ascii (fp) : "./hrtfs/elev-5/L-5e300a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e300a.dat right
+[ 17, 13 ] = ascii (fp) : "./hrtfs/elev-5/L-5e295a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e295a.dat right
+[ 17, 14 ] = ascii (fp) : "./hrtfs/elev-5/L-5e290a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e290a.dat right
+[ 17, 15 ] = ascii (fp) : "./hrtfs/elev-5/L-5e285a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e285a.dat right
+[ 17, 16 ] = ascii (fp) : "./hrtfs/elev-5/L-5e280a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e280a.dat right
+[ 17, 17 ] = ascii (fp) : "./hrtfs/elev-5/L-5e275a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e275a.dat right
+[ 17, 18 ] = ascii (fp) : "./hrtfs/elev-5/L-5e270a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e270a.dat right
+[ 17, 19 ] = ascii (fp) : "./hrtfs/elev-5/L-5e265a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e265a.dat right
+[ 17, 20 ] = ascii (fp) : "./hrtfs/elev-5/L-5e260a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e260a.dat right
+[ 17, 21 ] = ascii (fp) : "./hrtfs/elev-5/L-5e255a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e255a.dat right
+[ 17, 22 ] = ascii (fp) : "./hrtfs/elev-5/L-5e250a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e250a.dat right
+[ 17, 23 ] = ascii (fp) : "./hrtfs/elev-5/L-5e245a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e245a.dat right
+[ 17, 24 ] = ascii (fp) : "./hrtfs/elev-5/L-5e240a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e240a.dat right
+[ 17, 25 ] = ascii (fp) : "./hrtfs/elev-5/L-5e235a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e235a.dat right
+[ 17, 26 ] = ascii (fp) : "./hrtfs/elev-5/L-5e230a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e230a.dat right
+[ 17, 27 ] = ascii (fp) : "./hrtfs/elev-5/L-5e225a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e225a.dat right
+[ 17, 28 ] = ascii (fp) : "./hrtfs/elev-5/L-5e220a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e220a.dat right
+[ 17, 29 ] = ascii (fp) : "./hrtfs/elev-5/L-5e215a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e215a.dat right
+[ 17, 30 ] = ascii (fp) : "./hrtfs/elev-5/L-5e210a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e210a.dat right
+[ 17, 31 ] = ascii (fp) : "./hrtfs/elev-5/L-5e205a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e205a.dat right
+[ 17, 32 ] = ascii (fp) : "./hrtfs/elev-5/L-5e200a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e200a.dat right
+[ 17, 33 ] = ascii (fp) : "./hrtfs/elev-5/L-5e195a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e195a.dat right
+[ 17, 34 ] = ascii (fp) : "./hrtfs/elev-5/L-5e190a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e190a.dat right
+[ 17, 35 ] = ascii (fp) : "./hrtfs/elev-5/L-5e185a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e185a.dat right
+[ 17, 36 ] = ascii (fp) : "./hrtfs/elev-5/L-5e180a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e180a.dat right
+[ 17, 37 ] = ascii (fp) : "./hrtfs/elev-5/L-5e175a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e175a.dat right
+[ 17, 38 ] = ascii (fp) : "./hrtfs/elev-5/L-5e170a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e170a.dat right
+[ 17, 39 ] = ascii (fp) : "./hrtfs/elev-5/L-5e165a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e165a.dat right
+[ 17, 40 ] = ascii (fp) : "./hrtfs/elev-5/L-5e160a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e160a.dat right
+[ 17, 41 ] = ascii (fp) : "./hrtfs/elev-5/L-5e155a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e155a.dat right
+[ 17, 42 ] = ascii (fp) : "./hrtfs/elev-5/L-5e150a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e150a.dat right
+[ 17, 43 ] = ascii (fp) : "./hrtfs/elev-5/L-5e145a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e145a.dat right
+[ 17, 44 ] = ascii (fp) : "./hrtfs/elev-5/L-5e140a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e140a.dat right
+[ 17, 45 ] = ascii (fp) : "./hrtfs/elev-5/L-5e135a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e135a.dat right
+[ 17, 46 ] = ascii (fp) : "./hrtfs/elev-5/L-5e130a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e130a.dat right
+[ 17, 47 ] = ascii (fp) : "./hrtfs/elev-5/L-5e125a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e125a.dat right
+[ 17, 48 ] = ascii (fp) : "./hrtfs/elev-5/L-5e120a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e120a.dat right
+[ 17, 49 ] = ascii (fp) : "./hrtfs/elev-5/L-5e115a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e115a.dat right
+[ 17, 50 ] = ascii (fp) : "./hrtfs/elev-5/L-5e110a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e110a.dat right
+[ 17, 51 ] = ascii (fp) : "./hrtfs/elev-5/L-5e105a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e105a.dat right
+[ 17, 52 ] = ascii (fp) : "./hrtfs/elev-5/L-5e100a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e100a.dat right
+[ 17, 53 ] = ascii (fp) : "./hrtfs/elev-5/L-5e095a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e095a.dat right
+[ 17, 54 ] = ascii (fp) : "./hrtfs/elev-5/L-5e090a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e090a.dat right
+[ 17, 55 ] = ascii (fp) : "./hrtfs/elev-5/L-5e085a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e085a.dat right
+[ 17, 56 ] = ascii (fp) : "./hrtfs/elev-5/L-5e080a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e080a.dat right
+[ 17, 57 ] = ascii (fp) : "./hrtfs/elev-5/L-5e075a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e075a.dat right
+[ 17, 58 ] = ascii (fp) : "./hrtfs/elev-5/L-5e070a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e070a.dat right
+[ 17, 59 ] = ascii (fp) : "./hrtfs/elev-5/L-5e065a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e065a.dat right
+[ 17, 60 ] = ascii (fp) : "./hrtfs/elev-5/L-5e060a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e060a.dat right
+[ 17, 61 ] = ascii (fp) : "./hrtfs/elev-5/L-5e055a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e055a.dat right
+[ 17, 62 ] = ascii (fp) : "./hrtfs/elev-5/L-5e050a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e050a.dat right
+[ 17, 63 ] = ascii (fp) : "./hrtfs/elev-5/L-5e045a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e045a.dat right
+[ 17, 64 ] = ascii (fp) : "./hrtfs/elev-5/L-5e040a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e040a.dat right
+[ 17, 65 ] = ascii (fp) : "./hrtfs/elev-5/L-5e035a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e035a.dat right
+[ 17, 66 ] = ascii (fp) : "./hrtfs/elev-5/L-5e030a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e030a.dat right
+[ 17, 67 ] = ascii (fp) : "./hrtfs/elev-5/L-5e025a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e025a.dat right
+[ 17, 68 ] = ascii (fp) : "./hrtfs/elev-5/L-5e020a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e020a.dat right
+[ 17, 69 ] = ascii (fp) : "./hrtfs/elev-5/L-5e015a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e015a.dat right
+[ 17, 70 ] = ascii (fp) : "./hrtfs/elev-5/L-5e010a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e010a.dat right
+[ 17, 71 ] = ascii (fp) : "./hrtfs/elev-5/L-5e005a.dat left
+           + ascii (fp) : "./hrtfs/elev-5/R-5e005a.dat right
+
+[ 18,  0 ] = ascii (fp) : "./hrtfs/elev0/L0e000a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e000a.dat right
+[ 18,  1 ] = ascii (fp) : "./hrtfs/elev0/L0e355a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e355a.dat right
+[ 18,  2 ] = ascii (fp) : "./hrtfs/elev0/L0e350a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e350a.dat right
+[ 18,  3 ] = ascii (fp) : "./hrtfs/elev0/L0e345a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e345a.dat right
+[ 18,  4 ] = ascii (fp) : "./hrtfs/elev0/L0e340a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e340a.dat right
+[ 18,  5 ] = ascii (fp) : "./hrtfs/elev0/L0e335a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e335a.dat right
+[ 18,  6 ] = ascii (fp) : "./hrtfs/elev0/L0e330a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e330a.dat right
+[ 18,  7 ] = ascii (fp) : "./hrtfs/elev0/L0e325a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e325a.dat right
+[ 18,  8 ] = ascii (fp) : "./hrtfs/elev0/L0e320a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e320a.dat right
+[ 18,  9 ] = ascii (fp) : "./hrtfs/elev0/L0e315a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e315a.dat right
+[ 18, 10 ] = ascii (fp) : "./hrtfs/elev0/L0e310a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e310a.dat right
+[ 18, 11 ] = ascii (fp) : "./hrtfs/elev0/L0e305a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e305a.dat right
+[ 18, 12 ] = ascii (fp) : "./hrtfs/elev0/L0e300a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e300a.dat right
+[ 18, 13 ] = ascii (fp) : "./hrtfs/elev0/L0e295a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e295a.dat right
+[ 18, 14 ] = ascii (fp) : "./hrtfs/elev0/L0e290a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e290a.dat right
+[ 18, 15 ] = ascii (fp) : "./hrtfs/elev0/L0e285a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e285a.dat right
+[ 18, 16 ] = ascii (fp) : "./hrtfs/elev0/L0e280a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e280a.dat right
+[ 18, 17 ] = ascii (fp) : "./hrtfs/elev0/L0e275a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e275a.dat right
+[ 18, 18 ] = ascii (fp) : "./hrtfs/elev0/L0e270a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e270a.dat right
+[ 18, 19 ] = ascii (fp) : "./hrtfs/elev0/L0e265a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e265a.dat right
+[ 18, 20 ] = ascii (fp) : "./hrtfs/elev0/L0e260a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e260a.dat right
+[ 18, 21 ] = ascii (fp) : "./hrtfs/elev0/L0e255a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e255a.dat right
+[ 18, 22 ] = ascii (fp) : "./hrtfs/elev0/L0e250a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e250a.dat right
+[ 18, 23 ] = ascii (fp) : "./hrtfs/elev0/L0e245a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e245a.dat right
+[ 18, 24 ] = ascii (fp) : "./hrtfs/elev0/L0e240a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e240a.dat right
+[ 18, 25 ] = ascii (fp) : "./hrtfs/elev0/L0e235a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e235a.dat right
+[ 18, 26 ] = ascii (fp) : "./hrtfs/elev0/L0e230a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e230a.dat right
+[ 18, 27 ] = ascii (fp) : "./hrtfs/elev0/L0e225a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e225a.dat right
+[ 18, 28 ] = ascii (fp) : "./hrtfs/elev0/L0e220a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e220a.dat right
+[ 18, 29 ] = ascii (fp) : "./hrtfs/elev0/L0e215a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e215a.dat right
+[ 18, 30 ] = ascii (fp) : "./hrtfs/elev0/L0e210a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e210a.dat right
+[ 18, 31 ] = ascii (fp) : "./hrtfs/elev0/L0e205a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e205a.dat right
+[ 18, 32 ] = ascii (fp) : "./hrtfs/elev0/L0e200a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e200a.dat right
+[ 18, 33 ] = ascii (fp) : "./hrtfs/elev0/L0e195a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e195a.dat right
+[ 18, 34 ] = ascii (fp) : "./hrtfs/elev0/L0e190a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e190a.dat right
+[ 18, 35 ] = ascii (fp) : "./hrtfs/elev0/L0e185a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e185a.dat right
+[ 18, 36 ] = ascii (fp) : "./hrtfs/elev0/L0e180a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e180a.dat right
+[ 18, 37 ] = ascii (fp) : "./hrtfs/elev0/L0e175a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e175a.dat right
+[ 18, 38 ] = ascii (fp) : "./hrtfs/elev0/L0e170a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e170a.dat right
+[ 18, 39 ] = ascii (fp) : "./hrtfs/elev0/L0e165a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e165a.dat right
+[ 18, 40 ] = ascii (fp) : "./hrtfs/elev0/L0e160a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e160a.dat right
+[ 18, 41 ] = ascii (fp) : "./hrtfs/elev0/L0e155a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e155a.dat right
+[ 18, 42 ] = ascii (fp) : "./hrtfs/elev0/L0e150a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e150a.dat right
+[ 18, 43 ] = ascii (fp) : "./hrtfs/elev0/L0e145a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e145a.dat right
+[ 18, 44 ] = ascii (fp) : "./hrtfs/elev0/L0e140a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e140a.dat right
+[ 18, 45 ] = ascii (fp) : "./hrtfs/elev0/L0e135a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e135a.dat right
+[ 18, 46 ] = ascii (fp) : "./hrtfs/elev0/L0e130a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e130a.dat right
+[ 18, 47 ] = ascii (fp) : "./hrtfs/elev0/L0e125a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e125a.dat right
+[ 18, 48 ] = ascii (fp) : "./hrtfs/elev0/L0e120a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e120a.dat right
+[ 18, 49 ] = ascii (fp) : "./hrtfs/elev0/L0e115a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e115a.dat right
+[ 18, 50 ] = ascii (fp) : "./hrtfs/elev0/L0e110a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e110a.dat right
+[ 18, 51 ] = ascii (fp) : "./hrtfs/elev0/L0e105a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e105a.dat right
+[ 18, 52 ] = ascii (fp) : "./hrtfs/elev0/L0e100a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e100a.dat right
+[ 18, 53 ] = ascii (fp) : "./hrtfs/elev0/L0e095a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e095a.dat right
+[ 18, 54 ] = ascii (fp) : "./hrtfs/elev0/L0e090a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e090a.dat right
+[ 18, 55 ] = ascii (fp) : "./hrtfs/elev0/L0e085a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e085a.dat right
+[ 18, 56 ] = ascii (fp) : "./hrtfs/elev0/L0e080a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e080a.dat right
+[ 18, 57 ] = ascii (fp) : "./hrtfs/elev0/L0e075a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e075a.dat right
+[ 18, 58 ] = ascii (fp) : "./hrtfs/elev0/L0e070a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e070a.dat right
+[ 18, 59 ] = ascii (fp) : "./hrtfs/elev0/L0e065a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e065a.dat right
+[ 18, 60 ] = ascii (fp) : "./hrtfs/elev0/L0e060a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e060a.dat right
+[ 18, 61 ] = ascii (fp) : "./hrtfs/elev0/L0e055a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e055a.dat right
+[ 18, 62 ] = ascii (fp) : "./hrtfs/elev0/L0e050a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e050a.dat right
+[ 18, 63 ] = ascii (fp) : "./hrtfs/elev0/L0e045a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e045a.dat right
+[ 18, 64 ] = ascii (fp) : "./hrtfs/elev0/L0e040a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e040a.dat right
+[ 18, 65 ] = ascii (fp) : "./hrtfs/elev0/L0e035a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e035a.dat right
+[ 18, 66 ] = ascii (fp) : "./hrtfs/elev0/L0e030a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e030a.dat right
+[ 18, 67 ] = ascii (fp) : "./hrtfs/elev0/L0e025a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e025a.dat right
+[ 18, 68 ] = ascii (fp) : "./hrtfs/elev0/L0e020a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e020a.dat right
+[ 18, 69 ] = ascii (fp) : "./hrtfs/elev0/L0e015a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e015a.dat right
+[ 18, 70 ] = ascii (fp) : "./hrtfs/elev0/L0e010a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e010a.dat right
+[ 18, 71 ] = ascii (fp) : "./hrtfs/elev0/L0e005a.dat left
+           + ascii (fp) : "./hrtfs/elev0/R0e005a.dat right
+
+[ 19,  0 ] = ascii (fp) : "./hrtfs/elev5/L5e000a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e000a.dat right
+[ 19,  1 ] = ascii (fp) : "./hrtfs/elev5/L5e355a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e355a.dat right
+[ 19,  2 ] = ascii (fp) : "./hrtfs/elev5/L5e350a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e350a.dat right
+[ 19,  3 ] = ascii (fp) : "./hrtfs/elev5/L5e345a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e345a.dat right
+[ 19,  4 ] = ascii (fp) : "./hrtfs/elev5/L5e340a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e340a.dat right
+[ 19,  5 ] = ascii (fp) : "./hrtfs/elev5/L5e335a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e335a.dat right
+[ 19,  6 ] = ascii (fp) : "./hrtfs/elev5/L5e330a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e330a.dat right
+[ 19,  7 ] = ascii (fp) : "./hrtfs/elev5/L5e325a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e325a.dat right
+[ 19,  8 ] = ascii (fp) : "./hrtfs/elev5/L5e320a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e320a.dat right
+[ 19,  9 ] = ascii (fp) : "./hrtfs/elev5/L5e315a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e315a.dat right
+[ 19, 10 ] = ascii (fp) : "./hrtfs/elev5/L5e310a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e310a.dat right
+[ 19, 11 ] = ascii (fp) : "./hrtfs/elev5/L5e305a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e305a.dat right
+[ 19, 12 ] = ascii (fp) : "./hrtfs/elev5/L5e300a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e300a.dat right
+[ 19, 13 ] = ascii (fp) : "./hrtfs/elev5/L5e295a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e295a.dat right
+[ 19, 14 ] = ascii (fp) : "./hrtfs/elev5/L5e290a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e290a.dat right
+[ 19, 15 ] = ascii (fp) : "./hrtfs/elev5/L5e285a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e285a.dat right
+[ 19, 16 ] = ascii (fp) : "./hrtfs/elev5/L5e280a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e280a.dat right
+[ 19, 17 ] = ascii (fp) : "./hrtfs/elev5/L5e275a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e275a.dat right
+[ 19, 18 ] = ascii (fp) : "./hrtfs/elev5/L5e270a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e270a.dat right
+[ 19, 19 ] = ascii (fp) : "./hrtfs/elev5/L5e265a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e265a.dat right
+[ 19, 20 ] = ascii (fp) : "./hrtfs/elev5/L5e260a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e260a.dat right
+[ 19, 21 ] = ascii (fp) : "./hrtfs/elev5/L5e255a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e255a.dat right
+[ 19, 22 ] = ascii (fp) : "./hrtfs/elev5/L5e250a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e250a.dat right
+[ 19, 23 ] = ascii (fp) : "./hrtfs/elev5/L5e245a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e245a.dat right
+[ 19, 24 ] = ascii (fp) : "./hrtfs/elev5/L5e240a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e240a.dat right
+[ 19, 25 ] = ascii (fp) : "./hrtfs/elev5/L5e235a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e235a.dat right
+[ 19, 26 ] = ascii (fp) : "./hrtfs/elev5/L5e230a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e230a.dat right
+[ 19, 27 ] = ascii (fp) : "./hrtfs/elev5/L5e225a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e225a.dat right
+[ 19, 28 ] = ascii (fp) : "./hrtfs/elev5/L5e220a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e220a.dat right
+[ 19, 29 ] = ascii (fp) : "./hrtfs/elev5/L5e215a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e215a.dat right
+[ 19, 30 ] = ascii (fp) : "./hrtfs/elev5/L5e210a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e210a.dat right
+[ 19, 31 ] = ascii (fp) : "./hrtfs/elev5/L5e205a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e205a.dat right
+[ 19, 32 ] = ascii (fp) : "./hrtfs/elev5/L5e200a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e200a.dat right
+[ 19, 33 ] = ascii (fp) : "./hrtfs/elev5/L5e195a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e195a.dat right
+[ 19, 34 ] = ascii (fp) : "./hrtfs/elev5/L5e190a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e190a.dat right
+[ 19, 35 ] = ascii (fp) : "./hrtfs/elev5/L5e185a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e185a.dat right
+[ 19, 36 ] = ascii (fp) : "./hrtfs/elev5/L5e180a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e180a.dat right
+[ 19, 37 ] = ascii (fp) : "./hrtfs/elev5/L5e175a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e175a.dat right
+[ 19, 38 ] = ascii (fp) : "./hrtfs/elev5/L5e170a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e170a.dat right
+[ 19, 39 ] = ascii (fp) : "./hrtfs/elev5/L5e165a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e165a.dat right
+[ 19, 40 ] = ascii (fp) : "./hrtfs/elev5/L5e160a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e160a.dat right
+[ 19, 41 ] = ascii (fp) : "./hrtfs/elev5/L5e155a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e155a.dat right
+[ 19, 42 ] = ascii (fp) : "./hrtfs/elev5/L5e150a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e150a.dat right
+[ 19, 43 ] = ascii (fp) : "./hrtfs/elev5/L5e145a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e145a.dat right
+[ 19, 44 ] = ascii (fp) : "./hrtfs/elev5/L5e140a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e140a.dat right
+[ 19, 45 ] = ascii (fp) : "./hrtfs/elev5/L5e135a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e135a.dat right
+[ 19, 46 ] = ascii (fp) : "./hrtfs/elev5/L5e130a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e130a.dat right
+[ 19, 47 ] = ascii (fp) : "./hrtfs/elev5/L5e125a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e125a.dat right
+[ 19, 48 ] = ascii (fp) : "./hrtfs/elev5/L5e120a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e120a.dat right
+[ 19, 49 ] = ascii (fp) : "./hrtfs/elev5/L5e115a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e115a.dat right
+[ 19, 50 ] = ascii (fp) : "./hrtfs/elev5/L5e110a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e110a.dat right
+[ 19, 51 ] = ascii (fp) : "./hrtfs/elev5/L5e105a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e105a.dat right
+[ 19, 52 ] = ascii (fp) : "./hrtfs/elev5/L5e100a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e100a.dat right
+[ 19, 53 ] = ascii (fp) : "./hrtfs/elev5/L5e095a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e095a.dat right
+[ 19, 54 ] = ascii (fp) : "./hrtfs/elev5/L5e090a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e090a.dat right
+[ 19, 55 ] = ascii (fp) : "./hrtfs/elev5/L5e085a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e085a.dat right
+[ 19, 56 ] = ascii (fp) : "./hrtfs/elev5/L5e080a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e080a.dat right
+[ 19, 57 ] = ascii (fp) : "./hrtfs/elev5/L5e075a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e075a.dat right
+[ 19, 58 ] = ascii (fp) : "./hrtfs/elev5/L5e070a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e070a.dat right
+[ 19, 59 ] = ascii (fp) : "./hrtfs/elev5/L5e065a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e065a.dat right
+[ 19, 60 ] = ascii (fp) : "./hrtfs/elev5/L5e060a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e060a.dat right
+[ 19, 61 ] = ascii (fp) : "./hrtfs/elev5/L5e055a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e055a.dat right
+[ 19, 62 ] = ascii (fp) : "./hrtfs/elev5/L5e050a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e050a.dat right
+[ 19, 63 ] = ascii (fp) : "./hrtfs/elev5/L5e045a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e045a.dat right
+[ 19, 64 ] = ascii (fp) : "./hrtfs/elev5/L5e040a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e040a.dat right
+[ 19, 65 ] = ascii (fp) : "./hrtfs/elev5/L5e035a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e035a.dat right
+[ 19, 66 ] = ascii (fp) : "./hrtfs/elev5/L5e030a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e030a.dat right
+[ 19, 67 ] = ascii (fp) : "./hrtfs/elev5/L5e025a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e025a.dat right
+[ 19, 68 ] = ascii (fp) : "./hrtfs/elev5/L5e020a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e020a.dat right
+[ 19, 69 ] = ascii (fp) : "./hrtfs/elev5/L5e015a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e015a.dat right
+[ 19, 70 ] = ascii (fp) : "./hrtfs/elev5/L5e010a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e010a.dat right
+[ 19, 71 ] = ascii (fp) : "./hrtfs/elev5/L5e005a.dat left
+           + ascii (fp) : "./hrtfs/elev5/R5e005a.dat right
+
+[ 20,  0 ] = ascii (fp) : "./hrtfs/elev10/L10e000a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e000a.dat right
+[ 20,  1 ] = ascii (fp) : "./hrtfs/elev10/L10e355a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e355a.dat right
+[ 20,  2 ] = ascii (fp) : "./hrtfs/elev10/L10e350a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e350a.dat right
+[ 20,  3 ] = ascii (fp) : "./hrtfs/elev10/L10e345a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e345a.dat right
+[ 20,  4 ] = ascii (fp) : "./hrtfs/elev10/L10e340a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e340a.dat right
+[ 20,  5 ] = ascii (fp) : "./hrtfs/elev10/L10e335a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e335a.dat right
+[ 20,  6 ] = ascii (fp) : "./hrtfs/elev10/L10e330a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e330a.dat right
+[ 20,  7 ] = ascii (fp) : "./hrtfs/elev10/L10e325a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e325a.dat right
+[ 20,  8 ] = ascii (fp) : "./hrtfs/elev10/L10e320a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e320a.dat right
+[ 20,  9 ] = ascii (fp) : "./hrtfs/elev10/L10e315a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e315a.dat right
+[ 20, 10 ] = ascii (fp) : "./hrtfs/elev10/L10e310a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e310a.dat right
+[ 20, 11 ] = ascii (fp) : "./hrtfs/elev10/L10e305a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e305a.dat right
+[ 20, 12 ] = ascii (fp) : "./hrtfs/elev10/L10e300a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e300a.dat right
+[ 20, 13 ] = ascii (fp) : "./hrtfs/elev10/L10e295a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e295a.dat right
+[ 20, 14 ] = ascii (fp) : "./hrtfs/elev10/L10e290a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e290a.dat right
+[ 20, 15 ] = ascii (fp) : "./hrtfs/elev10/L10e285a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e285a.dat right
+[ 20, 16 ] = ascii (fp) : "./hrtfs/elev10/L10e280a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e280a.dat right
+[ 20, 17 ] = ascii (fp) : "./hrtfs/elev10/L10e275a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e275a.dat right
+[ 20, 18 ] = ascii (fp) : "./hrtfs/elev10/L10e270a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e270a.dat right
+[ 20, 19 ] = ascii (fp) : "./hrtfs/elev10/L10e265a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e265a.dat right
+[ 20, 20 ] = ascii (fp) : "./hrtfs/elev10/L10e260a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e260a.dat right
+[ 20, 21 ] = ascii (fp) : "./hrtfs/elev10/L10e255a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e255a.dat right
+[ 20, 22 ] = ascii (fp) : "./hrtfs/elev10/L10e250a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e250a.dat right
+[ 20, 23 ] = ascii (fp) : "./hrtfs/elev10/L10e245a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e245a.dat right
+[ 20, 24 ] = ascii (fp) : "./hrtfs/elev10/L10e240a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e240a.dat right
+[ 20, 25 ] = ascii (fp) : "./hrtfs/elev10/L10e235a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e235a.dat right
+[ 20, 26 ] = ascii (fp) : "./hrtfs/elev10/L10e230a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e230a.dat right
+[ 20, 27 ] = ascii (fp) : "./hrtfs/elev10/L10e225a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e225a.dat right
+[ 20, 28 ] = ascii (fp) : "./hrtfs/elev10/L10e220a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e220a.dat right
+[ 20, 29 ] = ascii (fp) : "./hrtfs/elev10/L10e215a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e215a.dat right
+[ 20, 30 ] = ascii (fp) : "./hrtfs/elev10/L10e210a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e210a.dat right
+[ 20, 31 ] = ascii (fp) : "./hrtfs/elev10/L10e205a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e205a.dat right
+[ 20, 32 ] = ascii (fp) : "./hrtfs/elev10/L10e200a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e200a.dat right
+[ 20, 33 ] = ascii (fp) : "./hrtfs/elev10/L10e195a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e195a.dat right
+[ 20, 34 ] = ascii (fp) : "./hrtfs/elev10/L10e190a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e190a.dat right
+[ 20, 35 ] = ascii (fp) : "./hrtfs/elev10/L10e185a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e185a.dat right
+[ 20, 36 ] = ascii (fp) : "./hrtfs/elev10/L10e180a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e180a.dat right
+[ 20, 37 ] = ascii (fp) : "./hrtfs/elev10/L10e175a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e175a.dat right
+[ 20, 38 ] = ascii (fp) : "./hrtfs/elev10/L10e170a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e170a.dat right
+[ 20, 39 ] = ascii (fp) : "./hrtfs/elev10/L10e165a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e165a.dat right
+[ 20, 40 ] = ascii (fp) : "./hrtfs/elev10/L10e160a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e160a.dat right
+[ 20, 41 ] = ascii (fp) : "./hrtfs/elev10/L10e155a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e155a.dat right
+[ 20, 42 ] = ascii (fp) : "./hrtfs/elev10/L10e150a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e150a.dat right
+[ 20, 43 ] = ascii (fp) : "./hrtfs/elev10/L10e145a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e145a.dat right
+[ 20, 44 ] = ascii (fp) : "./hrtfs/elev10/L10e140a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e140a.dat right
+[ 20, 45 ] = ascii (fp) : "./hrtfs/elev10/L10e135a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e135a.dat right
+[ 20, 46 ] = ascii (fp) : "./hrtfs/elev10/L10e130a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e130a.dat right
+[ 20, 47 ] = ascii (fp) : "./hrtfs/elev10/L10e125a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e125a.dat right
+[ 20, 48 ] = ascii (fp) : "./hrtfs/elev10/L10e120a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e120a.dat right
+[ 20, 49 ] = ascii (fp) : "./hrtfs/elev10/L10e115a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e115a.dat right
+[ 20, 50 ] = ascii (fp) : "./hrtfs/elev10/L10e110a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e110a.dat right
+[ 20, 51 ] = ascii (fp) : "./hrtfs/elev10/L10e105a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e105a.dat right
+[ 20, 52 ] = ascii (fp) : "./hrtfs/elev10/L10e100a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e100a.dat right
+[ 20, 53 ] = ascii (fp) : "./hrtfs/elev10/L10e095a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e095a.dat right
+[ 20, 54 ] = ascii (fp) : "./hrtfs/elev10/L10e090a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e090a.dat right
+[ 20, 55 ] = ascii (fp) : "./hrtfs/elev10/L10e085a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e085a.dat right
+[ 20, 56 ] = ascii (fp) : "./hrtfs/elev10/L10e080a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e080a.dat right
+[ 20, 57 ] = ascii (fp) : "./hrtfs/elev10/L10e075a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e075a.dat right
+[ 20, 58 ] = ascii (fp) : "./hrtfs/elev10/L10e070a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e070a.dat right
+[ 20, 59 ] = ascii (fp) : "./hrtfs/elev10/L10e065a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e065a.dat right
+[ 20, 60 ] = ascii (fp) : "./hrtfs/elev10/L10e060a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e060a.dat right
+[ 20, 61 ] = ascii (fp) : "./hrtfs/elev10/L10e055a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e055a.dat right
+[ 20, 62 ] = ascii (fp) : "./hrtfs/elev10/L10e050a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e050a.dat right
+[ 20, 63 ] = ascii (fp) : "./hrtfs/elev10/L10e045a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e045a.dat right
+[ 20, 64 ] = ascii (fp) : "./hrtfs/elev10/L10e040a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e040a.dat right
+[ 20, 65 ] = ascii (fp) : "./hrtfs/elev10/L10e035a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e035a.dat right
+[ 20, 66 ] = ascii (fp) : "./hrtfs/elev10/L10e030a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e030a.dat right
+[ 20, 67 ] = ascii (fp) : "./hrtfs/elev10/L10e025a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e025a.dat right
+[ 20, 68 ] = ascii (fp) : "./hrtfs/elev10/L10e020a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e020a.dat right
+[ 20, 69 ] = ascii (fp) : "./hrtfs/elev10/L10e015a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e015a.dat right
+[ 20, 70 ] = ascii (fp) : "./hrtfs/elev10/L10e010a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e010a.dat right
+[ 20, 71 ] = ascii (fp) : "./hrtfs/elev10/L10e005a.dat left
+           + ascii (fp) : "./hrtfs/elev10/R10e005a.dat right
+
+[ 21,  0 ] = ascii (fp) : "./hrtfs/elev15/L15e000a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e000a.dat right
+[ 21,  1 ] = ascii (fp) : "./hrtfs/elev15/L15e355a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e355a.dat right
+[ 21,  2 ] = ascii (fp) : "./hrtfs/elev15/L15e350a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e350a.dat right
+[ 21,  3 ] = ascii (fp) : "./hrtfs/elev15/L15e345a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e345a.dat right
+[ 21,  4 ] = ascii (fp) : "./hrtfs/elev15/L15e340a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e340a.dat right
+[ 21,  5 ] = ascii (fp) : "./hrtfs/elev15/L15e335a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e335a.dat right
+[ 21,  6 ] = ascii (fp) : "./hrtfs/elev15/L15e330a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e330a.dat right
+[ 21,  7 ] = ascii (fp) : "./hrtfs/elev15/L15e325a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e325a.dat right
+[ 21,  8 ] = ascii (fp) : "./hrtfs/elev15/L15e320a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e320a.dat right
+[ 21,  9 ] = ascii (fp) : "./hrtfs/elev15/L15e315a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e315a.dat right
+[ 21, 10 ] = ascii (fp) : "./hrtfs/elev15/L15e310a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e310a.dat right
+[ 21, 11 ] = ascii (fp) : "./hrtfs/elev15/L15e305a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e305a.dat right
+[ 21, 12 ] = ascii (fp) : "./hrtfs/elev15/L15e300a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e300a.dat right
+[ 21, 13 ] = ascii (fp) : "./hrtfs/elev15/L15e295a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e295a.dat right
+[ 21, 14 ] = ascii (fp) : "./hrtfs/elev15/L15e290a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e290a.dat right
+[ 21, 15 ] = ascii (fp) : "./hrtfs/elev15/L15e285a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e285a.dat right
+[ 21, 16 ] = ascii (fp) : "./hrtfs/elev15/L15e280a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e280a.dat right
+[ 21, 17 ] = ascii (fp) : "./hrtfs/elev15/L15e275a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e275a.dat right
+[ 21, 18 ] = ascii (fp) : "./hrtfs/elev15/L15e270a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e270a.dat right
+[ 21, 19 ] = ascii (fp) : "./hrtfs/elev15/L15e265a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e265a.dat right
+[ 21, 20 ] = ascii (fp) : "./hrtfs/elev15/L15e260a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e260a.dat right
+[ 21, 21 ] = ascii (fp) : "./hrtfs/elev15/L15e255a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e255a.dat right
+[ 21, 22 ] = ascii (fp) : "./hrtfs/elev15/L15e250a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e250a.dat right
+[ 21, 23 ] = ascii (fp) : "./hrtfs/elev15/L15e245a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e245a.dat right
+[ 21, 24 ] = ascii (fp) : "./hrtfs/elev15/L15e240a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e240a.dat right
+[ 21, 25 ] = ascii (fp) : "./hrtfs/elev15/L15e235a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e235a.dat right
+[ 21, 26 ] = ascii (fp) : "./hrtfs/elev15/L15e230a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e230a.dat right
+[ 21, 27 ] = ascii (fp) : "./hrtfs/elev15/L15e225a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e225a.dat right
+[ 21, 28 ] = ascii (fp) : "./hrtfs/elev15/L15e220a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e220a.dat right
+[ 21, 29 ] = ascii (fp) : "./hrtfs/elev15/L15e215a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e215a.dat right
+[ 21, 30 ] = ascii (fp) : "./hrtfs/elev15/L15e210a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e210a.dat right
+[ 21, 31 ] = ascii (fp) : "./hrtfs/elev15/L15e205a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e205a.dat right
+[ 21, 32 ] = ascii (fp) : "./hrtfs/elev15/L15e200a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e200a.dat right
+[ 21, 33 ] = ascii (fp) : "./hrtfs/elev15/L15e195a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e195a.dat right
+[ 21, 34 ] = ascii (fp) : "./hrtfs/elev15/L15e190a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e190a.dat right
+[ 21, 35 ] = ascii (fp) : "./hrtfs/elev15/L15e185a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e185a.dat right
+[ 21, 36 ] = ascii (fp) : "./hrtfs/elev15/L15e180a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e180a.dat right
+[ 21, 37 ] = ascii (fp) : "./hrtfs/elev15/L15e175a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e175a.dat right
+[ 21, 38 ] = ascii (fp) : "./hrtfs/elev15/L15e170a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e170a.dat right
+[ 21, 39 ] = ascii (fp) : "./hrtfs/elev15/L15e165a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e165a.dat right
+[ 21, 40 ] = ascii (fp) : "./hrtfs/elev15/L15e160a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e160a.dat right
+[ 21, 41 ] = ascii (fp) : "./hrtfs/elev15/L15e155a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e155a.dat right
+[ 21, 42 ] = ascii (fp) : "./hrtfs/elev15/L15e150a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e150a.dat right
+[ 21, 43 ] = ascii (fp) : "./hrtfs/elev15/L15e145a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e145a.dat right
+[ 21, 44 ] = ascii (fp) : "./hrtfs/elev15/L15e140a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e140a.dat right
+[ 21, 45 ] = ascii (fp) : "./hrtfs/elev15/L15e135a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e135a.dat right
+[ 21, 46 ] = ascii (fp) : "./hrtfs/elev15/L15e130a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e130a.dat right
+[ 21, 47 ] = ascii (fp) : "./hrtfs/elev15/L15e125a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e125a.dat right
+[ 21, 48 ] = ascii (fp) : "./hrtfs/elev15/L15e120a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e120a.dat right
+[ 21, 49 ] = ascii (fp) : "./hrtfs/elev15/L15e115a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e115a.dat right
+[ 21, 50 ] = ascii (fp) : "./hrtfs/elev15/L15e110a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e110a.dat right
+[ 21, 51 ] = ascii (fp) : "./hrtfs/elev15/L15e105a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e105a.dat right
+[ 21, 52 ] = ascii (fp) : "./hrtfs/elev15/L15e100a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e100a.dat right
+[ 21, 53 ] = ascii (fp) : "./hrtfs/elev15/L15e095a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e095a.dat right
+[ 21, 54 ] = ascii (fp) : "./hrtfs/elev15/L15e090a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e090a.dat right
+[ 21, 55 ] = ascii (fp) : "./hrtfs/elev15/L15e085a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e085a.dat right
+[ 21, 56 ] = ascii (fp) : "./hrtfs/elev15/L15e080a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e080a.dat right
+[ 21, 57 ] = ascii (fp) : "./hrtfs/elev15/L15e075a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e075a.dat right
+[ 21, 58 ] = ascii (fp) : "./hrtfs/elev15/L15e070a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e070a.dat right
+[ 21, 59 ] = ascii (fp) : "./hrtfs/elev15/L15e065a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e065a.dat right
+[ 21, 60 ] = ascii (fp) : "./hrtfs/elev15/L15e060a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e060a.dat right
+[ 21, 61 ] = ascii (fp) : "./hrtfs/elev15/L15e055a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e055a.dat right
+[ 21, 62 ] = ascii (fp) : "./hrtfs/elev15/L15e050a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e050a.dat right
+[ 21, 63 ] = ascii (fp) : "./hrtfs/elev15/L15e045a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e045a.dat right
+[ 21, 64 ] = ascii (fp) : "./hrtfs/elev15/L15e040a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e040a.dat right
+[ 21, 65 ] = ascii (fp) : "./hrtfs/elev15/L15e035a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e035a.dat right
+[ 21, 66 ] = ascii (fp) : "./hrtfs/elev15/L15e030a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e030a.dat right
+[ 21, 67 ] = ascii (fp) : "./hrtfs/elev15/L15e025a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e025a.dat right
+[ 21, 68 ] = ascii (fp) : "./hrtfs/elev15/L15e020a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e020a.dat right
+[ 21, 69 ] = ascii (fp) : "./hrtfs/elev15/L15e015a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e015a.dat right
+[ 21, 70 ] = ascii (fp) : "./hrtfs/elev15/L15e010a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e010a.dat right
+[ 21, 71 ] = ascii (fp) : "./hrtfs/elev15/L15e005a.dat left
+           + ascii (fp) : "./hrtfs/elev15/R15e005a.dat right
+
+[ 22,  0 ] = ascii (fp) : "./hrtfs/elev20/L20e000a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e000a.dat right
+[ 22,  1 ] = ascii (fp) : "./hrtfs/elev20/L20e355a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e355a.dat right
+[ 22,  2 ] = ascii (fp) : "./hrtfs/elev20/L20e350a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e350a.dat right
+[ 22,  3 ] = ascii (fp) : "./hrtfs/elev20/L20e345a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e345a.dat right
+[ 22,  4 ] = ascii (fp) : "./hrtfs/elev20/L20e340a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e340a.dat right
+[ 22,  5 ] = ascii (fp) : "./hrtfs/elev20/L20e335a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e335a.dat right
+[ 22,  6 ] = ascii (fp) : "./hrtfs/elev20/L20e330a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e330a.dat right
+[ 22,  7 ] = ascii (fp) : "./hrtfs/elev20/L20e325a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e325a.dat right
+[ 22,  8 ] = ascii (fp) : "./hrtfs/elev20/L20e320a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e320a.dat right
+[ 22,  9 ] = ascii (fp) : "./hrtfs/elev20/L20e315a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e315a.dat right
+[ 22, 10 ] = ascii (fp) : "./hrtfs/elev20/L20e310a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e310a.dat right
+[ 22, 11 ] = ascii (fp) : "./hrtfs/elev20/L20e305a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e305a.dat right
+[ 22, 12 ] = ascii (fp) : "./hrtfs/elev20/L20e300a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e300a.dat right
+[ 22, 13 ] = ascii (fp) : "./hrtfs/elev20/L20e295a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e295a.dat right
+[ 22, 14 ] = ascii (fp) : "./hrtfs/elev20/L20e290a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e290a.dat right
+[ 22, 15 ] = ascii (fp) : "./hrtfs/elev20/L20e285a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e285a.dat right
+[ 22, 16 ] = ascii (fp) : "./hrtfs/elev20/L20e280a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e280a.dat right
+[ 22, 17 ] = ascii (fp) : "./hrtfs/elev20/L20e275a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e275a.dat right
+[ 22, 18 ] = ascii (fp) : "./hrtfs/elev20/L20e270a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e270a.dat right
+[ 22, 19 ] = ascii (fp) : "./hrtfs/elev20/L20e265a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e265a.dat right
+[ 22, 20 ] = ascii (fp) : "./hrtfs/elev20/L20e260a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e260a.dat right
+[ 22, 21 ] = ascii (fp) : "./hrtfs/elev20/L20e255a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e255a.dat right
+[ 22, 22 ] = ascii (fp) : "./hrtfs/elev20/L20e250a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e250a.dat right
+[ 22, 23 ] = ascii (fp) : "./hrtfs/elev20/L20e245a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e245a.dat right
+[ 22, 24 ] = ascii (fp) : "./hrtfs/elev20/L20e240a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e240a.dat right
+[ 22, 25 ] = ascii (fp) : "./hrtfs/elev20/L20e235a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e235a.dat right
+[ 22, 26 ] = ascii (fp) : "./hrtfs/elev20/L20e230a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e230a.dat right
+[ 22, 27 ] = ascii (fp) : "./hrtfs/elev20/L20e225a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e225a.dat right
+[ 22, 28 ] = ascii (fp) : "./hrtfs/elev20/L20e220a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e220a.dat right
+[ 22, 29 ] = ascii (fp) : "./hrtfs/elev20/L20e215a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e215a.dat right
+[ 22, 30 ] = ascii (fp) : "./hrtfs/elev20/L20e210a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e210a.dat right
+[ 22, 31 ] = ascii (fp) : "./hrtfs/elev20/L20e205a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e205a.dat right
+[ 22, 32 ] = ascii (fp) : "./hrtfs/elev20/L20e200a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e200a.dat right
+[ 22, 33 ] = ascii (fp) : "./hrtfs/elev20/L20e195a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e195a.dat right
+[ 22, 34 ] = ascii (fp) : "./hrtfs/elev20/L20e190a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e190a.dat right
+[ 22, 35 ] = ascii (fp) : "./hrtfs/elev20/L20e185a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e185a.dat right
+[ 22, 36 ] = ascii (fp) : "./hrtfs/elev20/L20e180a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e180a.dat right
+[ 22, 37 ] = ascii (fp) : "./hrtfs/elev20/L20e175a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e175a.dat right
+[ 22, 38 ] = ascii (fp) : "./hrtfs/elev20/L20e170a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e170a.dat right
+[ 22, 39 ] = ascii (fp) : "./hrtfs/elev20/L20e165a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e165a.dat right
+[ 22, 40 ] = ascii (fp) : "./hrtfs/elev20/L20e160a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e160a.dat right
+[ 22, 41 ] = ascii (fp) : "./hrtfs/elev20/L20e155a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e155a.dat right
+[ 22, 42 ] = ascii (fp) : "./hrtfs/elev20/L20e150a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e150a.dat right
+[ 22, 43 ] = ascii (fp) : "./hrtfs/elev20/L20e145a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e145a.dat right
+[ 22, 44 ] = ascii (fp) : "./hrtfs/elev20/L20e140a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e140a.dat right
+[ 22, 45 ] = ascii (fp) : "./hrtfs/elev20/L20e135a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e135a.dat right
+[ 22, 46 ] = ascii (fp) : "./hrtfs/elev20/L20e130a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e130a.dat right
+[ 22, 47 ] = ascii (fp) : "./hrtfs/elev20/L20e125a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e125a.dat right
+[ 22, 48 ] = ascii (fp) : "./hrtfs/elev20/L20e120a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e120a.dat right
+[ 22, 49 ] = ascii (fp) : "./hrtfs/elev20/L20e115a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e115a.dat right
+[ 22, 50 ] = ascii (fp) : "./hrtfs/elev20/L20e110a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e110a.dat right
+[ 22, 51 ] = ascii (fp) : "./hrtfs/elev20/L20e105a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e105a.dat right
+[ 22, 52 ] = ascii (fp) : "./hrtfs/elev20/L20e100a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e100a.dat right
+[ 22, 53 ] = ascii (fp) : "./hrtfs/elev20/L20e095a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e095a.dat right
+[ 22, 54 ] = ascii (fp) : "./hrtfs/elev20/L20e090a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e090a.dat right
+[ 22, 55 ] = ascii (fp) : "./hrtfs/elev20/L20e085a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e085a.dat right
+[ 22, 56 ] = ascii (fp) : "./hrtfs/elev20/L20e080a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e080a.dat right
+[ 22, 57 ] = ascii (fp) : "./hrtfs/elev20/L20e075a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e075a.dat right
+[ 22, 58 ] = ascii (fp) : "./hrtfs/elev20/L20e070a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e070a.dat right
+[ 22, 59 ] = ascii (fp) : "./hrtfs/elev20/L20e065a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e065a.dat right
+[ 22, 60 ] = ascii (fp) : "./hrtfs/elev20/L20e060a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e060a.dat right
+[ 22, 61 ] = ascii (fp) : "./hrtfs/elev20/L20e055a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e055a.dat right
+[ 22, 62 ] = ascii (fp) : "./hrtfs/elev20/L20e050a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e050a.dat right
+[ 22, 63 ] = ascii (fp) : "./hrtfs/elev20/L20e045a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e045a.dat right
+[ 22, 64 ] = ascii (fp) : "./hrtfs/elev20/L20e040a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e040a.dat right
+[ 22, 65 ] = ascii (fp) : "./hrtfs/elev20/L20e035a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e035a.dat right
+[ 22, 66 ] = ascii (fp) : "./hrtfs/elev20/L20e030a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e030a.dat right
+[ 22, 67 ] = ascii (fp) : "./hrtfs/elev20/L20e025a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e025a.dat right
+[ 22, 68 ] = ascii (fp) : "./hrtfs/elev20/L20e020a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e020a.dat right
+[ 22, 69 ] = ascii (fp) : "./hrtfs/elev20/L20e015a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e015a.dat right
+[ 22, 70 ] = ascii (fp) : "./hrtfs/elev20/L20e010a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e010a.dat right
+[ 22, 71 ] = ascii (fp) : "./hrtfs/elev20/L20e005a.dat left
+           + ascii (fp) : "./hrtfs/elev20/R20e005a.dat right
+
+[ 23,  0 ] = ascii (fp) : "./hrtfs/elev25/L25e000a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e000a.dat right
+[ 23,  1 ] = ascii (fp) : "./hrtfs/elev25/L25e355a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e355a.dat right
+[ 23,  2 ] = ascii (fp) : "./hrtfs/elev25/L25e350a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e350a.dat right
+[ 23,  3 ] = ascii (fp) : "./hrtfs/elev25/L25e345a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e345a.dat right
+[ 23,  4 ] = ascii (fp) : "./hrtfs/elev25/L25e340a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e340a.dat right
+[ 23,  5 ] = ascii (fp) : "./hrtfs/elev25/L25e335a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e335a.dat right
+[ 23,  6 ] = ascii (fp) : "./hrtfs/elev25/L25e330a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e330a.dat right
+[ 23,  7 ] = ascii (fp) : "./hrtfs/elev25/L25e325a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e325a.dat right
+[ 23,  8 ] = ascii (fp) : "./hrtfs/elev25/L25e320a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e320a.dat right
+[ 23,  9 ] = ascii (fp) : "./hrtfs/elev25/L25e315a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e315a.dat right
+[ 23, 10 ] = ascii (fp) : "./hrtfs/elev25/L25e310a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e310a.dat right
+[ 23, 11 ] = ascii (fp) : "./hrtfs/elev25/L25e305a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e305a.dat right
+[ 23, 12 ] = ascii (fp) : "./hrtfs/elev25/L25e300a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e300a.dat right
+[ 23, 13 ] = ascii (fp) : "./hrtfs/elev25/L25e295a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e295a.dat right
+[ 23, 14 ] = ascii (fp) : "./hrtfs/elev25/L25e290a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e290a.dat right
+[ 23, 15 ] = ascii (fp) : "./hrtfs/elev25/L25e285a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e285a.dat right
+[ 23, 16 ] = ascii (fp) : "./hrtfs/elev25/L25e280a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e280a.dat right
+[ 23, 17 ] = ascii (fp) : "./hrtfs/elev25/L25e275a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e275a.dat right
+[ 23, 18 ] = ascii (fp) : "./hrtfs/elev25/L25e270a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e270a.dat right
+[ 23, 19 ] = ascii (fp) : "./hrtfs/elev25/L25e265a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e265a.dat right
+[ 23, 20 ] = ascii (fp) : "./hrtfs/elev25/L25e260a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e260a.dat right
+[ 23, 21 ] = ascii (fp) : "./hrtfs/elev25/L25e255a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e255a.dat right
+[ 23, 22 ] = ascii (fp) : "./hrtfs/elev25/L25e250a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e250a.dat right
+[ 23, 23 ] = ascii (fp) : "./hrtfs/elev25/L25e245a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e245a.dat right
+[ 23, 24 ] = ascii (fp) : "./hrtfs/elev25/L25e240a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e240a.dat right
+[ 23, 25 ] = ascii (fp) : "./hrtfs/elev25/L25e235a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e235a.dat right
+[ 23, 26 ] = ascii (fp) : "./hrtfs/elev25/L25e230a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e230a.dat right
+[ 23, 27 ] = ascii (fp) : "./hrtfs/elev25/L25e225a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e225a.dat right
+[ 23, 28 ] = ascii (fp) : "./hrtfs/elev25/L25e220a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e220a.dat right
+[ 23, 29 ] = ascii (fp) : "./hrtfs/elev25/L25e215a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e215a.dat right
+[ 23, 30 ] = ascii (fp) : "./hrtfs/elev25/L25e210a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e210a.dat right
+[ 23, 31 ] = ascii (fp) : "./hrtfs/elev25/L25e205a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e205a.dat right
+[ 23, 32 ] = ascii (fp) : "./hrtfs/elev25/L25e200a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e200a.dat right
+[ 23, 33 ] = ascii (fp) : "./hrtfs/elev25/L25e195a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e195a.dat right
+[ 23, 34 ] = ascii (fp) : "./hrtfs/elev25/L25e190a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e190a.dat right
+[ 23, 35 ] = ascii (fp) : "./hrtfs/elev25/L25e185a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e185a.dat right
+[ 23, 36 ] = ascii (fp) : "./hrtfs/elev25/L25e180a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e180a.dat right
+[ 23, 37 ] = ascii (fp) : "./hrtfs/elev25/L25e175a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e175a.dat right
+[ 23, 38 ] = ascii (fp) : "./hrtfs/elev25/L25e170a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e170a.dat right
+[ 23, 39 ] = ascii (fp) : "./hrtfs/elev25/L25e165a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e165a.dat right
+[ 23, 40 ] = ascii (fp) : "./hrtfs/elev25/L25e160a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e160a.dat right
+[ 23, 41 ] = ascii (fp) : "./hrtfs/elev25/L25e155a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e155a.dat right
+[ 23, 42 ] = ascii (fp) : "./hrtfs/elev25/L25e150a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e150a.dat right
+[ 23, 43 ] = ascii (fp) : "./hrtfs/elev25/L25e145a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e145a.dat right
+[ 23, 44 ] = ascii (fp) : "./hrtfs/elev25/L25e140a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e140a.dat right
+[ 23, 45 ] = ascii (fp) : "./hrtfs/elev25/L25e135a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e135a.dat right
+[ 23, 46 ] = ascii (fp) : "./hrtfs/elev25/L25e130a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e130a.dat right
+[ 23, 47 ] = ascii (fp) : "./hrtfs/elev25/L25e125a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e125a.dat right
+[ 23, 48 ] = ascii (fp) : "./hrtfs/elev25/L25e120a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e120a.dat right
+[ 23, 49 ] = ascii (fp) : "./hrtfs/elev25/L25e115a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e115a.dat right
+[ 23, 50 ] = ascii (fp) : "./hrtfs/elev25/L25e110a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e110a.dat right
+[ 23, 51 ] = ascii (fp) : "./hrtfs/elev25/L25e105a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e105a.dat right
+[ 23, 52 ] = ascii (fp) : "./hrtfs/elev25/L25e100a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e100a.dat right
+[ 23, 53 ] = ascii (fp) : "./hrtfs/elev25/L25e095a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e095a.dat right
+[ 23, 54 ] = ascii (fp) : "./hrtfs/elev25/L25e090a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e090a.dat right
+[ 23, 55 ] = ascii (fp) : "./hrtfs/elev25/L25e085a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e085a.dat right
+[ 23, 56 ] = ascii (fp) : "./hrtfs/elev25/L25e080a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e080a.dat right
+[ 23, 57 ] = ascii (fp) : "./hrtfs/elev25/L25e075a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e075a.dat right
+[ 23, 58 ] = ascii (fp) : "./hrtfs/elev25/L25e070a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e070a.dat right
+[ 23, 59 ] = ascii (fp) : "./hrtfs/elev25/L25e065a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e065a.dat right
+[ 23, 60 ] = ascii (fp) : "./hrtfs/elev25/L25e060a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e060a.dat right
+[ 23, 61 ] = ascii (fp) : "./hrtfs/elev25/L25e055a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e055a.dat right
+[ 23, 62 ] = ascii (fp) : "./hrtfs/elev25/L25e050a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e050a.dat right
+[ 23, 63 ] = ascii (fp) : "./hrtfs/elev25/L25e045a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e045a.dat right
+[ 23, 64 ] = ascii (fp) : "./hrtfs/elev25/L25e040a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e040a.dat right
+[ 23, 65 ] = ascii (fp) : "./hrtfs/elev25/L25e035a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e035a.dat right
+[ 23, 66 ] = ascii (fp) : "./hrtfs/elev25/L25e030a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e030a.dat right
+[ 23, 67 ] = ascii (fp) : "./hrtfs/elev25/L25e025a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e025a.dat right
+[ 23, 68 ] = ascii (fp) : "./hrtfs/elev25/L25e020a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e020a.dat right
+[ 23, 69 ] = ascii (fp) : "./hrtfs/elev25/L25e015a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e015a.dat right
+[ 23, 70 ] = ascii (fp) : "./hrtfs/elev25/L25e010a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e010a.dat right
+[ 23, 71 ] = ascii (fp) : "./hrtfs/elev25/L25e005a.dat left
+           + ascii (fp) : "./hrtfs/elev25/R25e005a.dat right
+
+[ 24,  0 ] = ascii (fp) : "./hrtfs/elev30/L30e000a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e000a.dat right
+[ 24,  1 ] = ascii (fp) : "./hrtfs/elev30/L30e355a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e355a.dat right
+[ 24,  2 ] = ascii (fp) : "./hrtfs/elev30/L30e350a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e350a.dat right
+[ 24,  3 ] = ascii (fp) : "./hrtfs/elev30/L30e345a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e345a.dat right
+[ 24,  4 ] = ascii (fp) : "./hrtfs/elev30/L30e340a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e340a.dat right
+[ 24,  5 ] = ascii (fp) : "./hrtfs/elev30/L30e335a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e335a.dat right
+[ 24,  6 ] = ascii (fp) : "./hrtfs/elev30/L30e330a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e330a.dat right
+[ 24,  7 ] = ascii (fp) : "./hrtfs/elev30/L30e325a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e325a.dat right
+[ 24,  8 ] = ascii (fp) : "./hrtfs/elev30/L30e320a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e320a.dat right
+[ 24,  9 ] = ascii (fp) : "./hrtfs/elev30/L30e315a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e315a.dat right
+[ 24, 10 ] = ascii (fp) : "./hrtfs/elev30/L30e310a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e310a.dat right
+[ 24, 11 ] = ascii (fp) : "./hrtfs/elev30/L30e305a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e305a.dat right
+[ 24, 12 ] = ascii (fp) : "./hrtfs/elev30/L30e300a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e300a.dat right
+[ 24, 13 ] = ascii (fp) : "./hrtfs/elev30/L30e295a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e295a.dat right
+[ 24, 14 ] = ascii (fp) : "./hrtfs/elev30/L30e290a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e290a.dat right
+[ 24, 15 ] = ascii (fp) : "./hrtfs/elev30/L30e285a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e285a.dat right
+[ 24, 16 ] = ascii (fp) : "./hrtfs/elev30/L30e280a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e280a.dat right
+[ 24, 17 ] = ascii (fp) : "./hrtfs/elev30/L30e275a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e275a.dat right
+[ 24, 18 ] = ascii (fp) : "./hrtfs/elev30/L30e270a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e270a.dat right
+[ 24, 19 ] = ascii (fp) : "./hrtfs/elev30/L30e265a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e265a.dat right
+[ 24, 20 ] = ascii (fp) : "./hrtfs/elev30/L30e260a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e260a.dat right
+[ 24, 21 ] = ascii (fp) : "./hrtfs/elev30/L30e255a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e255a.dat right
+[ 24, 22 ] = ascii (fp) : "./hrtfs/elev30/L30e250a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e250a.dat right
+[ 24, 23 ] = ascii (fp) : "./hrtfs/elev30/L30e245a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e245a.dat right
+[ 24, 24 ] = ascii (fp) : "./hrtfs/elev30/L30e240a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e240a.dat right
+[ 24, 25 ] = ascii (fp) : "./hrtfs/elev30/L30e235a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e235a.dat right
+[ 24, 26 ] = ascii (fp) : "./hrtfs/elev30/L30e230a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e230a.dat right
+[ 24, 27 ] = ascii (fp) : "./hrtfs/elev30/L30e225a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e225a.dat right
+[ 24, 28 ] = ascii (fp) : "./hrtfs/elev30/L30e220a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e220a.dat right
+[ 24, 29 ] = ascii (fp) : "./hrtfs/elev30/L30e215a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e215a.dat right
+[ 24, 30 ] = ascii (fp) : "./hrtfs/elev30/L30e210a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e210a.dat right
+[ 24, 31 ] = ascii (fp) : "./hrtfs/elev30/L30e205a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e205a.dat right
+[ 24, 32 ] = ascii (fp) : "./hrtfs/elev30/L30e200a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e200a.dat right
+[ 24, 33 ] = ascii (fp) : "./hrtfs/elev30/L30e195a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e195a.dat right
+[ 24, 34 ] = ascii (fp) : "./hrtfs/elev30/L30e190a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e190a.dat right
+[ 24, 35 ] = ascii (fp) : "./hrtfs/elev30/L30e185a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e185a.dat right
+[ 24, 36 ] = ascii (fp) : "./hrtfs/elev30/L30e180a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e180a.dat right
+[ 24, 37 ] = ascii (fp) : "./hrtfs/elev30/L30e175a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e175a.dat right
+[ 24, 38 ] = ascii (fp) : "./hrtfs/elev30/L30e170a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e170a.dat right
+[ 24, 39 ] = ascii (fp) : "./hrtfs/elev30/L30e165a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e165a.dat right
+[ 24, 40 ] = ascii (fp) : "./hrtfs/elev30/L30e160a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e160a.dat right
+[ 24, 41 ] = ascii (fp) : "./hrtfs/elev30/L30e155a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e155a.dat right
+[ 24, 42 ] = ascii (fp) : "./hrtfs/elev30/L30e150a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e150a.dat right
+[ 24, 43 ] = ascii (fp) : "./hrtfs/elev30/L30e145a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e145a.dat right
+[ 24, 44 ] = ascii (fp) : "./hrtfs/elev30/L30e140a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e140a.dat right
+[ 24, 45 ] = ascii (fp) : "./hrtfs/elev30/L30e135a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e135a.dat right
+[ 24, 46 ] = ascii (fp) : "./hrtfs/elev30/L30e130a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e130a.dat right
+[ 24, 47 ] = ascii (fp) : "./hrtfs/elev30/L30e125a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e125a.dat right
+[ 24, 48 ] = ascii (fp) : "./hrtfs/elev30/L30e120a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e120a.dat right
+[ 24, 49 ] = ascii (fp) : "./hrtfs/elev30/L30e115a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e115a.dat right
+[ 24, 50 ] = ascii (fp) : "./hrtfs/elev30/L30e110a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e110a.dat right
+[ 24, 51 ] = ascii (fp) : "./hrtfs/elev30/L30e105a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e105a.dat right
+[ 24, 52 ] = ascii (fp) : "./hrtfs/elev30/L30e100a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e100a.dat right
+[ 24, 53 ] = ascii (fp) : "./hrtfs/elev30/L30e095a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e095a.dat right
+[ 24, 54 ] = ascii (fp) : "./hrtfs/elev30/L30e090a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e090a.dat right
+[ 24, 55 ] = ascii (fp) : "./hrtfs/elev30/L30e085a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e085a.dat right
+[ 24, 56 ] = ascii (fp) : "./hrtfs/elev30/L30e080a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e080a.dat right
+[ 24, 57 ] = ascii (fp) : "./hrtfs/elev30/L30e075a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e075a.dat right
+[ 24, 58 ] = ascii (fp) : "./hrtfs/elev30/L30e070a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e070a.dat right
+[ 24, 59 ] = ascii (fp) : "./hrtfs/elev30/L30e065a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e065a.dat right
+[ 24, 60 ] = ascii (fp) : "./hrtfs/elev30/L30e060a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e060a.dat right
+[ 24, 61 ] = ascii (fp) : "./hrtfs/elev30/L30e055a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e055a.dat right
+[ 24, 62 ] = ascii (fp) : "./hrtfs/elev30/L30e050a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e050a.dat right
+[ 24, 63 ] = ascii (fp) : "./hrtfs/elev30/L30e045a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e045a.dat right
+[ 24, 64 ] = ascii (fp) : "./hrtfs/elev30/L30e040a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e040a.dat right
+[ 24, 65 ] = ascii (fp) : "./hrtfs/elev30/L30e035a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e035a.dat right
+[ 24, 66 ] = ascii (fp) : "./hrtfs/elev30/L30e030a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e030a.dat right
+[ 24, 67 ] = ascii (fp) : "./hrtfs/elev30/L30e025a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e025a.dat right
+[ 24, 68 ] = ascii (fp) : "./hrtfs/elev30/L30e020a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e020a.dat right
+[ 24, 69 ] = ascii (fp) : "./hrtfs/elev30/L30e015a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e015a.dat right
+[ 24, 70 ] = ascii (fp) : "./hrtfs/elev30/L30e010a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e010a.dat right
+[ 24, 71 ] = ascii (fp) : "./hrtfs/elev30/L30e005a.dat left
+           + ascii (fp) : "./hrtfs/elev30/R30e005a.dat right
+
+[ 25,  0 ] = ascii (fp) : "./hrtfs/elev35/L35e000a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e000a.dat right
+[ 25,  1 ] = ascii (fp) : "./hrtfs/elev35/L35e355a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e355a.dat right
+[ 25,  2 ] = ascii (fp) : "./hrtfs/elev35/L35e350a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e350a.dat right
+[ 25,  3 ] = ascii (fp) : "./hrtfs/elev35/L35e345a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e345a.dat right
+[ 25,  4 ] = ascii (fp) : "./hrtfs/elev35/L35e340a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e340a.dat right
+[ 25,  5 ] = ascii (fp) : "./hrtfs/elev35/L35e335a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e335a.dat right
+[ 25,  6 ] = ascii (fp) : "./hrtfs/elev35/L35e330a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e330a.dat right
+[ 25,  7 ] = ascii (fp) : "./hrtfs/elev35/L35e325a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e325a.dat right
+[ 25,  8 ] = ascii (fp) : "./hrtfs/elev35/L35e320a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e320a.dat right
+[ 25,  9 ] = ascii (fp) : "./hrtfs/elev35/L35e315a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e315a.dat right
+[ 25, 10 ] = ascii (fp) : "./hrtfs/elev35/L35e310a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e310a.dat right
+[ 25, 11 ] = ascii (fp) : "./hrtfs/elev35/L35e305a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e305a.dat right
+[ 25, 12 ] = ascii (fp) : "./hrtfs/elev35/L35e300a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e300a.dat right
+[ 25, 13 ] = ascii (fp) : "./hrtfs/elev35/L35e295a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e295a.dat right
+[ 25, 14 ] = ascii (fp) : "./hrtfs/elev35/L35e290a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e290a.dat right
+[ 25, 15 ] = ascii (fp) : "./hrtfs/elev35/L35e285a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e285a.dat right
+[ 25, 16 ] = ascii (fp) : "./hrtfs/elev35/L35e280a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e280a.dat right
+[ 25, 17 ] = ascii (fp) : "./hrtfs/elev35/L35e275a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e275a.dat right
+[ 25, 18 ] = ascii (fp) : "./hrtfs/elev35/L35e270a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e270a.dat right
+[ 25, 19 ] = ascii (fp) : "./hrtfs/elev35/L35e265a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e265a.dat right
+[ 25, 20 ] = ascii (fp) : "./hrtfs/elev35/L35e260a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e260a.dat right
+[ 25, 21 ] = ascii (fp) : "./hrtfs/elev35/L35e255a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e255a.dat right
+[ 25, 22 ] = ascii (fp) : "./hrtfs/elev35/L35e250a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e250a.dat right
+[ 25, 23 ] = ascii (fp) : "./hrtfs/elev35/L35e245a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e245a.dat right
+[ 25, 24 ] = ascii (fp) : "./hrtfs/elev35/L35e240a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e240a.dat right
+[ 25, 25 ] = ascii (fp) : "./hrtfs/elev35/L35e235a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e235a.dat right
+[ 25, 26 ] = ascii (fp) : "./hrtfs/elev35/L35e230a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e230a.dat right
+[ 25, 27 ] = ascii (fp) : "./hrtfs/elev35/L35e225a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e225a.dat right
+[ 25, 28 ] = ascii (fp) : "./hrtfs/elev35/L35e220a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e220a.dat right
+[ 25, 29 ] = ascii (fp) : "./hrtfs/elev35/L35e215a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e215a.dat right
+[ 25, 30 ] = ascii (fp) : "./hrtfs/elev35/L35e210a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e210a.dat right
+[ 25, 31 ] = ascii (fp) : "./hrtfs/elev35/L35e205a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e205a.dat right
+[ 25, 32 ] = ascii (fp) : "./hrtfs/elev35/L35e200a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e200a.dat right
+[ 25, 33 ] = ascii (fp) : "./hrtfs/elev35/L35e195a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e195a.dat right
+[ 25, 34 ] = ascii (fp) : "./hrtfs/elev35/L35e190a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e190a.dat right
+[ 25, 35 ] = ascii (fp) : "./hrtfs/elev35/L35e185a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e185a.dat right
+[ 25, 36 ] = ascii (fp) : "./hrtfs/elev35/L35e180a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e180a.dat right
+[ 25, 37 ] = ascii (fp) : "./hrtfs/elev35/L35e175a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e175a.dat right
+[ 25, 38 ] = ascii (fp) : "./hrtfs/elev35/L35e170a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e170a.dat right
+[ 25, 39 ] = ascii (fp) : "./hrtfs/elev35/L35e165a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e165a.dat right
+[ 25, 40 ] = ascii (fp) : "./hrtfs/elev35/L35e160a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e160a.dat right
+[ 25, 41 ] = ascii (fp) : "./hrtfs/elev35/L35e155a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e155a.dat right
+[ 25, 42 ] = ascii (fp) : "./hrtfs/elev35/L35e150a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e150a.dat right
+[ 25, 43 ] = ascii (fp) : "./hrtfs/elev35/L35e145a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e145a.dat right
+[ 25, 44 ] = ascii (fp) : "./hrtfs/elev35/L35e140a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e140a.dat right
+[ 25, 45 ] = ascii (fp) : "./hrtfs/elev35/L35e135a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e135a.dat right
+[ 25, 46 ] = ascii (fp) : "./hrtfs/elev35/L35e130a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e130a.dat right
+[ 25, 47 ] = ascii (fp) : "./hrtfs/elev35/L35e125a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e125a.dat right
+[ 25, 48 ] = ascii (fp) : "./hrtfs/elev35/L35e120a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e120a.dat right
+[ 25, 49 ] = ascii (fp) : "./hrtfs/elev35/L35e115a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e115a.dat right
+[ 25, 50 ] = ascii (fp) : "./hrtfs/elev35/L35e110a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e110a.dat right
+[ 25, 51 ] = ascii (fp) : "./hrtfs/elev35/L35e105a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e105a.dat right
+[ 25, 52 ] = ascii (fp) : "./hrtfs/elev35/L35e100a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e100a.dat right
+[ 25, 53 ] = ascii (fp) : "./hrtfs/elev35/L35e095a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e095a.dat right
+[ 25, 54 ] = ascii (fp) : "./hrtfs/elev35/L35e090a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e090a.dat right
+[ 25, 55 ] = ascii (fp) : "./hrtfs/elev35/L35e085a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e085a.dat right
+[ 25, 56 ] = ascii (fp) : "./hrtfs/elev35/L35e080a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e080a.dat right
+[ 25, 57 ] = ascii (fp) : "./hrtfs/elev35/L35e075a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e075a.dat right
+[ 25, 58 ] = ascii (fp) : "./hrtfs/elev35/L35e070a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e070a.dat right
+[ 25, 59 ] = ascii (fp) : "./hrtfs/elev35/L35e065a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e065a.dat right
+[ 25, 60 ] = ascii (fp) : "./hrtfs/elev35/L35e060a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e060a.dat right
+[ 25, 61 ] = ascii (fp) : "./hrtfs/elev35/L35e055a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e055a.dat right
+[ 25, 62 ] = ascii (fp) : "./hrtfs/elev35/L35e050a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e050a.dat right
+[ 25, 63 ] = ascii (fp) : "./hrtfs/elev35/L35e045a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e045a.dat right
+[ 25, 64 ] = ascii (fp) : "./hrtfs/elev35/L35e040a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e040a.dat right
+[ 25, 65 ] = ascii (fp) : "./hrtfs/elev35/L35e035a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e035a.dat right
+[ 25, 66 ] = ascii (fp) : "./hrtfs/elev35/L35e030a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e030a.dat right
+[ 25, 67 ] = ascii (fp) : "./hrtfs/elev35/L35e025a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e025a.dat right
+[ 25, 68 ] = ascii (fp) : "./hrtfs/elev35/L35e020a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e020a.dat right
+[ 25, 69 ] = ascii (fp) : "./hrtfs/elev35/L35e015a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e015a.dat right
+[ 25, 70 ] = ascii (fp) : "./hrtfs/elev35/L35e010a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e010a.dat right
+[ 25, 71 ] = ascii (fp) : "./hrtfs/elev35/L35e005a.dat left
+           + ascii (fp) : "./hrtfs/elev35/R35e005a.dat right
+
+[ 26,  0 ] = ascii (fp) : "./hrtfs/elev40/L40e000a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e000a.dat right
+[ 26,  1 ] = ascii (fp) : "./hrtfs/elev40/L40e355a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e355a.dat right
+[ 26,  2 ] = ascii (fp) : "./hrtfs/elev40/L40e350a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e350a.dat right
+[ 26,  3 ] = ascii (fp) : "./hrtfs/elev40/L40e345a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e345a.dat right
+[ 26,  4 ] = ascii (fp) : "./hrtfs/elev40/L40e340a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e340a.dat right
+[ 26,  5 ] = ascii (fp) : "./hrtfs/elev40/L40e335a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e335a.dat right
+[ 26,  6 ] = ascii (fp) : "./hrtfs/elev40/L40e330a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e330a.dat right
+[ 26,  7 ] = ascii (fp) : "./hrtfs/elev40/L40e325a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e325a.dat right
+[ 26,  8 ] = ascii (fp) : "./hrtfs/elev40/L40e320a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e320a.dat right
+[ 26,  9 ] = ascii (fp) : "./hrtfs/elev40/L40e315a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e315a.dat right
+[ 26, 10 ] = ascii (fp) : "./hrtfs/elev40/L40e310a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e310a.dat right
+[ 26, 11 ] = ascii (fp) : "./hrtfs/elev40/L40e305a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e305a.dat right
+[ 26, 12 ] = ascii (fp) : "./hrtfs/elev40/L40e300a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e300a.dat right
+[ 26, 13 ] = ascii (fp) : "./hrtfs/elev40/L40e295a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e295a.dat right
+[ 26, 14 ] = ascii (fp) : "./hrtfs/elev40/L40e290a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e290a.dat right
+[ 26, 15 ] = ascii (fp) : "./hrtfs/elev40/L40e285a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e285a.dat right
+[ 26, 16 ] = ascii (fp) : "./hrtfs/elev40/L40e280a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e280a.dat right
+[ 26, 17 ] = ascii (fp) : "./hrtfs/elev40/L40e275a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e275a.dat right
+[ 26, 18 ] = ascii (fp) : "./hrtfs/elev40/L40e270a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e270a.dat right
+[ 26, 19 ] = ascii (fp) : "./hrtfs/elev40/L40e265a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e265a.dat right
+[ 26, 20 ] = ascii (fp) : "./hrtfs/elev40/L40e260a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e260a.dat right
+[ 26, 21 ] = ascii (fp) : "./hrtfs/elev40/L40e255a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e255a.dat right
+[ 26, 22 ] = ascii (fp) : "./hrtfs/elev40/L40e250a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e250a.dat right
+[ 26, 23 ] = ascii (fp) : "./hrtfs/elev40/L40e245a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e245a.dat right
+[ 26, 24 ] = ascii (fp) : "./hrtfs/elev40/L40e240a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e240a.dat right
+[ 26, 25 ] = ascii (fp) : "./hrtfs/elev40/L40e235a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e235a.dat right
+[ 26, 26 ] = ascii (fp) : "./hrtfs/elev40/L40e230a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e230a.dat right
+[ 26, 27 ] = ascii (fp) : "./hrtfs/elev40/L40e225a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e225a.dat right
+[ 26, 28 ] = ascii (fp) : "./hrtfs/elev40/L40e220a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e220a.dat right
+[ 26, 29 ] = ascii (fp) : "./hrtfs/elev40/L40e215a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e215a.dat right
+[ 26, 30 ] = ascii (fp) : "./hrtfs/elev40/L40e210a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e210a.dat right
+[ 26, 31 ] = ascii (fp) : "./hrtfs/elev40/L40e205a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e205a.dat right
+[ 26, 32 ] = ascii (fp) : "./hrtfs/elev40/L40e200a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e200a.dat right
+[ 26, 33 ] = ascii (fp) : "./hrtfs/elev40/L40e195a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e195a.dat right
+[ 26, 34 ] = ascii (fp) : "./hrtfs/elev40/L40e190a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e190a.dat right
+[ 26, 35 ] = ascii (fp) : "./hrtfs/elev40/L40e185a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e185a.dat right
+[ 26, 36 ] = ascii (fp) : "./hrtfs/elev40/L40e180a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e180a.dat right
+[ 26, 37 ] = ascii (fp) : "./hrtfs/elev40/L40e175a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e175a.dat right
+[ 26, 38 ] = ascii (fp) : "./hrtfs/elev40/L40e170a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e170a.dat right
+[ 26, 39 ] = ascii (fp) : "./hrtfs/elev40/L40e165a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e165a.dat right
+[ 26, 40 ] = ascii (fp) : "./hrtfs/elev40/L40e160a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e160a.dat right
+[ 26, 41 ] = ascii (fp) : "./hrtfs/elev40/L40e155a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e155a.dat right
+[ 26, 42 ] = ascii (fp) : "./hrtfs/elev40/L40e150a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e150a.dat right
+[ 26, 43 ] = ascii (fp) : "./hrtfs/elev40/L40e145a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e145a.dat right
+[ 26, 44 ] = ascii (fp) : "./hrtfs/elev40/L40e140a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e140a.dat right
+[ 26, 45 ] = ascii (fp) : "./hrtfs/elev40/L40e135a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e135a.dat right
+[ 26, 46 ] = ascii (fp) : "./hrtfs/elev40/L40e130a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e130a.dat right
+[ 26, 47 ] = ascii (fp) : "./hrtfs/elev40/L40e125a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e125a.dat right
+[ 26, 48 ] = ascii (fp) : "./hrtfs/elev40/L40e120a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e120a.dat right
+[ 26, 49 ] = ascii (fp) : "./hrtfs/elev40/L40e115a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e115a.dat right
+[ 26, 50 ] = ascii (fp) : "./hrtfs/elev40/L40e110a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e110a.dat right
+[ 26, 51 ] = ascii (fp) : "./hrtfs/elev40/L40e105a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e105a.dat right
+[ 26, 52 ] = ascii (fp) : "./hrtfs/elev40/L40e100a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e100a.dat right
+[ 26, 53 ] = ascii (fp) : "./hrtfs/elev40/L40e095a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e095a.dat right
+[ 26, 54 ] = ascii (fp) : "./hrtfs/elev40/L40e090a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e090a.dat right
+[ 26, 55 ] = ascii (fp) : "./hrtfs/elev40/L40e085a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e085a.dat right
+[ 26, 56 ] = ascii (fp) : "./hrtfs/elev40/L40e080a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e080a.dat right
+[ 26, 57 ] = ascii (fp) : "./hrtfs/elev40/L40e075a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e075a.dat right
+[ 26, 58 ] = ascii (fp) : "./hrtfs/elev40/L40e070a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e070a.dat right
+[ 26, 59 ] = ascii (fp) : "./hrtfs/elev40/L40e065a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e065a.dat right
+[ 26, 60 ] = ascii (fp) : "./hrtfs/elev40/L40e060a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e060a.dat right
+[ 26, 61 ] = ascii (fp) : "./hrtfs/elev40/L40e055a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e055a.dat right
+[ 26, 62 ] = ascii (fp) : "./hrtfs/elev40/L40e050a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e050a.dat right
+[ 26, 63 ] = ascii (fp) : "./hrtfs/elev40/L40e045a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e045a.dat right
+[ 26, 64 ] = ascii (fp) : "./hrtfs/elev40/L40e040a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e040a.dat right
+[ 26, 65 ] = ascii (fp) : "./hrtfs/elev40/L40e035a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e035a.dat right
+[ 26, 66 ] = ascii (fp) : "./hrtfs/elev40/L40e030a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e030a.dat right
+[ 26, 67 ] = ascii (fp) : "./hrtfs/elev40/L40e025a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e025a.dat right
+[ 26, 68 ] = ascii (fp) : "./hrtfs/elev40/L40e020a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e020a.dat right
+[ 26, 69 ] = ascii (fp) : "./hrtfs/elev40/L40e015a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e015a.dat right
+[ 26, 70 ] = ascii (fp) : "./hrtfs/elev40/L40e010a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e010a.dat right
+[ 26, 71 ] = ascii (fp) : "./hrtfs/elev40/L40e005a.dat left
+           + ascii (fp) : "./hrtfs/elev40/R40e005a.dat right
+
+[ 27,  0 ] = ascii (fp) : "./hrtfs/elev45/L45e000a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e000a.dat right
+[ 27,  1 ] = ascii (fp) : "./hrtfs/elev45/L45e355a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e355a.dat right
+[ 27,  2 ] = ascii (fp) : "./hrtfs/elev45/L45e350a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e350a.dat right
+[ 27,  3 ] = ascii (fp) : "./hrtfs/elev45/L45e345a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e345a.dat right
+[ 27,  4 ] = ascii (fp) : "./hrtfs/elev45/L45e340a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e340a.dat right
+[ 27,  5 ] = ascii (fp) : "./hrtfs/elev45/L45e335a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e335a.dat right
+[ 27,  6 ] = ascii (fp) : "./hrtfs/elev45/L45e330a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e330a.dat right
+[ 27,  7 ] = ascii (fp) : "./hrtfs/elev45/L45e325a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e325a.dat right
+[ 27,  8 ] = ascii (fp) : "./hrtfs/elev45/L45e320a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e320a.dat right
+[ 27,  9 ] = ascii (fp) : "./hrtfs/elev45/L45e315a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e315a.dat right
+[ 27, 10 ] = ascii (fp) : "./hrtfs/elev45/L45e310a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e310a.dat right
+[ 27, 11 ] = ascii (fp) : "./hrtfs/elev45/L45e305a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e305a.dat right
+[ 27, 12 ] = ascii (fp) : "./hrtfs/elev45/L45e300a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e300a.dat right
+[ 27, 13 ] = ascii (fp) : "./hrtfs/elev45/L45e295a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e295a.dat right
+[ 27, 14 ] = ascii (fp) : "./hrtfs/elev45/L45e290a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e290a.dat right
+[ 27, 15 ] = ascii (fp) : "./hrtfs/elev45/L45e285a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e285a.dat right
+[ 27, 16 ] = ascii (fp) : "./hrtfs/elev45/L45e280a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e280a.dat right
+[ 27, 17 ] = ascii (fp) : "./hrtfs/elev45/L45e275a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e275a.dat right
+[ 27, 18 ] = ascii (fp) : "./hrtfs/elev45/L45e270a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e270a.dat right
+[ 27, 19 ] = ascii (fp) : "./hrtfs/elev45/L45e265a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e265a.dat right
+[ 27, 20 ] = ascii (fp) : "./hrtfs/elev45/L45e260a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e260a.dat right
+[ 27, 21 ] = ascii (fp) : "./hrtfs/elev45/L45e255a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e255a.dat right
+[ 27, 22 ] = ascii (fp) : "./hrtfs/elev45/L45e250a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e250a.dat right
+[ 27, 23 ] = ascii (fp) : "./hrtfs/elev45/L45e245a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e245a.dat right
+[ 27, 24 ] = ascii (fp) : "./hrtfs/elev45/L45e240a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e240a.dat right
+[ 27, 25 ] = ascii (fp) : "./hrtfs/elev45/L45e235a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e235a.dat right
+[ 27, 26 ] = ascii (fp) : "./hrtfs/elev45/L45e230a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e230a.dat right
+[ 27, 27 ] = ascii (fp) : "./hrtfs/elev45/L45e225a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e225a.dat right
+[ 27, 28 ] = ascii (fp) : "./hrtfs/elev45/L45e220a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e220a.dat right
+[ 27, 29 ] = ascii (fp) : "./hrtfs/elev45/L45e215a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e215a.dat right
+[ 27, 30 ] = ascii (fp) : "./hrtfs/elev45/L45e210a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e210a.dat right
+[ 27, 31 ] = ascii (fp) : "./hrtfs/elev45/L45e205a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e205a.dat right
+[ 27, 32 ] = ascii (fp) : "./hrtfs/elev45/L45e200a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e200a.dat right
+[ 27, 33 ] = ascii (fp) : "./hrtfs/elev45/L45e195a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e195a.dat right
+[ 27, 34 ] = ascii (fp) : "./hrtfs/elev45/L45e190a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e190a.dat right
+[ 27, 35 ] = ascii (fp) : "./hrtfs/elev45/L45e185a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e185a.dat right
+[ 27, 36 ] = ascii (fp) : "./hrtfs/elev45/L45e180a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e180a.dat right
+[ 27, 37 ] = ascii (fp) : "./hrtfs/elev45/L45e175a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e175a.dat right
+[ 27, 38 ] = ascii (fp) : "./hrtfs/elev45/L45e170a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e170a.dat right
+[ 27, 39 ] = ascii (fp) : "./hrtfs/elev45/L45e165a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e165a.dat right
+[ 27, 40 ] = ascii (fp) : "./hrtfs/elev45/L45e160a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e160a.dat right
+[ 27, 41 ] = ascii (fp) : "./hrtfs/elev45/L45e155a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e155a.dat right
+[ 27, 42 ] = ascii (fp) : "./hrtfs/elev45/L45e150a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e150a.dat right
+[ 27, 43 ] = ascii (fp) : "./hrtfs/elev45/L45e145a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e145a.dat right
+[ 27, 44 ] = ascii (fp) : "./hrtfs/elev45/L45e140a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e140a.dat right
+[ 27, 45 ] = ascii (fp) : "./hrtfs/elev45/L45e135a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e135a.dat right
+[ 27, 46 ] = ascii (fp) : "./hrtfs/elev45/L45e130a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e130a.dat right
+[ 27, 47 ] = ascii (fp) : "./hrtfs/elev45/L45e125a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e125a.dat right
+[ 27, 48 ] = ascii (fp) : "./hrtfs/elev45/L45e120a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e120a.dat right
+[ 27, 49 ] = ascii (fp) : "./hrtfs/elev45/L45e115a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e115a.dat right
+[ 27, 50 ] = ascii (fp) : "./hrtfs/elev45/L45e110a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e110a.dat right
+[ 27, 51 ] = ascii (fp) : "./hrtfs/elev45/L45e105a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e105a.dat right
+[ 27, 52 ] = ascii (fp) : "./hrtfs/elev45/L45e100a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e100a.dat right
+[ 27, 53 ] = ascii (fp) : "./hrtfs/elev45/L45e095a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e095a.dat right
+[ 27, 54 ] = ascii (fp) : "./hrtfs/elev45/L45e090a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e090a.dat right
+[ 27, 55 ] = ascii (fp) : "./hrtfs/elev45/L45e085a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e085a.dat right
+[ 27, 56 ] = ascii (fp) : "./hrtfs/elev45/L45e080a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e080a.dat right
+[ 27, 57 ] = ascii (fp) : "./hrtfs/elev45/L45e075a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e075a.dat right
+[ 27, 58 ] = ascii (fp) : "./hrtfs/elev45/L45e070a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e070a.dat right
+[ 27, 59 ] = ascii (fp) : "./hrtfs/elev45/L45e065a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e065a.dat right
+[ 27, 60 ] = ascii (fp) : "./hrtfs/elev45/L45e060a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e060a.dat right
+[ 27, 61 ] = ascii (fp) : "./hrtfs/elev45/L45e055a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e055a.dat right
+[ 27, 62 ] = ascii (fp) : "./hrtfs/elev45/L45e050a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e050a.dat right
+[ 27, 63 ] = ascii (fp) : "./hrtfs/elev45/L45e045a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e045a.dat right
+[ 27, 64 ] = ascii (fp) : "./hrtfs/elev45/L45e040a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e040a.dat right
+[ 27, 65 ] = ascii (fp) : "./hrtfs/elev45/L45e035a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e035a.dat right
+[ 27, 66 ] = ascii (fp) : "./hrtfs/elev45/L45e030a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e030a.dat right
+[ 27, 67 ] = ascii (fp) : "./hrtfs/elev45/L45e025a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e025a.dat right
+[ 27, 68 ] = ascii (fp) : "./hrtfs/elev45/L45e020a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e020a.dat right
+[ 27, 69 ] = ascii (fp) : "./hrtfs/elev45/L45e015a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e015a.dat right
+[ 27, 70 ] = ascii (fp) : "./hrtfs/elev45/L45e010a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e010a.dat right
+[ 27, 71 ] = ascii (fp) : "./hrtfs/elev45/L45e005a.dat left
+           + ascii (fp) : "./hrtfs/elev45/R45e005a.dat right
+
+[ 28,  0 ] = ascii (fp) : "./hrtfs/elev50/L50e000a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e000a.dat right
+[ 28,  1 ] = ascii (fp) : "./hrtfs/elev50/L50e355a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e355a.dat right
+[ 28,  2 ] = ascii (fp) : "./hrtfs/elev50/L50e350a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e350a.dat right
+[ 28,  3 ] = ascii (fp) : "./hrtfs/elev50/L50e345a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e345a.dat right
+[ 28,  4 ] = ascii (fp) : "./hrtfs/elev50/L50e340a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e340a.dat right
+[ 28,  5 ] = ascii (fp) : "./hrtfs/elev50/L50e335a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e335a.dat right
+[ 28,  6 ] = ascii (fp) : "./hrtfs/elev50/L50e330a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e330a.dat right
+[ 28,  7 ] = ascii (fp) : "./hrtfs/elev50/L50e325a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e325a.dat right
+[ 28,  8 ] = ascii (fp) : "./hrtfs/elev50/L50e320a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e320a.dat right
+[ 28,  9 ] = ascii (fp) : "./hrtfs/elev50/L50e315a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e315a.dat right
+[ 28, 10 ] = ascii (fp) : "./hrtfs/elev50/L50e310a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e310a.dat right
+[ 28, 11 ] = ascii (fp) : "./hrtfs/elev50/L50e305a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e305a.dat right
+[ 28, 12 ] = ascii (fp) : "./hrtfs/elev50/L50e300a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e300a.dat right
+[ 28, 13 ] = ascii (fp) : "./hrtfs/elev50/L50e295a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e295a.dat right
+[ 28, 14 ] = ascii (fp) : "./hrtfs/elev50/L50e290a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e290a.dat right
+[ 28, 15 ] = ascii (fp) : "./hrtfs/elev50/L50e285a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e285a.dat right
+[ 28, 16 ] = ascii (fp) : "./hrtfs/elev50/L50e280a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e280a.dat right
+[ 28, 17 ] = ascii (fp) : "./hrtfs/elev50/L50e275a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e275a.dat right
+[ 28, 18 ] = ascii (fp) : "./hrtfs/elev50/L50e270a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e270a.dat right
+[ 28, 19 ] = ascii (fp) : "./hrtfs/elev50/L50e265a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e265a.dat right
+[ 28, 20 ] = ascii (fp) : "./hrtfs/elev50/L50e260a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e260a.dat right
+[ 28, 21 ] = ascii (fp) : "./hrtfs/elev50/L50e255a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e255a.dat right
+[ 28, 22 ] = ascii (fp) : "./hrtfs/elev50/L50e250a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e250a.dat right
+[ 28, 23 ] = ascii (fp) : "./hrtfs/elev50/L50e245a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e245a.dat right
+[ 28, 24 ] = ascii (fp) : "./hrtfs/elev50/L50e240a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e240a.dat right
+[ 28, 25 ] = ascii (fp) : "./hrtfs/elev50/L50e235a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e235a.dat right
+[ 28, 26 ] = ascii (fp) : "./hrtfs/elev50/L50e230a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e230a.dat right
+[ 28, 27 ] = ascii (fp) : "./hrtfs/elev50/L50e225a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e225a.dat right
+[ 28, 28 ] = ascii (fp) : "./hrtfs/elev50/L50e220a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e220a.dat right
+[ 28, 29 ] = ascii (fp) : "./hrtfs/elev50/L50e215a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e215a.dat right
+[ 28, 30 ] = ascii (fp) : "./hrtfs/elev50/L50e210a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e210a.dat right
+[ 28, 31 ] = ascii (fp) : "./hrtfs/elev50/L50e205a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e205a.dat right
+[ 28, 32 ] = ascii (fp) : "./hrtfs/elev50/L50e200a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e200a.dat right
+[ 28, 33 ] = ascii (fp) : "./hrtfs/elev50/L50e195a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e195a.dat right
+[ 28, 34 ] = ascii (fp) : "./hrtfs/elev50/L50e190a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e190a.dat right
+[ 28, 35 ] = ascii (fp) : "./hrtfs/elev50/L50e185a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e185a.dat right
+[ 28, 36 ] = ascii (fp) : "./hrtfs/elev50/L50e180a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e180a.dat right
+[ 28, 37 ] = ascii (fp) : "./hrtfs/elev50/L50e175a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e175a.dat right
+[ 28, 38 ] = ascii (fp) : "./hrtfs/elev50/L50e170a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e170a.dat right
+[ 28, 39 ] = ascii (fp) : "./hrtfs/elev50/L50e165a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e165a.dat right
+[ 28, 40 ] = ascii (fp) : "./hrtfs/elev50/L50e160a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e160a.dat right
+[ 28, 41 ] = ascii (fp) : "./hrtfs/elev50/L50e155a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e155a.dat right
+[ 28, 42 ] = ascii (fp) : "./hrtfs/elev50/L50e150a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e150a.dat right
+[ 28, 43 ] = ascii (fp) : "./hrtfs/elev50/L50e145a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e145a.dat right
+[ 28, 44 ] = ascii (fp) : "./hrtfs/elev50/L50e140a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e140a.dat right
+[ 28, 45 ] = ascii (fp) : "./hrtfs/elev50/L50e135a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e135a.dat right
+[ 28, 46 ] = ascii (fp) : "./hrtfs/elev50/L50e130a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e130a.dat right
+[ 28, 47 ] = ascii (fp) : "./hrtfs/elev50/L50e125a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e125a.dat right
+[ 28, 48 ] = ascii (fp) : "./hrtfs/elev50/L50e120a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e120a.dat right
+[ 28, 49 ] = ascii (fp) : "./hrtfs/elev50/L50e115a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e115a.dat right
+[ 28, 50 ] = ascii (fp) : "./hrtfs/elev50/L50e110a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e110a.dat right
+[ 28, 51 ] = ascii (fp) : "./hrtfs/elev50/L50e105a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e105a.dat right
+[ 28, 52 ] = ascii (fp) : "./hrtfs/elev50/L50e100a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e100a.dat right
+[ 28, 53 ] = ascii (fp) : "./hrtfs/elev50/L50e095a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e095a.dat right
+[ 28, 54 ] = ascii (fp) : "./hrtfs/elev50/L50e090a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e090a.dat right
+[ 28, 55 ] = ascii (fp) : "./hrtfs/elev50/L50e085a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e085a.dat right
+[ 28, 56 ] = ascii (fp) : "./hrtfs/elev50/L50e080a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e080a.dat right
+[ 28, 57 ] = ascii (fp) : "./hrtfs/elev50/L50e075a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e075a.dat right
+[ 28, 58 ] = ascii (fp) : "./hrtfs/elev50/L50e070a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e070a.dat right
+[ 28, 59 ] = ascii (fp) : "./hrtfs/elev50/L50e065a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e065a.dat right
+[ 28, 60 ] = ascii (fp) : "./hrtfs/elev50/L50e060a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e060a.dat right
+[ 28, 61 ] = ascii (fp) : "./hrtfs/elev50/L50e055a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e055a.dat right
+[ 28, 62 ] = ascii (fp) : "./hrtfs/elev50/L50e050a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e050a.dat right
+[ 28, 63 ] = ascii (fp) : "./hrtfs/elev50/L50e045a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e045a.dat right
+[ 28, 64 ] = ascii (fp) : "./hrtfs/elev50/L50e040a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e040a.dat right
+[ 28, 65 ] = ascii (fp) : "./hrtfs/elev50/L50e035a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e035a.dat right
+[ 28, 66 ] = ascii (fp) : "./hrtfs/elev50/L50e030a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e030a.dat right
+[ 28, 67 ] = ascii (fp) : "./hrtfs/elev50/L50e025a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e025a.dat right
+[ 28, 68 ] = ascii (fp) : "./hrtfs/elev50/L50e020a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e020a.dat right
+[ 28, 69 ] = ascii (fp) : "./hrtfs/elev50/L50e015a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e015a.dat right
+[ 28, 70 ] = ascii (fp) : "./hrtfs/elev50/L50e010a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e010a.dat right
+[ 28, 71 ] = ascii (fp) : "./hrtfs/elev50/L50e005a.dat left
+           + ascii (fp) : "./hrtfs/elev50/R50e005a.dat right
+
+[ 29,  0 ] = ascii (fp) : "./hrtfs/elev55/L55e000a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e000a.dat right
+[ 29,  1 ] = ascii (fp) : "./hrtfs/elev55/L55e355a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e355a.dat right
+[ 29,  2 ] = ascii (fp) : "./hrtfs/elev55/L55e350a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e350a.dat right
+[ 29,  3 ] = ascii (fp) : "./hrtfs/elev55/L55e345a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e345a.dat right
+[ 29,  4 ] = ascii (fp) : "./hrtfs/elev55/L55e340a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e340a.dat right
+[ 29,  5 ] = ascii (fp) : "./hrtfs/elev55/L55e335a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e335a.dat right
+[ 29,  6 ] = ascii (fp) : "./hrtfs/elev55/L55e330a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e330a.dat right
+[ 29,  7 ] = ascii (fp) : "./hrtfs/elev55/L55e325a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e325a.dat right
+[ 29,  8 ] = ascii (fp) : "./hrtfs/elev55/L55e320a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e320a.dat right
+[ 29,  9 ] = ascii (fp) : "./hrtfs/elev55/L55e315a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e315a.dat right
+[ 29, 10 ] = ascii (fp) : "./hrtfs/elev55/L55e310a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e310a.dat right
+[ 29, 11 ] = ascii (fp) : "./hrtfs/elev55/L55e305a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e305a.dat right
+[ 29, 12 ] = ascii (fp) : "./hrtfs/elev55/L55e300a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e300a.dat right
+[ 29, 13 ] = ascii (fp) : "./hrtfs/elev55/L55e295a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e295a.dat right
+[ 29, 14 ] = ascii (fp) : "./hrtfs/elev55/L55e290a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e290a.dat right
+[ 29, 15 ] = ascii (fp) : "./hrtfs/elev55/L55e285a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e285a.dat right
+[ 29, 16 ] = ascii (fp) : "./hrtfs/elev55/L55e280a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e280a.dat right
+[ 29, 17 ] = ascii (fp) : "./hrtfs/elev55/L55e275a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e275a.dat right
+[ 29, 18 ] = ascii (fp) : "./hrtfs/elev55/L55e270a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e270a.dat right
+[ 29, 19 ] = ascii (fp) : "./hrtfs/elev55/L55e265a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e265a.dat right
+[ 29, 20 ] = ascii (fp) : "./hrtfs/elev55/L55e260a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e260a.dat right
+[ 29, 21 ] = ascii (fp) : "./hrtfs/elev55/L55e255a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e255a.dat right
+[ 29, 22 ] = ascii (fp) : "./hrtfs/elev55/L55e250a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e250a.dat right
+[ 29, 23 ] = ascii (fp) : "./hrtfs/elev55/L55e245a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e245a.dat right
+[ 29, 24 ] = ascii (fp) : "./hrtfs/elev55/L55e240a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e240a.dat right
+[ 29, 25 ] = ascii (fp) : "./hrtfs/elev55/L55e235a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e235a.dat right
+[ 29, 26 ] = ascii (fp) : "./hrtfs/elev55/L55e230a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e230a.dat right
+[ 29, 27 ] = ascii (fp) : "./hrtfs/elev55/L55e225a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e225a.dat right
+[ 29, 28 ] = ascii (fp) : "./hrtfs/elev55/L55e220a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e220a.dat right
+[ 29, 29 ] = ascii (fp) : "./hrtfs/elev55/L55e215a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e215a.dat right
+[ 29, 30 ] = ascii (fp) : "./hrtfs/elev55/L55e210a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e210a.dat right
+[ 29, 31 ] = ascii (fp) : "./hrtfs/elev55/L55e205a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e205a.dat right
+[ 29, 32 ] = ascii (fp) : "./hrtfs/elev55/L55e200a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e200a.dat right
+[ 29, 33 ] = ascii (fp) : "./hrtfs/elev55/L55e195a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e195a.dat right
+[ 29, 34 ] = ascii (fp) : "./hrtfs/elev55/L55e190a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e190a.dat right
+[ 29, 35 ] = ascii (fp) : "./hrtfs/elev55/L55e185a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e185a.dat right
+[ 29, 36 ] = ascii (fp) : "./hrtfs/elev55/L55e180a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e180a.dat right
+[ 29, 37 ] = ascii (fp) : "./hrtfs/elev55/L55e175a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e175a.dat right
+[ 29, 38 ] = ascii (fp) : "./hrtfs/elev55/L55e170a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e170a.dat right
+[ 29, 39 ] = ascii (fp) : "./hrtfs/elev55/L55e165a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e165a.dat right
+[ 29, 40 ] = ascii (fp) : "./hrtfs/elev55/L55e160a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e160a.dat right
+[ 29, 41 ] = ascii (fp) : "./hrtfs/elev55/L55e155a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e155a.dat right
+[ 29, 42 ] = ascii (fp) : "./hrtfs/elev55/L55e150a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e150a.dat right
+[ 29, 43 ] = ascii (fp) : "./hrtfs/elev55/L55e145a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e145a.dat right
+[ 29, 44 ] = ascii (fp) : "./hrtfs/elev55/L55e140a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e140a.dat right
+[ 29, 45 ] = ascii (fp) : "./hrtfs/elev55/L55e135a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e135a.dat right
+[ 29, 46 ] = ascii (fp) : "./hrtfs/elev55/L55e130a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e130a.dat right
+[ 29, 47 ] = ascii (fp) : "./hrtfs/elev55/L55e125a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e125a.dat right
+[ 29, 48 ] = ascii (fp) : "./hrtfs/elev55/L55e120a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e120a.dat right
+[ 29, 49 ] = ascii (fp) : "./hrtfs/elev55/L55e115a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e115a.dat right
+[ 29, 50 ] = ascii (fp) : "./hrtfs/elev55/L55e110a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e110a.dat right
+[ 29, 51 ] = ascii (fp) : "./hrtfs/elev55/L55e105a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e105a.dat right
+[ 29, 52 ] = ascii (fp) : "./hrtfs/elev55/L55e100a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e100a.dat right
+[ 29, 53 ] = ascii (fp) : "./hrtfs/elev55/L55e095a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e095a.dat right
+[ 29, 54 ] = ascii (fp) : "./hrtfs/elev55/L55e090a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e090a.dat right
+[ 29, 55 ] = ascii (fp) : "./hrtfs/elev55/L55e085a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e085a.dat right
+[ 29, 56 ] = ascii (fp) : "./hrtfs/elev55/L55e080a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e080a.dat right
+[ 29, 57 ] = ascii (fp) : "./hrtfs/elev55/L55e075a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e075a.dat right
+[ 29, 58 ] = ascii (fp) : "./hrtfs/elev55/L55e070a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e070a.dat right
+[ 29, 59 ] = ascii (fp) : "./hrtfs/elev55/L55e065a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e065a.dat right
+[ 29, 60 ] = ascii (fp) : "./hrtfs/elev55/L55e060a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e060a.dat right
+[ 29, 61 ] = ascii (fp) : "./hrtfs/elev55/L55e055a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e055a.dat right
+[ 29, 62 ] = ascii (fp) : "./hrtfs/elev55/L55e050a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e050a.dat right
+[ 29, 63 ] = ascii (fp) : "./hrtfs/elev55/L55e045a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e045a.dat right
+[ 29, 64 ] = ascii (fp) : "./hrtfs/elev55/L55e040a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e040a.dat right
+[ 29, 65 ] = ascii (fp) : "./hrtfs/elev55/L55e035a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e035a.dat right
+[ 29, 66 ] = ascii (fp) : "./hrtfs/elev55/L55e030a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e030a.dat right
+[ 29, 67 ] = ascii (fp) : "./hrtfs/elev55/L55e025a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e025a.dat right
+[ 29, 68 ] = ascii (fp) : "./hrtfs/elev55/L55e020a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e020a.dat right
+[ 29, 69 ] = ascii (fp) : "./hrtfs/elev55/L55e015a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e015a.dat right
+[ 29, 70 ] = ascii (fp) : "./hrtfs/elev55/L55e010a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e010a.dat right
+[ 29, 71 ] = ascii (fp) : "./hrtfs/elev55/L55e005a.dat left
+           + ascii (fp) : "./hrtfs/elev55/R55e005a.dat right
+
+[ 30,  0 ] = ascii (fp) : "./hrtfs/elev60/L60e000a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e000a.dat right
+[ 30,  1 ] = ascii (fp) : "./hrtfs/elev60/L60e355a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e355a.dat right
+[ 30,  2 ] = ascii (fp) : "./hrtfs/elev60/L60e350a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e350a.dat right
+[ 30,  3 ] = ascii (fp) : "./hrtfs/elev60/L60e345a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e345a.dat right
+[ 30,  4 ] = ascii (fp) : "./hrtfs/elev60/L60e340a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e340a.dat right
+[ 30,  5 ] = ascii (fp) : "./hrtfs/elev60/L60e335a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e335a.dat right
+[ 30,  6 ] = ascii (fp) : "./hrtfs/elev60/L60e330a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e330a.dat right
+[ 30,  7 ] = ascii (fp) : "./hrtfs/elev60/L60e325a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e325a.dat right
+[ 30,  8 ] = ascii (fp) : "./hrtfs/elev60/L60e320a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e320a.dat right
+[ 30,  9 ] = ascii (fp) : "./hrtfs/elev60/L60e315a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e315a.dat right
+[ 30, 10 ] = ascii (fp) : "./hrtfs/elev60/L60e310a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e310a.dat right
+[ 30, 11 ] = ascii (fp) : "./hrtfs/elev60/L60e305a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e305a.dat right
+[ 30, 12 ] = ascii (fp) : "./hrtfs/elev60/L60e300a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e300a.dat right
+[ 30, 13 ] = ascii (fp) : "./hrtfs/elev60/L60e295a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e295a.dat right
+[ 30, 14 ] = ascii (fp) : "./hrtfs/elev60/L60e290a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e290a.dat right
+[ 30, 15 ] = ascii (fp) : "./hrtfs/elev60/L60e285a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e285a.dat right
+[ 30, 16 ] = ascii (fp) : "./hrtfs/elev60/L60e280a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e280a.dat right
+[ 30, 17 ] = ascii (fp) : "./hrtfs/elev60/L60e275a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e275a.dat right
+[ 30, 18 ] = ascii (fp) : "./hrtfs/elev60/L60e270a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e270a.dat right
+[ 30, 19 ] = ascii (fp) : "./hrtfs/elev60/L60e265a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e265a.dat right
+[ 30, 20 ] = ascii (fp) : "./hrtfs/elev60/L60e260a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e260a.dat right
+[ 30, 21 ] = ascii (fp) : "./hrtfs/elev60/L60e255a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e255a.dat right
+[ 30, 22 ] = ascii (fp) : "./hrtfs/elev60/L60e250a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e250a.dat right
+[ 30, 23 ] = ascii (fp) : "./hrtfs/elev60/L60e245a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e245a.dat right
+[ 30, 24 ] = ascii (fp) : "./hrtfs/elev60/L60e240a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e240a.dat right
+[ 30, 25 ] = ascii (fp) : "./hrtfs/elev60/L60e235a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e235a.dat right
+[ 30, 26 ] = ascii (fp) : "./hrtfs/elev60/L60e230a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e230a.dat right
+[ 30, 27 ] = ascii (fp) : "./hrtfs/elev60/L60e225a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e225a.dat right
+[ 30, 28 ] = ascii (fp) : "./hrtfs/elev60/L60e220a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e220a.dat right
+[ 30, 29 ] = ascii (fp) : "./hrtfs/elev60/L60e215a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e215a.dat right
+[ 30, 30 ] = ascii (fp) : "./hrtfs/elev60/L60e210a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e210a.dat right
+[ 30, 31 ] = ascii (fp) : "./hrtfs/elev60/L60e205a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e205a.dat right
+[ 30, 32 ] = ascii (fp) : "./hrtfs/elev60/L60e200a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e200a.dat right
+[ 30, 33 ] = ascii (fp) : "./hrtfs/elev60/L60e195a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e195a.dat right
+[ 30, 34 ] = ascii (fp) : "./hrtfs/elev60/L60e190a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e190a.dat right
+[ 30, 35 ] = ascii (fp) : "./hrtfs/elev60/L60e185a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e185a.dat right
+[ 30, 36 ] = ascii (fp) : "./hrtfs/elev60/L60e180a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e180a.dat right
+[ 30, 37 ] = ascii (fp) : "./hrtfs/elev60/L60e175a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e175a.dat right
+[ 30, 38 ] = ascii (fp) : "./hrtfs/elev60/L60e170a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e170a.dat right
+[ 30, 39 ] = ascii (fp) : "./hrtfs/elev60/L60e165a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e165a.dat right
+[ 30, 40 ] = ascii (fp) : "./hrtfs/elev60/L60e160a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e160a.dat right
+[ 30, 41 ] = ascii (fp) : "./hrtfs/elev60/L60e155a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e155a.dat right
+[ 30, 42 ] = ascii (fp) : "./hrtfs/elev60/L60e150a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e150a.dat right
+[ 30, 43 ] = ascii (fp) : "./hrtfs/elev60/L60e145a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e145a.dat right
+[ 30, 44 ] = ascii (fp) : "./hrtfs/elev60/L60e140a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e140a.dat right
+[ 30, 45 ] = ascii (fp) : "./hrtfs/elev60/L60e135a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e135a.dat right
+[ 30, 46 ] = ascii (fp) : "./hrtfs/elev60/L60e130a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e130a.dat right
+[ 30, 47 ] = ascii (fp) : "./hrtfs/elev60/L60e125a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e125a.dat right
+[ 30, 48 ] = ascii (fp) : "./hrtfs/elev60/L60e120a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e120a.dat right
+[ 30, 49 ] = ascii (fp) : "./hrtfs/elev60/L60e115a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e115a.dat right
+[ 30, 50 ] = ascii (fp) : "./hrtfs/elev60/L60e110a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e110a.dat right
+[ 30, 51 ] = ascii (fp) : "./hrtfs/elev60/L60e105a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e105a.dat right
+[ 30, 52 ] = ascii (fp) : "./hrtfs/elev60/L60e100a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e100a.dat right
+[ 30, 53 ] = ascii (fp) : "./hrtfs/elev60/L60e095a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e095a.dat right
+[ 30, 54 ] = ascii (fp) : "./hrtfs/elev60/L60e090a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e090a.dat right
+[ 30, 55 ] = ascii (fp) : "./hrtfs/elev60/L60e085a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e085a.dat right
+[ 30, 56 ] = ascii (fp) : "./hrtfs/elev60/L60e080a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e080a.dat right
+[ 30, 57 ] = ascii (fp) : "./hrtfs/elev60/L60e075a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e075a.dat right
+[ 30, 58 ] = ascii (fp) : "./hrtfs/elev60/L60e070a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e070a.dat right
+[ 30, 59 ] = ascii (fp) : "./hrtfs/elev60/L60e065a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e065a.dat right
+[ 30, 60 ] = ascii (fp) : "./hrtfs/elev60/L60e060a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e060a.dat right
+[ 30, 61 ] = ascii (fp) : "./hrtfs/elev60/L60e055a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e055a.dat right
+[ 30, 62 ] = ascii (fp) : "./hrtfs/elev60/L60e050a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e050a.dat right
+[ 30, 63 ] = ascii (fp) : "./hrtfs/elev60/L60e045a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e045a.dat right
+[ 30, 64 ] = ascii (fp) : "./hrtfs/elev60/L60e040a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e040a.dat right
+[ 30, 65 ] = ascii (fp) : "./hrtfs/elev60/L60e035a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e035a.dat right
+[ 30, 66 ] = ascii (fp) : "./hrtfs/elev60/L60e030a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e030a.dat right
+[ 30, 67 ] = ascii (fp) : "./hrtfs/elev60/L60e025a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e025a.dat right
+[ 30, 68 ] = ascii (fp) : "./hrtfs/elev60/L60e020a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e020a.dat right
+[ 30, 69 ] = ascii (fp) : "./hrtfs/elev60/L60e015a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e015a.dat right
+[ 30, 70 ] = ascii (fp) : "./hrtfs/elev60/L60e010a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e010a.dat right
+[ 30, 71 ] = ascii (fp) : "./hrtfs/elev60/L60e005a.dat left
+           + ascii (fp) : "./hrtfs/elev60/R60e005a.dat right
+
+[ 31,  0 ] = ascii (fp) : "./hrtfs/elev65/L65e000a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e000a.dat right
+[ 31,  1 ] = ascii (fp) : "./hrtfs/elev65/L65e355a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e355a.dat right
+[ 31,  2 ] = ascii (fp) : "./hrtfs/elev65/L65e350a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e350a.dat right
+[ 31,  3 ] = ascii (fp) : "./hrtfs/elev65/L65e345a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e345a.dat right
+[ 31,  4 ] = ascii (fp) : "./hrtfs/elev65/L65e340a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e340a.dat right
+[ 31,  5 ] = ascii (fp) : "./hrtfs/elev65/L65e335a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e335a.dat right
+[ 31,  6 ] = ascii (fp) : "./hrtfs/elev65/L65e330a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e330a.dat right
+[ 31,  7 ] = ascii (fp) : "./hrtfs/elev65/L65e325a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e325a.dat right
+[ 31,  8 ] = ascii (fp) : "./hrtfs/elev65/L65e320a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e320a.dat right
+[ 31,  9 ] = ascii (fp) : "./hrtfs/elev65/L65e315a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e315a.dat right
+[ 31, 10 ] = ascii (fp) : "./hrtfs/elev65/L65e310a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e310a.dat right
+[ 31, 11 ] = ascii (fp) : "./hrtfs/elev65/L65e305a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e305a.dat right
+[ 31, 12 ] = ascii (fp) : "./hrtfs/elev65/L65e300a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e300a.dat right
+[ 31, 13 ] = ascii (fp) : "./hrtfs/elev65/L65e295a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e295a.dat right
+[ 31, 14 ] = ascii (fp) : "./hrtfs/elev65/L65e290a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e290a.dat right
+[ 31, 15 ] = ascii (fp) : "./hrtfs/elev65/L65e285a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e285a.dat right
+[ 31, 16 ] = ascii (fp) : "./hrtfs/elev65/L65e280a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e280a.dat right
+[ 31, 17 ] = ascii (fp) : "./hrtfs/elev65/L65e275a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e275a.dat right
+[ 31, 18 ] = ascii (fp) : "./hrtfs/elev65/L65e270a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e270a.dat right
+[ 31, 19 ] = ascii (fp) : "./hrtfs/elev65/L65e265a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e265a.dat right
+[ 31, 20 ] = ascii (fp) : "./hrtfs/elev65/L65e260a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e260a.dat right
+[ 31, 21 ] = ascii (fp) : "./hrtfs/elev65/L65e255a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e255a.dat right
+[ 31, 22 ] = ascii (fp) : "./hrtfs/elev65/L65e250a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e250a.dat right
+[ 31, 23 ] = ascii (fp) : "./hrtfs/elev65/L65e245a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e245a.dat right
+[ 31, 24 ] = ascii (fp) : "./hrtfs/elev65/L65e240a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e240a.dat right
+[ 31, 25 ] = ascii (fp) : "./hrtfs/elev65/L65e235a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e235a.dat right
+[ 31, 26 ] = ascii (fp) : "./hrtfs/elev65/L65e230a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e230a.dat right
+[ 31, 27 ] = ascii (fp) : "./hrtfs/elev65/L65e225a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e225a.dat right
+[ 31, 28 ] = ascii (fp) : "./hrtfs/elev65/L65e220a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e220a.dat right
+[ 31, 29 ] = ascii (fp) : "./hrtfs/elev65/L65e215a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e215a.dat right
+[ 31, 30 ] = ascii (fp) : "./hrtfs/elev65/L65e210a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e210a.dat right
+[ 31, 31 ] = ascii (fp) : "./hrtfs/elev65/L65e205a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e205a.dat right
+[ 31, 32 ] = ascii (fp) : "./hrtfs/elev65/L65e200a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e200a.dat right
+[ 31, 33 ] = ascii (fp) : "./hrtfs/elev65/L65e195a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e195a.dat right
+[ 31, 34 ] = ascii (fp) : "./hrtfs/elev65/L65e190a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e190a.dat right
+[ 31, 35 ] = ascii (fp) : "./hrtfs/elev65/L65e185a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e185a.dat right
+[ 31, 36 ] = ascii (fp) : "./hrtfs/elev65/L65e180a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e180a.dat right
+[ 31, 37 ] = ascii (fp) : "./hrtfs/elev65/L65e175a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e175a.dat right
+[ 31, 38 ] = ascii (fp) : "./hrtfs/elev65/L65e170a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e170a.dat right
+[ 31, 39 ] = ascii (fp) : "./hrtfs/elev65/L65e165a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e165a.dat right
+[ 31, 40 ] = ascii (fp) : "./hrtfs/elev65/L65e160a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e160a.dat right
+[ 31, 41 ] = ascii (fp) : "./hrtfs/elev65/L65e155a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e155a.dat right
+[ 31, 42 ] = ascii (fp) : "./hrtfs/elev65/L65e150a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e150a.dat right
+[ 31, 43 ] = ascii (fp) : "./hrtfs/elev65/L65e145a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e145a.dat right
+[ 31, 44 ] = ascii (fp) : "./hrtfs/elev65/L65e140a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e140a.dat right
+[ 31, 45 ] = ascii (fp) : "./hrtfs/elev65/L65e135a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e135a.dat right
+[ 31, 46 ] = ascii (fp) : "./hrtfs/elev65/L65e130a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e130a.dat right
+[ 31, 47 ] = ascii (fp) : "./hrtfs/elev65/L65e125a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e125a.dat right
+[ 31, 48 ] = ascii (fp) : "./hrtfs/elev65/L65e120a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e120a.dat right
+[ 31, 49 ] = ascii (fp) : "./hrtfs/elev65/L65e115a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e115a.dat right
+[ 31, 50 ] = ascii (fp) : "./hrtfs/elev65/L65e110a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e110a.dat right
+[ 31, 51 ] = ascii (fp) : "./hrtfs/elev65/L65e105a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e105a.dat right
+[ 31, 52 ] = ascii (fp) : "./hrtfs/elev65/L65e100a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e100a.dat right
+[ 31, 53 ] = ascii (fp) : "./hrtfs/elev65/L65e095a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e095a.dat right
+[ 31, 54 ] = ascii (fp) : "./hrtfs/elev65/L65e090a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e090a.dat right
+[ 31, 55 ] = ascii (fp) : "./hrtfs/elev65/L65e085a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e085a.dat right
+[ 31, 56 ] = ascii (fp) : "./hrtfs/elev65/L65e080a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e080a.dat right
+[ 31, 57 ] = ascii (fp) : "./hrtfs/elev65/L65e075a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e075a.dat right
+[ 31, 58 ] = ascii (fp) : "./hrtfs/elev65/L65e070a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e070a.dat right
+[ 31, 59 ] = ascii (fp) : "./hrtfs/elev65/L65e065a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e065a.dat right
+[ 31, 60 ] = ascii (fp) : "./hrtfs/elev65/L65e060a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e060a.dat right
+[ 31, 61 ] = ascii (fp) : "./hrtfs/elev65/L65e055a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e055a.dat right
+[ 31, 62 ] = ascii (fp) : "./hrtfs/elev65/L65e050a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e050a.dat right
+[ 31, 63 ] = ascii (fp) : "./hrtfs/elev65/L65e045a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e045a.dat right
+[ 31, 64 ] = ascii (fp) : "./hrtfs/elev65/L65e040a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e040a.dat right
+[ 31, 65 ] = ascii (fp) : "./hrtfs/elev65/L65e035a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e035a.dat right
+[ 31, 66 ] = ascii (fp) : "./hrtfs/elev65/L65e030a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e030a.dat right
+[ 31, 67 ] = ascii (fp) : "./hrtfs/elev65/L65e025a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e025a.dat right
+[ 31, 68 ] = ascii (fp) : "./hrtfs/elev65/L65e020a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e020a.dat right
+[ 31, 69 ] = ascii (fp) : "./hrtfs/elev65/L65e015a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e015a.dat right
+[ 31, 70 ] = ascii (fp) : "./hrtfs/elev65/L65e010a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e010a.dat right
+[ 31, 71 ] = ascii (fp) : "./hrtfs/elev65/L65e005a.dat left
+           + ascii (fp) : "./hrtfs/elev65/R65e005a.dat right
+
+[ 32,  0 ] = ascii (fp) : "./hrtfs/elev70/L70e000a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e000a.dat right
+[ 32,  1 ] = ascii (fp) : "./hrtfs/elev70/L70e355a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e355a.dat right
+[ 32,  2 ] = ascii (fp) : "./hrtfs/elev70/L70e350a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e350a.dat right
+[ 32,  3 ] = ascii (fp) : "./hrtfs/elev70/L70e345a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e345a.dat right
+[ 32,  4 ] = ascii (fp) : "./hrtfs/elev70/L70e340a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e340a.dat right
+[ 32,  5 ] = ascii (fp) : "./hrtfs/elev70/L70e335a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e335a.dat right
+[ 32,  6 ] = ascii (fp) : "./hrtfs/elev70/L70e330a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e330a.dat right
+[ 32,  7 ] = ascii (fp) : "./hrtfs/elev70/L70e325a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e325a.dat right
+[ 32,  8 ] = ascii (fp) : "./hrtfs/elev70/L70e320a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e320a.dat right
+[ 32,  9 ] = ascii (fp) : "./hrtfs/elev70/L70e315a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e315a.dat right
+[ 32, 10 ] = ascii (fp) : "./hrtfs/elev70/L70e310a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e310a.dat right
+[ 32, 11 ] = ascii (fp) : "./hrtfs/elev70/L70e305a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e305a.dat right
+[ 32, 12 ] = ascii (fp) : "./hrtfs/elev70/L70e300a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e300a.dat right
+[ 32, 13 ] = ascii (fp) : "./hrtfs/elev70/L70e295a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e295a.dat right
+[ 32, 14 ] = ascii (fp) : "./hrtfs/elev70/L70e290a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e290a.dat right
+[ 32, 15 ] = ascii (fp) : "./hrtfs/elev70/L70e285a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e285a.dat right
+[ 32, 16 ] = ascii (fp) : "./hrtfs/elev70/L70e280a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e280a.dat right
+[ 32, 17 ] = ascii (fp) : "./hrtfs/elev70/L70e275a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e275a.dat right
+[ 32, 18 ] = ascii (fp) : "./hrtfs/elev70/L70e270a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e270a.dat right
+[ 32, 19 ] = ascii (fp) : "./hrtfs/elev70/L70e265a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e265a.dat right
+[ 32, 20 ] = ascii (fp) : "./hrtfs/elev70/L70e260a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e260a.dat right
+[ 32, 21 ] = ascii (fp) : "./hrtfs/elev70/L70e255a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e255a.dat right
+[ 32, 22 ] = ascii (fp) : "./hrtfs/elev70/L70e250a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e250a.dat right
+[ 32, 23 ] = ascii (fp) : "./hrtfs/elev70/L70e245a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e245a.dat right
+[ 32, 24 ] = ascii (fp) : "./hrtfs/elev70/L70e240a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e240a.dat right
+[ 32, 25 ] = ascii (fp) : "./hrtfs/elev70/L70e235a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e235a.dat right
+[ 32, 26 ] = ascii (fp) : "./hrtfs/elev70/L70e230a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e230a.dat right
+[ 32, 27 ] = ascii (fp) : "./hrtfs/elev70/L70e225a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e225a.dat right
+[ 32, 28 ] = ascii (fp) : "./hrtfs/elev70/L70e220a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e220a.dat right
+[ 32, 29 ] = ascii (fp) : "./hrtfs/elev70/L70e215a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e215a.dat right
+[ 32, 30 ] = ascii (fp) : "./hrtfs/elev70/L70e210a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e210a.dat right
+[ 32, 31 ] = ascii (fp) : "./hrtfs/elev70/L70e205a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e205a.dat right
+[ 32, 32 ] = ascii (fp) : "./hrtfs/elev70/L70e200a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e200a.dat right
+[ 32, 33 ] = ascii (fp) : "./hrtfs/elev70/L70e195a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e195a.dat right
+[ 32, 34 ] = ascii (fp) : "./hrtfs/elev70/L70e190a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e190a.dat right
+[ 32, 35 ] = ascii (fp) : "./hrtfs/elev70/L70e185a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e185a.dat right
+[ 32, 36 ] = ascii (fp) : "./hrtfs/elev70/L70e180a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e180a.dat right
+[ 32, 37 ] = ascii (fp) : "./hrtfs/elev70/L70e175a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e175a.dat right
+[ 32, 38 ] = ascii (fp) : "./hrtfs/elev70/L70e170a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e170a.dat right
+[ 32, 39 ] = ascii (fp) : "./hrtfs/elev70/L70e165a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e165a.dat right
+[ 32, 40 ] = ascii (fp) : "./hrtfs/elev70/L70e160a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e160a.dat right
+[ 32, 41 ] = ascii (fp) : "./hrtfs/elev70/L70e155a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e155a.dat right
+[ 32, 42 ] = ascii (fp) : "./hrtfs/elev70/L70e150a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e150a.dat right
+[ 32, 43 ] = ascii (fp) : "./hrtfs/elev70/L70e145a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e145a.dat right
+[ 32, 44 ] = ascii (fp) : "./hrtfs/elev70/L70e140a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e140a.dat right
+[ 32, 45 ] = ascii (fp) : "./hrtfs/elev70/L70e135a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e135a.dat right
+[ 32, 46 ] = ascii (fp) : "./hrtfs/elev70/L70e130a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e130a.dat right
+[ 32, 47 ] = ascii (fp) : "./hrtfs/elev70/L70e125a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e125a.dat right
+[ 32, 48 ] = ascii (fp) : "./hrtfs/elev70/L70e120a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e120a.dat right
+[ 32, 49 ] = ascii (fp) : "./hrtfs/elev70/L70e115a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e115a.dat right
+[ 32, 50 ] = ascii (fp) : "./hrtfs/elev70/L70e110a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e110a.dat right
+[ 32, 51 ] = ascii (fp) : "./hrtfs/elev70/L70e105a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e105a.dat right
+[ 32, 52 ] = ascii (fp) : "./hrtfs/elev70/L70e100a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e100a.dat right
+[ 32, 53 ] = ascii (fp) : "./hrtfs/elev70/L70e095a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e095a.dat right
+[ 32, 54 ] = ascii (fp) : "./hrtfs/elev70/L70e090a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e090a.dat right
+[ 32, 55 ] = ascii (fp) : "./hrtfs/elev70/L70e085a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e085a.dat right
+[ 32, 56 ] = ascii (fp) : "./hrtfs/elev70/L70e080a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e080a.dat right
+[ 32, 57 ] = ascii (fp) : "./hrtfs/elev70/L70e075a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e075a.dat right
+[ 32, 58 ] = ascii (fp) : "./hrtfs/elev70/L70e070a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e070a.dat right
+[ 32, 59 ] = ascii (fp) : "./hrtfs/elev70/L70e065a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e065a.dat right
+[ 32, 60 ] = ascii (fp) : "./hrtfs/elev70/L70e060a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e060a.dat right
+[ 32, 61 ] = ascii (fp) : "./hrtfs/elev70/L70e055a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e055a.dat right
+[ 32, 62 ] = ascii (fp) : "./hrtfs/elev70/L70e050a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e050a.dat right
+[ 32, 63 ] = ascii (fp) : "./hrtfs/elev70/L70e045a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e045a.dat right
+[ 32, 64 ] = ascii (fp) : "./hrtfs/elev70/L70e040a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e040a.dat right
+[ 32, 65 ] = ascii (fp) : "./hrtfs/elev70/L70e035a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e035a.dat right
+[ 32, 66 ] = ascii (fp) : "./hrtfs/elev70/L70e030a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e030a.dat right
+[ 32, 67 ] = ascii (fp) : "./hrtfs/elev70/L70e025a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e025a.dat right
+[ 32, 68 ] = ascii (fp) : "./hrtfs/elev70/L70e020a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e020a.dat right
+[ 32, 69 ] = ascii (fp) : "./hrtfs/elev70/L70e015a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e015a.dat right
+[ 32, 70 ] = ascii (fp) : "./hrtfs/elev70/L70e010a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e010a.dat right
+[ 32, 71 ] = ascii (fp) : "./hrtfs/elev70/L70e005a.dat left
+           + ascii (fp) : "./hrtfs/elev70/R70e005a.dat right
+
+[ 33,  0 ] = ascii (fp) : "./hrtfs/elev75/L75e000a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e000a.dat right
+[ 33,  1 ] = ascii (fp) : "./hrtfs/elev75/L75e355a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e355a.dat right
+[ 33,  2 ] = ascii (fp) : "./hrtfs/elev75/L75e350a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e350a.dat right
+[ 33,  3 ] = ascii (fp) : "./hrtfs/elev75/L75e345a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e345a.dat right
+[ 33,  4 ] = ascii (fp) : "./hrtfs/elev75/L75e340a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e340a.dat right
+[ 33,  5 ] = ascii (fp) : "./hrtfs/elev75/L75e335a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e335a.dat right
+[ 33,  6 ] = ascii (fp) : "./hrtfs/elev75/L75e330a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e330a.dat right
+[ 33,  7 ] = ascii (fp) : "./hrtfs/elev75/L75e325a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e325a.dat right
+[ 33,  8 ] = ascii (fp) : "./hrtfs/elev75/L75e320a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e320a.dat right
+[ 33,  9 ] = ascii (fp) : "./hrtfs/elev75/L75e315a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e315a.dat right
+[ 33, 10 ] = ascii (fp) : "./hrtfs/elev75/L75e310a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e310a.dat right
+[ 33, 11 ] = ascii (fp) : "./hrtfs/elev75/L75e305a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e305a.dat right
+[ 33, 12 ] = ascii (fp) : "./hrtfs/elev75/L75e300a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e300a.dat right
+[ 33, 13 ] = ascii (fp) : "./hrtfs/elev75/L75e295a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e295a.dat right
+[ 33, 14 ] = ascii (fp) : "./hrtfs/elev75/L75e290a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e290a.dat right
+[ 33, 15 ] = ascii (fp) : "./hrtfs/elev75/L75e285a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e285a.dat right
+[ 33, 16 ] = ascii (fp) : "./hrtfs/elev75/L75e280a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e280a.dat right
+[ 33, 17 ] = ascii (fp) : "./hrtfs/elev75/L75e275a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e275a.dat right
+[ 33, 18 ] = ascii (fp) : "./hrtfs/elev75/L75e270a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e270a.dat right
+[ 33, 19 ] = ascii (fp) : "./hrtfs/elev75/L75e265a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e265a.dat right
+[ 33, 20 ] = ascii (fp) : "./hrtfs/elev75/L75e260a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e260a.dat right
+[ 33, 21 ] = ascii (fp) : "./hrtfs/elev75/L75e255a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e255a.dat right
+[ 33, 22 ] = ascii (fp) : "./hrtfs/elev75/L75e250a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e250a.dat right
+[ 33, 23 ] = ascii (fp) : "./hrtfs/elev75/L75e245a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e245a.dat right
+[ 33, 24 ] = ascii (fp) : "./hrtfs/elev75/L75e240a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e240a.dat right
+[ 33, 25 ] = ascii (fp) : "./hrtfs/elev75/L75e235a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e235a.dat right
+[ 33, 26 ] = ascii (fp) : "./hrtfs/elev75/L75e230a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e230a.dat right
+[ 33, 27 ] = ascii (fp) : "./hrtfs/elev75/L75e225a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e225a.dat right
+[ 33, 28 ] = ascii (fp) : "./hrtfs/elev75/L75e220a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e220a.dat right
+[ 33, 29 ] = ascii (fp) : "./hrtfs/elev75/L75e215a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e215a.dat right
+[ 33, 30 ] = ascii (fp) : "./hrtfs/elev75/L75e210a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e210a.dat right
+[ 33, 31 ] = ascii (fp) : "./hrtfs/elev75/L75e205a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e205a.dat right
+[ 33, 32 ] = ascii (fp) : "./hrtfs/elev75/L75e200a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e200a.dat right
+[ 33, 33 ] = ascii (fp) : "./hrtfs/elev75/L75e195a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e195a.dat right
+[ 33, 34 ] = ascii (fp) : "./hrtfs/elev75/L75e190a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e190a.dat right
+[ 33, 35 ] = ascii (fp) : "./hrtfs/elev75/L75e185a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e185a.dat right
+[ 33, 36 ] = ascii (fp) : "./hrtfs/elev75/L75e180a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e180a.dat right
+[ 33, 37 ] = ascii (fp) : "./hrtfs/elev75/L75e175a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e175a.dat right
+[ 33, 38 ] = ascii (fp) : "./hrtfs/elev75/L75e170a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e170a.dat right
+[ 33, 39 ] = ascii (fp) : "./hrtfs/elev75/L75e165a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e165a.dat right
+[ 33, 40 ] = ascii (fp) : "./hrtfs/elev75/L75e160a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e160a.dat right
+[ 33, 41 ] = ascii (fp) : "./hrtfs/elev75/L75e155a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e155a.dat right
+[ 33, 42 ] = ascii (fp) : "./hrtfs/elev75/L75e150a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e150a.dat right
+[ 33, 43 ] = ascii (fp) : "./hrtfs/elev75/L75e145a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e145a.dat right
+[ 33, 44 ] = ascii (fp) : "./hrtfs/elev75/L75e140a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e140a.dat right
+[ 33, 45 ] = ascii (fp) : "./hrtfs/elev75/L75e135a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e135a.dat right
+[ 33, 46 ] = ascii (fp) : "./hrtfs/elev75/L75e130a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e130a.dat right
+[ 33, 47 ] = ascii (fp) : "./hrtfs/elev75/L75e125a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e125a.dat right
+[ 33, 48 ] = ascii (fp) : "./hrtfs/elev75/L75e120a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e120a.dat right
+[ 33, 49 ] = ascii (fp) : "./hrtfs/elev75/L75e115a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e115a.dat right
+[ 33, 50 ] = ascii (fp) : "./hrtfs/elev75/L75e110a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e110a.dat right
+[ 33, 51 ] = ascii (fp) : "./hrtfs/elev75/L75e105a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e105a.dat right
+[ 33, 52 ] = ascii (fp) : "./hrtfs/elev75/L75e100a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e100a.dat right
+[ 33, 53 ] = ascii (fp) : "./hrtfs/elev75/L75e095a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e095a.dat right
+[ 33, 54 ] = ascii (fp) : "./hrtfs/elev75/L75e090a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e090a.dat right
+[ 33, 55 ] = ascii (fp) : "./hrtfs/elev75/L75e085a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e085a.dat right
+[ 33, 56 ] = ascii (fp) : "./hrtfs/elev75/L75e080a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e080a.dat right
+[ 33, 57 ] = ascii (fp) : "./hrtfs/elev75/L75e075a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e075a.dat right
+[ 33, 58 ] = ascii (fp) : "./hrtfs/elev75/L75e070a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e070a.dat right
+[ 33, 59 ] = ascii (fp) : "./hrtfs/elev75/L75e065a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e065a.dat right
+[ 33, 60 ] = ascii (fp) : "./hrtfs/elev75/L75e060a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e060a.dat right
+[ 33, 61 ] = ascii (fp) : "./hrtfs/elev75/L75e055a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e055a.dat right
+[ 33, 62 ] = ascii (fp) : "./hrtfs/elev75/L75e050a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e050a.dat right
+[ 33, 63 ] = ascii (fp) : "./hrtfs/elev75/L75e045a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e045a.dat right
+[ 33, 64 ] = ascii (fp) : "./hrtfs/elev75/L75e040a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e040a.dat right
+[ 33, 65 ] = ascii (fp) : "./hrtfs/elev75/L75e035a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e035a.dat right
+[ 33, 66 ] = ascii (fp) : "./hrtfs/elev75/L75e030a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e030a.dat right
+[ 33, 67 ] = ascii (fp) : "./hrtfs/elev75/L75e025a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e025a.dat right
+[ 33, 68 ] = ascii (fp) : "./hrtfs/elev75/L75e020a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e020a.dat right
+[ 33, 69 ] = ascii (fp) : "./hrtfs/elev75/L75e015a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e015a.dat right
+[ 33, 70 ] = ascii (fp) : "./hrtfs/elev75/L75e010a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e010a.dat right
+[ 33, 71 ] = ascii (fp) : "./hrtfs/elev75/L75e005a.dat left
+           + ascii (fp) : "./hrtfs/elev75/R75e005a.dat right
+
+[ 34,  0 ] = ascii (fp) : "./hrtfs/elev80/L80e000a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e000a.dat right
+[ 34,  1 ] = ascii (fp) : "./hrtfs/elev80/L80e355a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e355a.dat right
+[ 34,  2 ] = ascii (fp) : "./hrtfs/elev80/L80e350a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e350a.dat right
+[ 34,  3 ] = ascii (fp) : "./hrtfs/elev80/L80e345a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e345a.dat right
+[ 34,  4 ] = ascii (fp) : "./hrtfs/elev80/L80e340a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e340a.dat right
+[ 34,  5 ] = ascii (fp) : "./hrtfs/elev80/L80e335a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e335a.dat right
+[ 34,  6 ] = ascii (fp) : "./hrtfs/elev80/L80e330a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e330a.dat right
+[ 34,  7 ] = ascii (fp) : "./hrtfs/elev80/L80e325a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e325a.dat right
+[ 34,  8 ] = ascii (fp) : "./hrtfs/elev80/L80e320a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e320a.dat right
+[ 34,  9 ] = ascii (fp) : "./hrtfs/elev80/L80e315a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e315a.dat right
+[ 34, 10 ] = ascii (fp) : "./hrtfs/elev80/L80e310a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e310a.dat right
+[ 34, 11 ] = ascii (fp) : "./hrtfs/elev80/L80e305a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e305a.dat right
+[ 34, 12 ] = ascii (fp) : "./hrtfs/elev80/L80e300a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e300a.dat right
+[ 34, 13 ] = ascii (fp) : "./hrtfs/elev80/L80e295a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e295a.dat right
+[ 34, 14 ] = ascii (fp) : "./hrtfs/elev80/L80e290a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e290a.dat right
+[ 34, 15 ] = ascii (fp) : "./hrtfs/elev80/L80e285a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e285a.dat right
+[ 34, 16 ] = ascii (fp) : "./hrtfs/elev80/L80e280a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e280a.dat right
+[ 34, 17 ] = ascii (fp) : "./hrtfs/elev80/L80e275a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e275a.dat right
+[ 34, 18 ] = ascii (fp) : "./hrtfs/elev80/L80e270a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e270a.dat right
+[ 34, 19 ] = ascii (fp) : "./hrtfs/elev80/L80e265a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e265a.dat right
+[ 34, 20 ] = ascii (fp) : "./hrtfs/elev80/L80e260a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e260a.dat right
+[ 34, 21 ] = ascii (fp) : "./hrtfs/elev80/L80e255a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e255a.dat right
+[ 34, 22 ] = ascii (fp) : "./hrtfs/elev80/L80e250a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e250a.dat right
+[ 34, 23 ] = ascii (fp) : "./hrtfs/elev80/L80e245a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e245a.dat right
+[ 34, 24 ] = ascii (fp) : "./hrtfs/elev80/L80e240a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e240a.dat right
+[ 34, 25 ] = ascii (fp) : "./hrtfs/elev80/L80e235a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e235a.dat right
+[ 34, 26 ] = ascii (fp) : "./hrtfs/elev80/L80e230a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e230a.dat right
+[ 34, 27 ] = ascii (fp) : "./hrtfs/elev80/L80e225a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e225a.dat right
+[ 34, 28 ] = ascii (fp) : "./hrtfs/elev80/L80e220a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e220a.dat right
+[ 34, 29 ] = ascii (fp) : "./hrtfs/elev80/L80e215a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e215a.dat right
+[ 34, 30 ] = ascii (fp) : "./hrtfs/elev80/L80e210a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e210a.dat right
+[ 34, 31 ] = ascii (fp) : "./hrtfs/elev80/L80e205a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e205a.dat right
+[ 34, 32 ] = ascii (fp) : "./hrtfs/elev80/L80e200a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e200a.dat right
+[ 34, 33 ] = ascii (fp) : "./hrtfs/elev80/L80e195a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e195a.dat right
+[ 34, 34 ] = ascii (fp) : "./hrtfs/elev80/L80e190a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e190a.dat right
+[ 34, 35 ] = ascii (fp) : "./hrtfs/elev80/L80e185a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e185a.dat right
+[ 34, 36 ] = ascii (fp) : "./hrtfs/elev80/L80e180a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e180a.dat right
+[ 34, 37 ] = ascii (fp) : "./hrtfs/elev80/L80e175a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e175a.dat right
+[ 34, 38 ] = ascii (fp) : "./hrtfs/elev80/L80e170a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e170a.dat right
+[ 34, 39 ] = ascii (fp) : "./hrtfs/elev80/L80e165a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e165a.dat right
+[ 34, 40 ] = ascii (fp) : "./hrtfs/elev80/L80e160a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e160a.dat right
+[ 34, 41 ] = ascii (fp) : "./hrtfs/elev80/L80e155a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e155a.dat right
+[ 34, 42 ] = ascii (fp) : "./hrtfs/elev80/L80e150a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e150a.dat right
+[ 34, 43 ] = ascii (fp) : "./hrtfs/elev80/L80e145a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e145a.dat right
+[ 34, 44 ] = ascii (fp) : "./hrtfs/elev80/L80e140a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e140a.dat right
+[ 34, 45 ] = ascii (fp) : "./hrtfs/elev80/L80e135a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e135a.dat right
+[ 34, 46 ] = ascii (fp) : "./hrtfs/elev80/L80e130a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e130a.dat right
+[ 34, 47 ] = ascii (fp) : "./hrtfs/elev80/L80e125a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e125a.dat right
+[ 34, 48 ] = ascii (fp) : "./hrtfs/elev80/L80e120a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e120a.dat right
+[ 34, 49 ] = ascii (fp) : "./hrtfs/elev80/L80e115a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e115a.dat right
+[ 34, 50 ] = ascii (fp) : "./hrtfs/elev80/L80e110a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e110a.dat right
+[ 34, 51 ] = ascii (fp) : "./hrtfs/elev80/L80e105a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e105a.dat right
+[ 34, 52 ] = ascii (fp) : "./hrtfs/elev80/L80e100a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e100a.dat right
+[ 34, 53 ] = ascii (fp) : "./hrtfs/elev80/L80e095a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e095a.dat right
+[ 34, 54 ] = ascii (fp) : "./hrtfs/elev80/L80e090a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e090a.dat right
+[ 34, 55 ] = ascii (fp) : "./hrtfs/elev80/L80e085a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e085a.dat right
+[ 34, 56 ] = ascii (fp) : "./hrtfs/elev80/L80e080a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e080a.dat right
+[ 34, 57 ] = ascii (fp) : "./hrtfs/elev80/L80e075a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e075a.dat right
+[ 34, 58 ] = ascii (fp) : "./hrtfs/elev80/L80e070a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e070a.dat right
+[ 34, 59 ] = ascii (fp) : "./hrtfs/elev80/L80e065a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e065a.dat right
+[ 34, 60 ] = ascii (fp) : "./hrtfs/elev80/L80e060a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e060a.dat right
+[ 34, 61 ] = ascii (fp) : "./hrtfs/elev80/L80e055a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e055a.dat right
+[ 34, 62 ] = ascii (fp) : "./hrtfs/elev80/L80e050a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e050a.dat right
+[ 34, 63 ] = ascii (fp) : "./hrtfs/elev80/L80e045a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e045a.dat right
+[ 34, 64 ] = ascii (fp) : "./hrtfs/elev80/L80e040a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e040a.dat right
+[ 34, 65 ] = ascii (fp) : "./hrtfs/elev80/L80e035a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e035a.dat right
+[ 34, 66 ] = ascii (fp) : "./hrtfs/elev80/L80e030a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e030a.dat right
+[ 34, 67 ] = ascii (fp) : "./hrtfs/elev80/L80e025a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e025a.dat right
+[ 34, 68 ] = ascii (fp) : "./hrtfs/elev80/L80e020a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e020a.dat right
+[ 34, 69 ] = ascii (fp) : "./hrtfs/elev80/L80e015a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e015a.dat right
+[ 34, 70 ] = ascii (fp) : "./hrtfs/elev80/L80e010a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e010a.dat right
+[ 34, 71 ] = ascii (fp) : "./hrtfs/elev80/L80e005a.dat left
+           + ascii (fp) : "./hrtfs/elev80/R80e005a.dat right
+
+[ 35,  0 ] = ascii (fp) : "./hrtfs/elev85/L85e000a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e000a.dat right
+[ 35,  1 ] = ascii (fp) : "./hrtfs/elev85/L85e355a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e355a.dat right
+[ 35,  2 ] = ascii (fp) : "./hrtfs/elev85/L85e350a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e350a.dat right
+[ 35,  3 ] = ascii (fp) : "./hrtfs/elev85/L85e345a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e345a.dat right
+[ 35,  4 ] = ascii (fp) : "./hrtfs/elev85/L85e340a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e340a.dat right
+[ 35,  5 ] = ascii (fp) : "./hrtfs/elev85/L85e335a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e335a.dat right
+[ 35,  6 ] = ascii (fp) : "./hrtfs/elev85/L85e330a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e330a.dat right
+[ 35,  7 ] = ascii (fp) : "./hrtfs/elev85/L85e325a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e325a.dat right
+[ 35,  8 ] = ascii (fp) : "./hrtfs/elev85/L85e320a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e320a.dat right
+[ 35,  9 ] = ascii (fp) : "./hrtfs/elev85/L85e315a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e315a.dat right
+[ 35, 10 ] = ascii (fp) : "./hrtfs/elev85/L85e310a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e310a.dat right
+[ 35, 11 ] = ascii (fp) : "./hrtfs/elev85/L85e305a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e305a.dat right
+[ 35, 12 ] = ascii (fp) : "./hrtfs/elev85/L85e300a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e300a.dat right
+[ 35, 13 ] = ascii (fp) : "./hrtfs/elev85/L85e295a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e295a.dat right
+[ 35, 14 ] = ascii (fp) : "./hrtfs/elev85/L85e290a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e290a.dat right
+[ 35, 15 ] = ascii (fp) : "./hrtfs/elev85/L85e285a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e285a.dat right
+[ 35, 16 ] = ascii (fp) : "./hrtfs/elev85/L85e280a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e280a.dat right
+[ 35, 17 ] = ascii (fp) : "./hrtfs/elev85/L85e275a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e275a.dat right
+[ 35, 18 ] = ascii (fp) : "./hrtfs/elev85/L85e270a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e270a.dat right
+[ 35, 19 ] = ascii (fp) : "./hrtfs/elev85/L85e265a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e265a.dat right
+[ 35, 20 ] = ascii (fp) : "./hrtfs/elev85/L85e260a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e260a.dat right
+[ 35, 21 ] = ascii (fp) : "./hrtfs/elev85/L85e255a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e255a.dat right
+[ 35, 22 ] = ascii (fp) : "./hrtfs/elev85/L85e250a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e250a.dat right
+[ 35, 23 ] = ascii (fp) : "./hrtfs/elev85/L85e245a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e245a.dat right
+[ 35, 24 ] = ascii (fp) : "./hrtfs/elev85/L85e240a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e240a.dat right
+[ 35, 25 ] = ascii (fp) : "./hrtfs/elev85/L85e235a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e235a.dat right
+[ 35, 26 ] = ascii (fp) : "./hrtfs/elev85/L85e230a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e230a.dat right
+[ 35, 27 ] = ascii (fp) : "./hrtfs/elev85/L85e225a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e225a.dat right
+[ 35, 28 ] = ascii (fp) : "./hrtfs/elev85/L85e220a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e220a.dat right
+[ 35, 29 ] = ascii (fp) : "./hrtfs/elev85/L85e215a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e215a.dat right
+[ 35, 30 ] = ascii (fp) : "./hrtfs/elev85/L85e210a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e210a.dat right
+[ 35, 31 ] = ascii (fp) : "./hrtfs/elev85/L85e205a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e205a.dat right
+[ 35, 32 ] = ascii (fp) : "./hrtfs/elev85/L85e200a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e200a.dat right
+[ 35, 33 ] = ascii (fp) : "./hrtfs/elev85/L85e195a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e195a.dat right
+[ 35, 34 ] = ascii (fp) : "./hrtfs/elev85/L85e190a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e190a.dat right
+[ 35, 35 ] = ascii (fp) : "./hrtfs/elev85/L85e185a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e185a.dat right
+[ 35, 36 ] = ascii (fp) : "./hrtfs/elev85/L85e180a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e180a.dat right
+[ 35, 37 ] = ascii (fp) : "./hrtfs/elev85/L85e175a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e175a.dat right
+[ 35, 38 ] = ascii (fp) : "./hrtfs/elev85/L85e170a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e170a.dat right
+[ 35, 39 ] = ascii (fp) : "./hrtfs/elev85/L85e165a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e165a.dat right
+[ 35, 40 ] = ascii (fp) : "./hrtfs/elev85/L85e160a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e160a.dat right
+[ 35, 41 ] = ascii (fp) : "./hrtfs/elev85/L85e155a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e155a.dat right
+[ 35, 42 ] = ascii (fp) : "./hrtfs/elev85/L85e150a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e150a.dat right
+[ 35, 43 ] = ascii (fp) : "./hrtfs/elev85/L85e145a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e145a.dat right
+[ 35, 44 ] = ascii (fp) : "./hrtfs/elev85/L85e140a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e140a.dat right
+[ 35, 45 ] = ascii (fp) : "./hrtfs/elev85/L85e135a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e135a.dat right
+[ 35, 46 ] = ascii (fp) : "./hrtfs/elev85/L85e130a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e130a.dat right
+[ 35, 47 ] = ascii (fp) : "./hrtfs/elev85/L85e125a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e125a.dat right
+[ 35, 48 ] = ascii (fp) : "./hrtfs/elev85/L85e120a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e120a.dat right
+[ 35, 49 ] = ascii (fp) : "./hrtfs/elev85/L85e115a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e115a.dat right
+[ 35, 50 ] = ascii (fp) : "./hrtfs/elev85/L85e110a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e110a.dat right
+[ 35, 51 ] = ascii (fp) : "./hrtfs/elev85/L85e105a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e105a.dat right
+[ 35, 52 ] = ascii (fp) : "./hrtfs/elev85/L85e100a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e100a.dat right
+[ 35, 53 ] = ascii (fp) : "./hrtfs/elev85/L85e095a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e095a.dat right
+[ 35, 54 ] = ascii (fp) : "./hrtfs/elev85/L85e090a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e090a.dat right
+[ 35, 55 ] = ascii (fp) : "./hrtfs/elev85/L85e085a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e085a.dat right
+[ 35, 56 ] = ascii (fp) : "./hrtfs/elev85/L85e080a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e080a.dat right
+[ 35, 57 ] = ascii (fp) : "./hrtfs/elev85/L85e075a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e075a.dat right
+[ 35, 58 ] = ascii (fp) : "./hrtfs/elev85/L85e070a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e070a.dat right
+[ 35, 59 ] = ascii (fp) : "./hrtfs/elev85/L85e065a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e065a.dat right
+[ 35, 60 ] = ascii (fp) : "./hrtfs/elev85/L85e060a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e060a.dat right
+[ 35, 61 ] = ascii (fp) : "./hrtfs/elev85/L85e055a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e055a.dat right
+[ 35, 62 ] = ascii (fp) : "./hrtfs/elev85/L85e050a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e050a.dat right
+[ 35, 63 ] = ascii (fp) : "./hrtfs/elev85/L85e045a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e045a.dat right
+[ 35, 64 ] = ascii (fp) : "./hrtfs/elev85/L85e040a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e040a.dat right
+[ 35, 65 ] = ascii (fp) : "./hrtfs/elev85/L85e035a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e035a.dat right
+[ 35, 66 ] = ascii (fp) : "./hrtfs/elev85/L85e030a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e030a.dat right
+[ 35, 67 ] = ascii (fp) : "./hrtfs/elev85/L85e025a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e025a.dat right
+[ 35, 68 ] = ascii (fp) : "./hrtfs/elev85/L85e020a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e020a.dat right
+[ 35, 69 ] = ascii (fp) : "./hrtfs/elev85/L85e015a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e015a.dat right
+[ 35, 70 ] = ascii (fp) : "./hrtfs/elev85/L85e010a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e010a.dat right
+[ 35, 71 ] = ascii (fp) : "./hrtfs/elev85/L85e005a.dat left
+           + ascii (fp) : "./hrtfs/elev85/R85e005a.dat right
+
+[ 36,  0 ] = ascii (fp) : "./hrtfs/elev90/L90e000a.dat left
+           + ascii (fp) : "./hrtfs/elev90/R90e000a.dat right
+
+
diff --git a/utils/IRC_1005.def b/utils/IRC_1005.def
new file mode 100644 (file)
index 0000000..c91e7b4
--- /dev/null
@@ -0,0 +1,425 @@
+# This is a makemhr HRIR definition file.  It is used to define the layout and
+# source data to be processed into an OpenAL Soft compatible HRTF.
+#
+# This definition is used to transform the left and right ear HRIRs of any
+# raw data set from the IRCAM/AKG Listen HRTF database.
+#
+# The data sets are available free of charge from:
+#
+#  http://recherche.ircam.fr/equipes/salles/listen/index.html
+#
+# Contact for the Listen HRTF Database:
+#
+#  Olivier Warusfel <olivier.warusfel@ircam.fr>,
+#  Room Acoustics Team, IRCAM
+#  1, place Igor Stravinsky
+#  75004 PARIS, France
+
+rate     = 44100
+
+# The IRCAM sets are stereo because they provide both ear HRIRs.
+type     = stereo
+
+# The raw sets have up to 8192 samples, but 2048 seems large enough.
+points   = 2048
+
+# No head radius was provided.  Just use the average radius of 9 cm.
+radius   = 0.09
+
+# The IRCAM sets are single-field (like most others) with a distance between
+# the source and the listener of 1.95 meters.
+distance = 1.95
+
+# This set isn't as dense as the MIT set.
+azimuths = 1, 6, 12, 24, 24, 24, 24, 24, 24, 24, 12, 6, 1
+
+# The IRCAM source azimuth is counter-clockwise, so it needs to be flipped.
+# Left and right ear HRIRs (from the respective WAVE channels) are used to
+# create a stereo HRTF.
+
+# Replace all occurrences of IRC_#### for the desired subject (1005 was used
+# in this demonstration).
+[  3,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P315.wav" right
+[  3,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P315.wav" right
+[  3,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P315.wav" right
+[  3,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P315.wav" right
+[  3,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P315.wav" right
+[  3,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P315.wav" right
+[  3,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P315.wav" right
+[  3,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P315.wav" right
+[  3,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P315.wav" right
+[  3,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P315.wav" right
+[  3, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P315.wav" right
+[  3, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P315.wav" right
+[  3, 12 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P315.wav" right
+[  3, 13 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P315.wav" right
+[  3, 14 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P315.wav" right
+[  3, 15 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P315.wav" right
+[  3, 16 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P315.wav" right
+[  3, 17 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P315.wav" right
+[  3, 18 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P315.wav" right
+[  3, 19 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P315.wav" right
+[  3, 20 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P315.wav" right
+[  3, 21 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P315.wav" right
+[  3, 22 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P315.wav" right
+[  3, 23 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P315.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P315.wav" right
+
+[  4,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P330.wav" right
+[  4,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P330.wav" right
+[  4,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P330.wav" right
+[  4,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P330.wav" right
+[  4,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P330.wav" right
+[  4,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P330.wav" right
+[  4,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P330.wav" right
+[  4,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P330.wav" right
+[  4,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P330.wav" right
+[  4,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P330.wav" right
+[  4, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P330.wav" right
+[  4, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P330.wav" right
+[  4, 12 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P330.wav" right
+[  4, 13 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P330.wav" right
+[  4, 14 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P330.wav" right
+[  4, 15 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P330.wav" right
+[  4, 16 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P330.wav" right
+[  4, 17 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P330.wav" right
+[  4, 18 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P330.wav" right
+[  4, 19 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P330.wav" right
+[  4, 20 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P330.wav" right
+[  4, 21 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P330.wav" right
+[  4, 22 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P330.wav" right
+[  4, 23 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P330.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P330.wav" right
+
+[  5,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P345.wav" right
+[  5,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P345.wav" right
+[  5,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P345.wav" right
+[  5,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P345.wav" right
+[  5,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P345.wav" right
+[  5,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P345.wav" right
+[  5,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P345.wav" right
+[  5,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P345.wav" right
+[  5,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P345.wav" right
+[  5,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P345.wav" right
+[  5, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P345.wav" right
+[  5, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P345.wav" right
+[  5, 12 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P345.wav" right
+[  5, 13 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P345.wav" right
+[  5, 14 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P345.wav" right
+[  5, 15 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P345.wav" right
+[  5, 16 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P345.wav" right
+[  5, 17 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P345.wav" right
+[  5, 18 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P345.wav" right
+[  5, 19 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P345.wav" right
+[  5, 20 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P345.wav" right
+[  5, 21 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P345.wav" right
+[  5, 22 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P345.wav" right
+[  5, 23 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P345.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P345.wav" right
+
+[  6,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P000.wav" right
+[  6,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P000.wav" right
+[  6,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P000.wav" right
+[  6,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P000.wav" right
+[  6,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P000.wav" right
+[  6,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P000.wav" right
+[  6,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P000.wav" right
+[  6,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P000.wav" right
+[  6,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P000.wav" right
+[  6,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P000.wav" right
+[  6, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P000.wav" right
+[  6, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P000.wav" right
+[  6, 12 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P000.wav" right
+[  6, 13 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P000.wav" right
+[  6, 14 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P000.wav" right
+[  6, 15 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P000.wav" right
+[  6, 16 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P000.wav" right
+[  6, 17 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P000.wav" right
+[  6, 18 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P000.wav" right
+[  6, 19 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P000.wav" right
+[  6, 20 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P000.wav" right
+[  6, 21 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P000.wav" right
+[  6, 22 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P000.wav" right
+[  6, 23 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P000.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P000.wav" right
+
+[  7,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P015.wav" right
+[  7,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P015.wav" right
+[  7,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P015.wav" right
+[  7,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P015.wav" right
+[  7,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P015.wav" right
+[  7,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P015.wav" right
+[  7,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P015.wav" right
+[  7,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P015.wav" right
+[  7,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P015.wav" right
+[  7,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P015.wav" right
+[  7, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P015.wav" right
+[  7, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P015.wav" right
+[  7, 12 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P015.wav" right
+[  7, 13 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P015.wav" right
+[  7, 14 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P015.wav" right
+[  7, 15 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P015.wav" right
+[  7, 16 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P015.wav" right
+[  7, 17 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P015.wav" right
+[  7, 18 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P015.wav" right
+[  7, 19 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P015.wav" right
+[  7, 20 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P015.wav" right
+[  7, 21 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P015.wav" right
+[  7, 22 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P015.wav" right
+[  7, 23 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P015.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P015.wav" right
+
+[  8,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P030.wav" right
+[  8,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P030.wav" right
+[  8,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P030.wav" right
+[  8,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P030.wav" right
+[  8,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P030.wav" right
+[  8,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P030.wav" right
+[  8,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P030.wav" right
+[  8,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P030.wav" right
+[  8,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P030.wav" right
+[  8,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P030.wav" right
+[  8, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P030.wav" right
+[  8, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P030.wav" right
+[  8, 12 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P030.wav" right
+[  8, 13 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P030.wav" right
+[  8, 14 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P030.wav" right
+[  8, 15 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P030.wav" right
+[  8, 16 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P030.wav" right
+[  8, 17 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P030.wav" right
+[  8, 18 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P030.wav" right
+[  8, 19 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P030.wav" right
+[  8, 20 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P030.wav" right
+[  8, 21 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P030.wav" right
+[  8, 22 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P030.wav" right
+[  8, 23 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P030.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P030.wav" right
+
+[  9,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P045.wav" right
+[  9,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T345_P045.wav" right
+[  9,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P045.wav" right
+[  9,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T315_P045.wav" right
+[  9,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P045.wav" right
+[  9,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T285_P045.wav" right
+[  9,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P045.wav" right
+[  9,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T255_P045.wav" right
+[  9,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P045.wav" right
+[  9,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T225_P045.wav" right
+[  9, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P045.wav" right
+[  9, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T195_P045.wav" right
+[  9, 12 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P045.wav" right
+[  9, 13 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T165_P045.wav" right
+[  9, 14 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P045.wav" right
+[  9, 15 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T135_P045.wav" right
+[  9, 16 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P045.wav" right
+[  9, 17 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T105_P045.wav" right
+[  9, 18 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P045.wav" right
+[  9, 19 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T075_P045.wav" right
+[  9, 20 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P045.wav" right
+[  9, 21 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T045_P045.wav" right
+[  9, 22 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P045.wav" right
+[  9, 23 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P045.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T015_P045.wav" right
+
+[ 10,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P060.wav" right
+[ 10,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T330_P060.wav" right
+[ 10,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P060.wav" right
+[ 10,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T270_P060.wav" right
+[ 10,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P060.wav" right
+[ 10,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T210_P060.wav" right
+[ 10,  6 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P060.wav" right
+[ 10,  7 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T150_P060.wav" right
+[ 10,  8 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P060.wav" right
+[ 10,  9 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T090_P060.wav" right
+[ 10, 10 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P060.wav" right
+[ 10, 11 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P060.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T030_P060.wav" right
+
+[ 11,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P075.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P075.wav" right
+[ 11,  1 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P075.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T300_P075.wav" right
+[ 11,  2 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P075.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T240_P075.wav" right
+[ 11,  3 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P075.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T180_P075.wav" right
+[ 11,  4 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P075.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T120_P075.wav" right
+[ 11,  5 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P075.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T060_P075.wav" right
+
+[ 12,  0 ] = wave (0) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P090.wav" left
+           + wave (1) : "./IRC/RAW/WAV/IRC_1005_R/IRC_1005_R_R0195_T000_P090.wav" right
+
diff --git a/utils/MIT_KEMAR.def b/utils/MIT_KEMAR.def
new file mode 100644 (file)
index 0000000..15036d9
--- /dev/null
@@ -0,0 +1,844 @@
+# This is a makemhr HRIR definition file.  It is used to define the layout and
+# source data to be processed into an OpenAL Soft compatible HRTF.
+#
+# This definition is used to transform the left ear HRIRs from the full set
+# of KEMAR HRIRs provided by Bill Gardner <billg@media.mit.edu> and Keith
+# Martin <kdm@media.mit.edu> of MIT Media Laboratory.
+#
+# The data (full.zip) is available from:
+#
+#  http://sound.media.mit.edu/resources/KEMAR.html
+#
+# It is copyrighted 1994 by MIT Media Laboratory, and provided free of charge
+# with no restrictions on use so long as the authors (above) are cited.
+#
+# This definition is used to generate the default HRTF table used by OpenAL
+# Soft.
+
+# The following are the data set metrics.  They must always be specified at
+# the start of a definition file, but their order is not important.
+
+# Sampling rate of the HRIR data (in hertz).
+rate     = 44100
+
+# The channel type of incoming HRIR data (mono or stereo).  Mono channel
+# inputs will result in mirroring to provide the right ear HRIRs.  If not
+# specified, this defaults to mono.
+type     = mono
+
+# The number of points to use from the HRIR data.  This should be a
+# sufficiently large value (to encompass the entire impulse response).  It
+# cannot be smaller than the truncation size (default is 32) specified on the
+# command line.
+points   = 512
+
+# The radius of the listener's head (measured ear-to-ear in meters).  The
+# makemhr utility uses this value to rescale measured propagation delays when
+# a custom head radius is specified on the command line.  It is also used as
+# the default radius when the spherical model is used to calculate an
+# approximate set of delays.  This should match the data set as close as
+# possible for accurate rescaling when using the measured delays (the
+# default).  At the moment, radius rescaling does not adjust HRIR coupling.
+radius   = 0.09
+
+# A list of the distances between the source and the listener (in meters) for
+# each field.  These must start at or above the head radius and proceed in
+# ascending order.  Since the MIT set is single-field, there is only one
+# distance.
+distance = 1.4
+
+# A list of the number of azimuths measured for each elevation per field.
+# Elevations are separated by commas (,) while fields are separated by
+# semicolons (;).  There must be at least 5 elevations covering 180 degrees
+# degrees of elevation for the data set to be viable.  The poles (first and
+# last elevation) must be singular (an azimuth count of 1).
+azimuths = 1, 12, 24, 36, 45, 56, 60, 72, 72, 72, 72, 72, 60, 56, 45, 36, 24, 12, 1
+
+# Following the metrics is the list of source HRIRs for each field,
+# elevation, and azimuth triplet.  They don't have to be specified in order,
+# but the final composition must not be sparse.  They can however begin above
+# a number of elevations (as typical for HRIR measurements).
+#
+# The field index is used to determine the distance coordinate (for mult-
+# field HRTFs) while the elevation and azimuth indices are used to determine
+# the resulting polar coordinates following OpenAL Soft's convention (-90
+# degree elevation increasing counter-clockwise from the bottom; 0 degree
+# azimuth increasing clockwise from the front).
+#
+# More than one HRIR can be used per source.  This allows the composition of
+# averaged magnitude responses or the specification of stereo HRTFs.  Target
+# ears must (and can only be) specified for each source when the type metric
+# is set to 'stereo'.
+#
+# Source specification is of the form (~BNF):
+#
+#  source = ( sf_index | mf_index ) source_ref [ '+' source_ref ]*
+#
+#  sf_index = '[' ev_index ',' az_index ']' '='
+#  mf_index = '[' fd_index ',' ev_index ',' az_index ']' '='
+#  source_ref = mono_ref | stereo_ref
+#
+#  fd_index   = unsigned_integer
+#  ev_index   = unsigned_integer
+#  az_index   = unsigned_integer
+#  mono_ref   = ref_spec ':' filename
+#  stereo_ref = ref_spec ':' filename ear
+#
+#  ref_spec = ( wave_fmt  '(' wave_parms ')' [ '@' start_sample  ] ) |
+#             ( bin_fmt   '(' bini_parms ')' [ '@' start_byte    ] ) |
+#             ( bin_fmt   '(' binf_parms ')' [ '@' start_byte    ] ) |
+#             ( ascii_fmt '(' asci_parms ')' [ '@' start_element ] ) |
+#             ( ascii_fmt '(' ascf_parms ')' [ '@' start_element ] )
+#  filename = double_quoted_string
+#  ear      = 'left' | 'right'
+#
+#  wave_fmt      = 'wave'
+#  wave_parms    = channel
+#  bin_fmt       = 'bin_le' | 'bin_be'
+#  bini_parms    = 'int' ',' byte_size [ ',' bin_sig_bits ] [ ';' skip_bytes ]
+#  binf_parms    = 'fp'  ',' byte_size                      [ ';' skip_bytes ]
+#  ascii_fmt     = 'ascii'
+#  asci_parms    = 'int' ',' sig_bits  [ ';' skip_elements ]
+#  ascf_parms    = 'fp'                [ ';' skip_elements ]
+#  start_sample  = unsigned_integer
+#  start_byte    = unsigned_integer
+#  start_element = unsigned_integer
+#
+#  channel             = unsigned_integer
+#  byte_size           = unsigned_integer
+#  bin_sig_bits        = signed_integer
+#  skip_bytes          = unsigned_integer
+#  sig_bits            = unsigned_integer
+#  skip_elements       = unsigned_integer
+#
+# For bin_sig_bits, positive values mean the significant bits start at the
+# MSB (padding toward the LSB) while negative values mean they start at the
+# LSB.
+
+# Even though the MIT set is provided as stereo .wav files, each channel is
+# for a different sized KEMAR ear.  Since it is not a stereo data set, no ear
+# is specified.  The smaller KEMAR ear (in the left channel: 0) is used.
+[  5,  0 ] = wave (0) : "./MITfull/elev-40/L-40e000a.wav"
+[  5,  1 ] = wave (0) : "./MITfull/elev-40/L-40e006a.wav"
+[  5,  2 ] = wave (0) : "./MITfull/elev-40/L-40e013a.wav"
+[  5,  3 ] = wave (0) : "./MITfull/elev-40/L-40e019a.wav"
+[  5,  4 ] = wave (0) : "./MITfull/elev-40/L-40e026a.wav"
+[  5,  5 ] = wave (0) : "./MITfull/elev-40/L-40e032a.wav"
+[  5,  6 ] = wave (0) : "./MITfull/elev-40/L-40e039a.wav"
+[  5,  7 ] = wave (0) : "./MITfull/elev-40/L-40e045a.wav"
+[  5,  8 ] = wave (0) : "./MITfull/elev-40/L-40e051a.wav"
+[  5,  9 ] = wave (0) : "./MITfull/elev-40/L-40e058a.wav"
+[  5, 10 ] = wave (0) : "./MITfull/elev-40/L-40e064a.wav"
+[  5, 11 ] = wave (0) : "./MITfull/elev-40/L-40e071a.wav"
+[  5, 12 ] = wave (0) : "./MITfull/elev-40/L-40e077a.wav"
+[  5, 13 ] = wave (0) : "./MITfull/elev-40/L-40e084a.wav"
+[  5, 14 ] = wave (0) : "./MITfull/elev-40/L-40e090a.wav"
+[  5, 15 ] = wave (0) : "./MITfull/elev-40/L-40e096a.wav"
+[  5, 16 ] = wave (0) : "./MITfull/elev-40/L-40e103a.wav"
+[  5, 17 ] = wave (0) : "./MITfull/elev-40/L-40e109a.wav"
+[  5, 18 ] = wave (0) : "./MITfull/elev-40/L-40e116a.wav"
+[  5, 19 ] = wave (0) : "./MITfull/elev-40/L-40e122a.wav"
+[  5, 20 ] = wave (0) : "./MITfull/elev-40/L-40e129a.wav"
+[  5, 21 ] = wave (0) : "./MITfull/elev-40/L-40e135a.wav"
+[  5, 22 ] = wave (0) : "./MITfull/elev-40/L-40e141a.wav"
+[  5, 23 ] = wave (0) : "./MITfull/elev-40/L-40e148a.wav"
+[  5, 24 ] = wave (0) : "./MITfull/elev-40/L-40e154a.wav"
+[  5, 25 ] = wave (0) : "./MITfull/elev-40/L-40e161a.wav"
+[  5, 26 ] = wave (0) : "./MITfull/elev-40/L-40e167a.wav"
+[  5, 27 ] = wave (0) : "./MITfull/elev-40/L-40e174a.wav"
+[  5, 28 ] = wave (0) : "./MITfull/elev-40/L-40e180a.wav"
+[  5, 29 ] = wave (0) : "./MITfull/elev-40/L-40e186a.wav"
+[  5, 30 ] = wave (0) : "./MITfull/elev-40/L-40e193a.wav"
+[  5, 31 ] = wave (0) : "./MITfull/elev-40/L-40e199a.wav"
+[  5, 32 ] = wave (0) : "./MITfull/elev-40/L-40e206a.wav"
+[  5, 33 ] = wave (0) : "./MITfull/elev-40/L-40e212a.wav"
+[  5, 34 ] = wave (0) : "./MITfull/elev-40/L-40e219a.wav"
+[  5, 35 ] = wave (0) : "./MITfull/elev-40/L-40e225a.wav"
+[  5, 36 ] = wave (0) : "./MITfull/elev-40/L-40e231a.wav"
+[  5, 37 ] = wave (0) : "./MITfull/elev-40/L-40e238a.wav"
+[  5, 38 ] = wave (0) : "./MITfull/elev-40/L-40e244a.wav"
+[  5, 39 ] = wave (0) : "./MITfull/elev-40/L-40e251a.wav"
+[  5, 40 ] = wave (0) : "./MITfull/elev-40/L-40e257a.wav"
+[  5, 41 ] = wave (0) : "./MITfull/elev-40/L-40e264a.wav"
+[  5, 42 ] = wave (0) : "./MITfull/elev-40/L-40e270a.wav"
+[  5, 43 ] = wave (0) : "./MITfull/elev-40/L-40e276a.wav"
+[  5, 44 ] = wave (0) : "./MITfull/elev-40/L-40e283a.wav"
+[  5, 45 ] = wave (0) : "./MITfull/elev-40/L-40e289a.wav"
+[  5, 46 ] = wave (0) : "./MITfull/elev-40/L-40e296a.wav"
+[  5, 47 ] = wave (0) : "./MITfull/elev-40/L-40e302a.wav"
+[  5, 48 ] = wave (0) : "./MITfull/elev-40/L-40e309a.wav"
+[  5, 49 ] = wave (0) : "./MITfull/elev-40/L-40e315a.wav"
+[  5, 50 ] = wave (0) : "./MITfull/elev-40/L-40e321a.wav"
+[  5, 51 ] = wave (0) : "./MITfull/elev-40/L-40e328a.wav"
+[  5, 52 ] = wave (0) : "./MITfull/elev-40/L-40e334a.wav"
+[  5, 53 ] = wave (0) : "./MITfull/elev-40/L-40e341a.wav"
+[  5, 54 ] = wave (0) : "./MITfull/elev-40/L-40e347a.wav"
+[  5, 55 ] = wave (0) : "./MITfull/elev-40/L-40e354a.wav"
+
+[  6,  0 ] = wave (0) : "./MITfull/elev-30/L-30e000a.wav"
+[  6,  1 ] = wave (0) : "./MITfull/elev-30/L-30e006a.wav"
+[  6,  2 ] = wave (0) : "./MITfull/elev-30/L-30e012a.wav"
+[  6,  3 ] = wave (0) : "./MITfull/elev-30/L-30e018a.wav"
+[  6,  4 ] = wave (0) : "./MITfull/elev-30/L-30e024a.wav"
+[  6,  5 ] = wave (0) : "./MITfull/elev-30/L-30e030a.wav"
+[  6,  6 ] = wave (0) : "./MITfull/elev-30/L-30e036a.wav"
+[  6,  7 ] = wave (0) : "./MITfull/elev-30/L-30e042a.wav"
+[  6,  8 ] = wave (0) : "./MITfull/elev-30/L-30e048a.wav"
+[  6,  9 ] = wave (0) : "./MITfull/elev-30/L-30e054a.wav"
+[  6, 10 ] = wave (0) : "./MITfull/elev-30/L-30e060a.wav"
+[  6, 11 ] = wave (0) : "./MITfull/elev-30/L-30e066a.wav"
+[  6, 12 ] = wave (0) : "./MITfull/elev-30/L-30e072a.wav"
+[  6, 13 ] = wave (0) : "./MITfull/elev-30/L-30e078a.wav"
+[  6, 14 ] = wave (0) : "./MITfull/elev-30/L-30e084a.wav"
+[  6, 15 ] = wave (0) : "./MITfull/elev-30/L-30e090a.wav"
+[  6, 16 ] = wave (0) : "./MITfull/elev-30/L-30e096a.wav"
+[  6, 17 ] = wave (0) : "./MITfull/elev-30/L-30e102a.wav"
+[  6, 18 ] = wave (0) : "./MITfull/elev-30/L-30e108a.wav"
+[  6, 19 ] = wave (0) : "./MITfull/elev-30/L-30e114a.wav"
+[  6, 20 ] = wave (0) : "./MITfull/elev-30/L-30e120a.wav"
+[  6, 21 ] = wave (0) : "./MITfull/elev-30/L-30e126a.wav"
+[  6, 22 ] = wave (0) : "./MITfull/elev-30/L-30e132a.wav"
+[  6, 23 ] = wave (0) : "./MITfull/elev-30/L-30e138a.wav"
+[  6, 24 ] = wave (0) : "./MITfull/elev-30/L-30e144a.wav"
+[  6, 25 ] = wave (0) : "./MITfull/elev-30/L-30e150a.wav"
+[  6, 26 ] = wave (0) : "./MITfull/elev-30/L-30e156a.wav"
+[  6, 27 ] = wave (0) : "./MITfull/elev-30/L-30e162a.wav"
+[  6, 28 ] = wave (0) : "./MITfull/elev-30/L-30e168a.wav"
+[  6, 29 ] = wave (0) : "./MITfull/elev-30/L-30e174a.wav"
+[  6, 30 ] = wave (0) : "./MITfull/elev-30/L-30e180a.wav"
+[  6, 31 ] = wave (0) : "./MITfull/elev-30/L-30e186a.wav"
+[  6, 32 ] = wave (0) : "./MITfull/elev-30/L-30e192a.wav"
+[  6, 33 ] = wave (0) : "./MITfull/elev-30/L-30e198a.wav"
+[  6, 34 ] = wave (0) : "./MITfull/elev-30/L-30e204a.wav"
+[  6, 35 ] = wave (0) : "./MITfull/elev-30/L-30e210a.wav"
+[  6, 36 ] = wave (0) : "./MITfull/elev-30/L-30e216a.wav"
+[  6, 37 ] = wave (0) : "./MITfull/elev-30/L-30e222a.wav"
+[  6, 38 ] = wave (0) : "./MITfull/elev-30/L-30e228a.wav"
+[  6, 39 ] = wave (0) : "./MITfull/elev-30/L-30e234a.wav"
+[  6, 40 ] = wave (0) : "./MITfull/elev-30/L-30e240a.wav"
+[  6, 41 ] = wave (0) : "./MITfull/elev-30/L-30e246a.wav"
+[  6, 42 ] = wave (0) : "./MITfull/elev-30/L-30e252a.wav"
+[  6, 43 ] = wave (0) : "./MITfull/elev-30/L-30e258a.wav"
+[  6, 44 ] = wave (0) : "./MITfull/elev-30/L-30e264a.wav"
+[  6, 45 ] = wave (0) : "./MITfull/elev-30/L-30e270a.wav"
+[  6, 46 ] = wave (0) : "./MITfull/elev-30/L-30e276a.wav"
+[  6, 47 ] = wave (0) : "./MITfull/elev-30/L-30e282a.wav"
+[  6, 48 ] = wave (0) : "./MITfull/elev-30/L-30e288a.wav"
+[  6, 49 ] = wave (0) : "./MITfull/elev-30/L-30e294a.wav"
+[  6, 50 ] = wave (0) : "./MITfull/elev-30/L-30e300a.wav"
+[  6, 51 ] = wave (0) : "./MITfull/elev-30/L-30e306a.wav"
+[  6, 52 ] = wave (0) : "./MITfull/elev-30/L-30e312a.wav"
+[  6, 53 ] = wave (0) : "./MITfull/elev-30/L-30e318a.wav"
+[  6, 54 ] = wave (0) : "./MITfull/elev-30/L-30e324a.wav"
+[  6, 55 ] = wave (0) : "./MITfull/elev-30/L-30e330a.wav"
+[  6, 56 ] = wave (0) : "./MITfull/elev-30/L-30e336a.wav"
+[  6, 57 ] = wave (0) : "./MITfull/elev-30/L-30e342a.wav"
+[  6, 58 ] = wave (0) : "./MITfull/elev-30/L-30e348a.wav"
+[  6, 59 ] = wave (0) : "./MITfull/elev-30/L-30e354a.wav"
+
+[  7,  0 ] = wave (0) : "./MITfull/elev-20/L-20e000a.wav"
+[  7,  1 ] = wave (0) : "./MITfull/elev-20/L-20e005a.wav"
+[  7,  2 ] = wave (0) : "./MITfull/elev-20/L-20e010a.wav"
+[  7,  3 ] = wave (0) : "./MITfull/elev-20/L-20e015a.wav"
+[  7,  4 ] = wave (0) : "./MITfull/elev-20/L-20e020a.wav"
+[  7,  5 ] = wave (0) : "./MITfull/elev-20/L-20e025a.wav"
+[  7,  6 ] = wave (0) : "./MITfull/elev-20/L-20e030a.wav"
+[  7,  7 ] = wave (0) : "./MITfull/elev-20/L-20e035a.wav"
+[  7,  8 ] = wave (0) : "./MITfull/elev-20/L-20e040a.wav"
+[  7,  9 ] = wave (0) : "./MITfull/elev-20/L-20e045a.wav"
+[  7, 10 ] = wave (0) : "./MITfull/elev-20/L-20e050a.wav"
+[  7, 11 ] = wave (0) : "./MITfull/elev-20/L-20e055a.wav"
+[  7, 12 ] = wave (0) : "./MITfull/elev-20/L-20e060a.wav"
+[  7, 13 ] = wave (0) : "./MITfull/elev-20/L-20e065a.wav"
+[  7, 14 ] = wave (0) : "./MITfull/elev-20/L-20e070a.wav"
+[  7, 15 ] = wave (0) : "./MITfull/elev-20/L-20e075a.wav"
+[  7, 16 ] = wave (0) : "./MITfull/elev-20/L-20e080a.wav"
+[  7, 17 ] = wave (0) : "./MITfull/elev-20/L-20e085a.wav"
+[  7, 18 ] = wave (0) : "./MITfull/elev-20/L-20e090a.wav"
+[  7, 19 ] = wave (0) : "./MITfull/elev-20/L-20e095a.wav"
+[  7, 20 ] = wave (0) : "./MITfull/elev-20/L-20e100a.wav"
+[  7, 21 ] = wave (0) : "./MITfull/elev-20/L-20e105a.wav"
+[  7, 22 ] = wave (0) : "./MITfull/elev-20/L-20e110a.wav"
+[  7, 23 ] = wave (0) : "./MITfull/elev-20/L-20e115a.wav"
+[  7, 24 ] = wave (0) : "./MITfull/elev-20/L-20e120a.wav"
+[  7, 25 ] = wave (0) : "./MITfull/elev-20/L-20e125a.wav"
+[  7, 26 ] = wave (0) : "./MITfull/elev-20/L-20e130a.wav"
+[  7, 27 ] = wave (0) : "./MITfull/elev-20/L-20e135a.wav"
+[  7, 28 ] = wave (0) : "./MITfull/elev-20/L-20e140a.wav"
+[  7, 29 ] = wave (0) : "./MITfull/elev-20/L-20e145a.wav"
+[  7, 30 ] = wave (0) : "./MITfull/elev-20/L-20e150a.wav"
+[  7, 31 ] = wave (0) : "./MITfull/elev-20/L-20e155a.wav"
+[  7, 32 ] = wave (0) : "./MITfull/elev-20/L-20e160a.wav"
+[  7, 33 ] = wave (0) : "./MITfull/elev-20/L-20e165a.wav"
+[  7, 34 ] = wave (0) : "./MITfull/elev-20/L-20e170a.wav"
+[  7, 35 ] = wave (0) : "./MITfull/elev-20/L-20e175a.wav"
+[  7, 36 ] = wave (0) : "./MITfull/elev-20/L-20e180a.wav"
+[  7, 37 ] = wave (0) : "./MITfull/elev-20/L-20e185a.wav"
+[  7, 38 ] = wave (0) : "./MITfull/elev-20/L-20e190a.wav"
+[  7, 39 ] = wave (0) : "./MITfull/elev-20/L-20e195a.wav"
+[  7, 40 ] = wave (0) : "./MITfull/elev-20/L-20e200a.wav"
+[  7, 41 ] = wave (0) : "./MITfull/elev-20/L-20e205a.wav"
+[  7, 42 ] = wave (0) : "./MITfull/elev-20/L-20e210a.wav"
+[  7, 43 ] = wave (0) : "./MITfull/elev-20/L-20e215a.wav"
+[  7, 44 ] = wave (0) : "./MITfull/elev-20/L-20e220a.wav"
+[  7, 45 ] = wave (0) : "./MITfull/elev-20/L-20e225a.wav"
+[  7, 46 ] = wave (0) : "./MITfull/elev-20/L-20e230a.wav"
+[  7, 47 ] = wave (0) : "./MITfull/elev-20/L-20e235a.wav"
+[  7, 48 ] = wave (0) : "./MITfull/elev-20/L-20e240a.wav"
+[  7, 49 ] = wave (0) : "./MITfull/elev-20/L-20e245a.wav"
+[  7, 50 ] = wave (0) : "./MITfull/elev-20/L-20e250a.wav"
+[  7, 51 ] = wave (0) : "./MITfull/elev-20/L-20e255a.wav"
+[  7, 52 ] = wave (0) : "./MITfull/elev-20/L-20e260a.wav"
+[  7, 53 ] = wave (0) : "./MITfull/elev-20/L-20e265a.wav"
+[  7, 54 ] = wave (0) : "./MITfull/elev-20/L-20e270a.wav"
+[  7, 55 ] = wave (0) : "./MITfull/elev-20/L-20e275a.wav"
+[  7, 56 ] = wave (0) : "./MITfull/elev-20/L-20e280a.wav"
+[  7, 57 ] = wave (0) : "./MITfull/elev-20/L-20e285a.wav"
+[  7, 58 ] = wave (0) : "./MITfull/elev-20/L-20e290a.wav"
+[  7, 59 ] = wave (0) : "./MITfull/elev-20/L-20e295a.wav"
+[  7, 60 ] = wave (0) : "./MITfull/elev-20/L-20e300a.wav"
+[  7, 61 ] = wave (0) : "./MITfull/elev-20/L-20e305a.wav"
+[  7, 62 ] = wave (0) : "./MITfull/elev-20/L-20e310a.wav"
+[  7, 63 ] = wave (0) : "./MITfull/elev-20/L-20e315a.wav"
+[  7, 64 ] = wave (0) : "./MITfull/elev-20/L-20e320a.wav"
+[  7, 65 ] = wave (0) : "./MITfull/elev-20/L-20e325a.wav"
+[  7, 66 ] = wave (0) : "./MITfull/elev-20/L-20e330a.wav"
+[  7, 67 ] = wave (0) : "./MITfull/elev-20/L-20e335a.wav"
+[  7, 68 ] = wave (0) : "./MITfull/elev-20/L-20e340a.wav"
+[  7, 69 ] = wave (0) : "./MITfull/elev-20/L-20e345a.wav"
+[  7, 70 ] = wave (0) : "./MITfull/elev-20/L-20e350a.wav"
+[  7, 71 ] = wave (0) : "./MITfull/elev-20/L-20e355a.wav"
+
+[  8,  0 ] = wave (0) : "./MITfull/elev-10/L-10e000a.wav"
+[  8,  1 ] = wave (0) : "./MITfull/elev-10/L-10e005a.wav"
+[  8,  2 ] = wave (0) : "./MITfull/elev-10/L-10e010a.wav"
+[  8,  3 ] = wave (0) : "./MITfull/elev-10/L-10e015a.wav"
+[  8,  4 ] = wave (0) : "./MITfull/elev-10/L-10e020a.wav"
+[  8,  5 ] = wave (0) : "./MITfull/elev-10/L-10e025a.wav"
+[  8,  6 ] = wave (0) : "./MITfull/elev-10/L-10e030a.wav"
+[  8,  7 ] = wave (0) : "./MITfull/elev-10/L-10e035a.wav"
+[  8,  8 ] = wave (0) : "./MITfull/elev-10/L-10e040a.wav"
+[  8,  9 ] = wave (0) : "./MITfull/elev-10/L-10e045a.wav"
+[  8, 10 ] = wave (0) : "./MITfull/elev-10/L-10e050a.wav"
+[  8, 11 ] = wave (0) : "./MITfull/elev-10/L-10e055a.wav"
+[  8, 12 ] = wave (0) : "./MITfull/elev-10/L-10e060a.wav"
+[  8, 13 ] = wave (0) : "./MITfull/elev-10/L-10e065a.wav"
+[  8, 14 ] = wave (0) : "./MITfull/elev-10/L-10e070a.wav"
+[  8, 15 ] = wave (0) : "./MITfull/elev-10/L-10e075a.wav"
+[  8, 16 ] = wave (0) : "./MITfull/elev-10/L-10e080a.wav"
+[  8, 17 ] = wave (0) : "./MITfull/elev-10/L-10e085a.wav"
+[  8, 18 ] = wave (0) : "./MITfull/elev-10/L-10e090a.wav"
+[  8, 19 ] = wave (0) : "./MITfull/elev-10/L-10e095a.wav"
+[  8, 20 ] = wave (0) : "./MITfull/elev-10/L-10e100a.wav"
+[  8, 21 ] = wave (0) : "./MITfull/elev-10/L-10e105a.wav"
+[  8, 22 ] = wave (0) : "./MITfull/elev-10/L-10e110a.wav"
+[  8, 23 ] = wave (0) : "./MITfull/elev-10/L-10e115a.wav"
+[  8, 24 ] = wave (0) : "./MITfull/elev-10/L-10e120a.wav"
+[  8, 25 ] = wave (0) : "./MITfull/elev-10/L-10e125a.wav"
+[  8, 26 ] = wave (0) : "./MITfull/elev-10/L-10e130a.wav"
+[  8, 27 ] = wave (0) : "./MITfull/elev-10/L-10e135a.wav"
+[  8, 28 ] = wave (0) : "./MITfull/elev-10/L-10e140a.wav"
+[  8, 29 ] = wave (0) : "./MITfull/elev-10/L-10e145a.wav"
+[  8, 30 ] = wave (0) : "./MITfull/elev-10/L-10e150a.wav"
+[  8, 31 ] = wave (0) : "./MITfull/elev-10/L-10e155a.wav"
+[  8, 32 ] = wave (0) : "./MITfull/elev-10/L-10e160a.wav"
+[  8, 33 ] = wave (0) : "./MITfull/elev-10/L-10e165a.wav"
+[  8, 34 ] = wave (0) : "./MITfull/elev-10/L-10e170a.wav"
+[  8, 35 ] = wave (0) : "./MITfull/elev-10/L-10e175a.wav"
+[  8, 36 ] = wave (0) : "./MITfull/elev-10/L-10e180a.wav"
+[  8, 37 ] = wave (0) : "./MITfull/elev-10/L-10e185a.wav"
+[  8, 38 ] = wave (0) : "./MITfull/elev-10/L-10e190a.wav"
+[  8, 39 ] = wave (0) : "./MITfull/elev-10/L-10e195a.wav"
+[  8, 40 ] = wave (0) : "./MITfull/elev-10/L-10e200a.wav"
+[  8, 41 ] = wave (0) : "./MITfull/elev-10/L-10e205a.wav"
+[  8, 42 ] = wave (0) : "./MITfull/elev-10/L-10e210a.wav"
+[  8, 43 ] = wave (0) : "./MITfull/elev-10/L-10e215a.wav"
+[  8, 44 ] = wave (0) : "./MITfull/elev-10/L-10e220a.wav"
+[  8, 45 ] = wave (0) : "./MITfull/elev-10/L-10e225a.wav"
+[  8, 46 ] = wave (0) : "./MITfull/elev-10/L-10e230a.wav"
+[  8, 47 ] = wave (0) : "./MITfull/elev-10/L-10e235a.wav"
+[  8, 48 ] = wave (0) : "./MITfull/elev-10/L-10e240a.wav"
+[  8, 49 ] = wave (0) : "./MITfull/elev-10/L-10e245a.wav"
+[  8, 50 ] = wave (0) : "./MITfull/elev-10/L-10e250a.wav"
+[  8, 51 ] = wave (0) : "./MITfull/elev-10/L-10e255a.wav"
+[  8, 52 ] = wave (0) : "./MITfull/elev-10/L-10e260a.wav"
+[  8, 53 ] = wave (0) : "./MITfull/elev-10/L-10e265a.wav"
+[  8, 54 ] = wave (0) : "./MITfull/elev-10/L-10e270a.wav"
+[  8, 55 ] = wave (0) : "./MITfull/elev-10/L-10e275a.wav"
+[  8, 56 ] = wave (0) : "./MITfull/elev-10/L-10e280a.wav"
+[  8, 57 ] = wave (0) : "./MITfull/elev-10/L-10e285a.wav"
+[  8, 58 ] = wave (0) : "./MITfull/elev-10/L-10e290a.wav"
+[  8, 59 ] = wave (0) : "./MITfull/elev-10/L-10e295a.wav"
+[  8, 60 ] = wave (0) : "./MITfull/elev-10/L-10e300a.wav"
+[  8, 61 ] = wave (0) : "./MITfull/elev-10/L-10e305a.wav"
+[  8, 62 ] = wave (0) : "./MITfull/elev-10/L-10e310a.wav"
+[  8, 63 ] = wave (0) : "./MITfull/elev-10/L-10e315a.wav"
+[  8, 64 ] = wave (0) : "./MITfull/elev-10/L-10e320a.wav"
+[  8, 65 ] = wave (0) : "./MITfull/elev-10/L-10e325a.wav"
+[  8, 66 ] = wave (0) : "./MITfull/elev-10/L-10e330a.wav"
+[  8, 67 ] = wave (0) : "./MITfull/elev-10/L-10e335a.wav"
+[  8, 68 ] = wave (0) : "./MITfull/elev-10/L-10e340a.wav"
+[  8, 69 ] = wave (0) : "./MITfull/elev-10/L-10e345a.wav"
+[  8, 70 ] = wave (0) : "./MITfull/elev-10/L-10e350a.wav"
+[  8, 71 ] = wave (0) : "./MITfull/elev-10/L-10e355a.wav"
+
+[  9,  0 ] = wave (0) : "./MITfull/elev0/L0e000a.wav"
+[  9,  1 ] = wave (0) : "./MITfull/elev0/L0e005a.wav"
+[  9,  2 ] = wave (0) : "./MITfull/elev0/L0e010a.wav"
+[  9,  3 ] = wave (0) : "./MITfull/elev0/L0e015a.wav"
+[  9,  4 ] = wave (0) : "./MITfull/elev0/L0e020a.wav"
+[  9,  5 ] = wave (0) : "./MITfull/elev0/L0e025a.wav"
+[  9,  6 ] = wave (0) : "./MITfull/elev0/L0e030a.wav"
+[  9,  7 ] = wave (0) : "./MITfull/elev0/L0e035a.wav"
+[  9,  8 ] = wave (0) : "./MITfull/elev0/L0e040a.wav"
+[  9,  9 ] = wave (0) : "./MITfull/elev0/L0e045a.wav"
+[  9, 10 ] = wave (0) : "./MITfull/elev0/L0e050a.wav"
+[  9, 11 ] = wave (0) : "./MITfull/elev0/L0e055a.wav"
+[  9, 12 ] = wave (0) : "./MITfull/elev0/L0e060a.wav"
+[  9, 13 ] = wave (0) : "./MITfull/elev0/L0e065a.wav"
+[  9, 14 ] = wave (0) : "./MITfull/elev0/L0e070a.wav"
+[  9, 15 ] = wave (0) : "./MITfull/elev0/L0e075a.wav"
+[  9, 16 ] = wave (0) : "./MITfull/elev0/L0e080a.wav"
+[  9, 17 ] = wave (0) : "./MITfull/elev0/L0e085a.wav"
+[  9, 18 ] = wave (0) : "./MITfull/elev0/L0e090a.wav"
+[  9, 19 ] = wave (0) : "./MITfull/elev0/L0e095a.wav"
+[  9, 20 ] = wave (0) : "./MITfull/elev0/L0e100a.wav"
+[  9, 21 ] = wave (0) : "./MITfull/elev0/L0e105a.wav"
+[  9, 22 ] = wave (0) : "./MITfull/elev0/L0e110a.wav"
+[  9, 23 ] = wave (0) : "./MITfull/elev0/L0e115a.wav"
+[  9, 24 ] = wave (0) : "./MITfull/elev0/L0e120a.wav"
+[  9, 25 ] = wave (0) : "./MITfull/elev0/L0e125a.wav"
+[  9, 26 ] = wave (0) : "./MITfull/elev0/L0e130a.wav"
+[  9, 27 ] = wave (0) : "./MITfull/elev0/L0e135a.wav"
+[  9, 28 ] = wave (0) : "./MITfull/elev0/L0e140a.wav"
+[  9, 29 ] = wave (0) : "./MITfull/elev0/L0e145a.wav"
+[  9, 30 ] = wave (0) : "./MITfull/elev0/L0e150a.wav"
+[  9, 31 ] = wave (0) : "./MITfull/elev0/L0e155a.wav"
+[  9, 32 ] = wave (0) : "./MITfull/elev0/L0e160a.wav"
+[  9, 33 ] = wave (0) : "./MITfull/elev0/L0e165a.wav"
+[  9, 34 ] = wave (0) : "./MITfull/elev0/L0e170a.wav"
+[  9, 35 ] = wave (0) : "./MITfull/elev0/L0e175a.wav"
+[  9, 36 ] = wave (0) : "./MITfull/elev0/L0e180a.wav"
+[  9, 37 ] = wave (0) : "./MITfull/elev0/L0e185a.wav"
+[  9, 38 ] = wave (0) : "./MITfull/elev0/L0e190a.wav"
+[  9, 39 ] = wave (0) : "./MITfull/elev0/L0e195a.wav"
+[  9, 40 ] = wave (0) : "./MITfull/elev0/L0e200a.wav"
+[  9, 41 ] = wave (0) : "./MITfull/elev0/L0e205a.wav"
+[  9, 42 ] = wave (0) : "./MITfull/elev0/L0e210a.wav"
+[  9, 43 ] = wave (0) : "./MITfull/elev0/L0e215a.wav"
+[  9, 44 ] = wave (0) : "./MITfull/elev0/L0e220a.wav"
+[  9, 45 ] = wave (0) : "./MITfull/elev0/L0e225a.wav"
+[  9, 46 ] = wave (0) : "./MITfull/elev0/L0e230a.wav"
+[  9, 47 ] = wave (0) : "./MITfull/elev0/L0e235a.wav"
+[  9, 48 ] = wave (0) : "./MITfull/elev0/L0e240a.wav"
+[  9, 49 ] = wave (0) : "./MITfull/elev0/L0e245a.wav"
+[  9, 50 ] = wave (0) : "./MITfull/elev0/L0e250a.wav"
+[  9, 51 ] = wave (0) : "./MITfull/elev0/L0e255a.wav"
+[  9, 52 ] = wave (0) : "./MITfull/elev0/L0e260a.wav"
+[  9, 53 ] = wave (0) : "./MITfull/elev0/L0e265a.wav"
+[  9, 54 ] = wave (0) : "./MITfull/elev0/L0e270a.wav"
+[  9, 55 ] = wave (0) : "./MITfull/elev0/L0e275a.wav"
+[  9, 56 ] = wave (0) : "./MITfull/elev0/L0e280a.wav"
+[  9, 57 ] = wave (0) : "./MITfull/elev0/L0e285a.wav"
+[  9, 58 ] = wave (0) : "./MITfull/elev0/L0e290a.wav"
+[  9, 59 ] = wave (0) : "./MITfull/elev0/L0e295a.wav"
+[  9, 60 ] = wave (0) : "./MITfull/elev0/L0e300a.wav"
+[  9, 61 ] = wave (0) : "./MITfull/elev0/L0e305a.wav"
+[  9, 62 ] = wave (0) : "./MITfull/elev0/L0e310a.wav"
+[  9, 63 ] = wave (0) : "./MITfull/elev0/L0e315a.wav"
+[  9, 64 ] = wave (0) : "./MITfull/elev0/L0e320a.wav"
+[  9, 65 ] = wave (0) : "./MITfull/elev0/L0e325a.wav"
+[  9, 66 ] = wave (0) : "./MITfull/elev0/L0e330a.wav"
+[  9, 67 ] = wave (0) : "./MITfull/elev0/L0e335a.wav"
+[  9, 68 ] = wave (0) : "./MITfull/elev0/L0e340a.wav"
+[  9, 69 ] = wave (0) : "./MITfull/elev0/L0e345a.wav"
+[  9, 70 ] = wave (0) : "./MITfull/elev0/L0e350a.wav"
+[  9, 71 ] = wave (0) : "./MITfull/elev0/L0e355a.wav"
+
+[ 10,  0 ] = wave (0) : "./MITfull/elev10/L10e000a.wav"
+[ 10,  1 ] = wave (0) : "./MITfull/elev10/L10e005a.wav"
+[ 10,  2 ] = wave (0) : "./MITfull/elev10/L10e010a.wav"
+[ 10,  3 ] = wave (0) : "./MITfull/elev10/L10e015a.wav"
+[ 10,  4 ] = wave (0) : "./MITfull/elev10/L10e020a.wav"
+[ 10,  5 ] = wave (0) : "./MITfull/elev10/L10e025a.wav"
+[ 10,  6 ] = wave (0) : "./MITfull/elev10/L10e030a.wav"
+[ 10,  7 ] = wave (0) : "./MITfull/elev10/L10e035a.wav"
+[ 10,  8 ] = wave (0) : "./MITfull/elev10/L10e040a.wav"
+[ 10,  9 ] = wave (0) : "./MITfull/elev10/L10e045a.wav"
+[ 10, 10 ] = wave (0) : "./MITfull/elev10/L10e050a.wav"
+[ 10, 11 ] = wave (0) : "./MITfull/elev10/L10e055a.wav"
+[ 10, 12 ] = wave (0) : "./MITfull/elev10/L10e060a.wav"
+[ 10, 13 ] = wave (0) : "./MITfull/elev10/L10e065a.wav"
+[ 10, 14 ] = wave (0) : "./MITfull/elev10/L10e070a.wav"
+[ 10, 15 ] = wave (0) : "./MITfull/elev10/L10e075a.wav"
+[ 10, 16 ] = wave (0) : "./MITfull/elev10/L10e080a.wav"
+[ 10, 17 ] = wave (0) : "./MITfull/elev10/L10e085a.wav"
+[ 10, 18 ] = wave (0) : "./MITfull/elev10/L10e090a.wav"
+[ 10, 19 ] = wave (0) : "./MITfull/elev10/L10e095a.wav"
+[ 10, 20 ] = wave (0) : "./MITfull/elev10/L10e100a.wav"
+[ 10, 21 ] = wave (0) : "./MITfull/elev10/L10e105a.wav"
+[ 10, 22 ] = wave (0) : "./MITfull/elev10/L10e110a.wav"
+[ 10, 23 ] = wave (0) : "./MITfull/elev10/L10e115a.wav"
+[ 10, 24 ] = wave (0) : "./MITfull/elev10/L10e120a.wav"
+[ 10, 25 ] = wave (0) : "./MITfull/elev10/L10e125a.wav"
+[ 10, 26 ] = wave (0) : "./MITfull/elev10/L10e130a.wav"
+[ 10, 27 ] = wave (0) : "./MITfull/elev10/L10e135a.wav"
+[ 10, 28 ] = wave (0) : "./MITfull/elev10/L10e140a.wav"
+[ 10, 29 ] = wave (0) : "./MITfull/elev10/L10e145a.wav"
+[ 10, 30 ] = wave (0) : "./MITfull/elev10/L10e150a.wav"
+[ 10, 31 ] = wave (0) : "./MITfull/elev10/L10e155a.wav"
+[ 10, 32 ] = wave (0) : "./MITfull/elev10/L10e160a.wav"
+[ 10, 33 ] = wave (0) : "./MITfull/elev10/L10e165a.wav"
+[ 10, 34 ] = wave (0) : "./MITfull/elev10/L10e170a.wav"
+[ 10, 35 ] = wave (0) : "./MITfull/elev10/L10e175a.wav"
+[ 10, 36 ] = wave (0) : "./MITfull/elev10/L10e180a.wav"
+[ 10, 37 ] = wave (0) : "./MITfull/elev10/L10e185a.wav"
+[ 10, 38 ] = wave (0) : "./MITfull/elev10/L10e190a.wav"
+[ 10, 39 ] = wave (0) : "./MITfull/elev10/L10e195a.wav"
+[ 10, 40 ] = wave (0) : "./MITfull/elev10/L10e200a.wav"
+[ 10, 41 ] = wave (0) : "./MITfull/elev10/L10e205a.wav"
+[ 10, 42 ] = wave (0) : "./MITfull/elev10/L10e210a.wav"
+[ 10, 43 ] = wave (0) : "./MITfull/elev10/L10e215a.wav"
+[ 10, 44 ] = wave (0) : "./MITfull/elev10/L10e220a.wav"
+[ 10, 45 ] = wave (0) : "./MITfull/elev10/L10e225a.wav"
+[ 10, 46 ] = wave (0) : "./MITfull/elev10/L10e230a.wav"
+[ 10, 47 ] = wave (0) : "./MITfull/elev10/L10e235a.wav"
+[ 10, 48 ] = wave (0) : "./MITfull/elev10/L10e240a.wav"
+[ 10, 49 ] = wave (0) : "./MITfull/elev10/L10e245a.wav"
+[ 10, 50 ] = wave (0) : "./MITfull/elev10/L10e250a.wav"
+[ 10, 51 ] = wave (0) : "./MITfull/elev10/L10e255a.wav"
+[ 10, 52 ] = wave (0) : "./MITfull/elev10/L10e260a.wav"
+[ 10, 53 ] = wave (0) : "./MITfull/elev10/L10e265a.wav"
+[ 10, 54 ] = wave (0) : "./MITfull/elev10/L10e270a.wav"
+[ 10, 55 ] = wave (0) : "./MITfull/elev10/L10e275a.wav"
+[ 10, 56 ] = wave (0) : "./MITfull/elev10/L10e280a.wav"
+[ 10, 57 ] = wave (0) : "./MITfull/elev10/L10e285a.wav"
+[ 10, 58 ] = wave (0) : "./MITfull/elev10/L10e290a.wav"
+[ 10, 59 ] = wave (0) : "./MITfull/elev10/L10e295a.wav"
+[ 10, 60 ] = wave (0) : "./MITfull/elev10/L10e300a.wav"
+[ 10, 61 ] = wave (0) : "./MITfull/elev10/L10e305a.wav"
+[ 10, 62 ] = wave (0) : "./MITfull/elev10/L10e310a.wav"
+[ 10, 63 ] = wave (0) : "./MITfull/elev10/L10e315a.wav"
+[ 10, 64 ] = wave (0) : "./MITfull/elev10/L10e320a.wav"
+[ 10, 65 ] = wave (0) : "./MITfull/elev10/L10e325a.wav"
+[ 10, 66 ] = wave (0) : "./MITfull/elev10/L10e330a.wav"
+[ 10, 67 ] = wave (0) : "./MITfull/elev10/L10e335a.wav"
+[ 10, 68 ] = wave (0) : "./MITfull/elev10/L10e340a.wav"
+[ 10, 69 ] = wave (0) : "./MITfull/elev10/L10e345a.wav"
+[ 10, 70 ] = wave (0) : "./MITfull/elev10/L10e350a.wav"
+[ 10, 71 ] = wave (0) : "./MITfull/elev10/L10e355a.wav"
+
+[ 11,  0 ] = wave (0) : "./MITfull/elev20/L20e000a.wav"
+[ 11,  1 ] = wave (0) : "./MITfull/elev20/L20e005a.wav"
+[ 11,  2 ] = wave (0) : "./MITfull/elev20/L20e010a.wav"
+[ 11,  3 ] = wave (0) : "./MITfull/elev20/L20e015a.wav"
+[ 11,  4 ] = wave (0) : "./MITfull/elev20/L20e020a.wav"
+[ 11,  5 ] = wave (0) : "./MITfull/elev20/L20e025a.wav"
+[ 11,  6 ] = wave (0) : "./MITfull/elev20/L20e030a.wav"
+[ 11,  7 ] = wave (0) : "./MITfull/elev20/L20e035a.wav"
+[ 11,  8 ] = wave (0) : "./MITfull/elev20/L20e040a.wav"
+[ 11,  9 ] = wave (0) : "./MITfull/elev20/L20e045a.wav"
+[ 11, 10 ] = wave (0) : "./MITfull/elev20/L20e050a.wav"
+[ 11, 11 ] = wave (0) : "./MITfull/elev20/L20e055a.wav"
+[ 11, 12 ] = wave (0) : "./MITfull/elev20/L20e060a.wav"
+[ 11, 13 ] = wave (0) : "./MITfull/elev20/L20e065a.wav"
+[ 11, 14 ] = wave (0) : "./MITfull/elev20/L20e070a.wav"
+[ 11, 15 ] = wave (0) : "./MITfull/elev20/L20e075a.wav"
+[ 11, 16 ] = wave (0) : "./MITfull/elev20/L20e080a.wav"
+[ 11, 17 ] = wave (0) : "./MITfull/elev20/L20e085a.wav"
+[ 11, 18 ] = wave (0) : "./MITfull/elev20/L20e090a.wav"
+[ 11, 19 ] = wave (0) : "./MITfull/elev20/L20e095a.wav"
+[ 11, 20 ] = wave (0) : "./MITfull/elev20/L20e100a.wav"
+[ 11, 21 ] = wave (0) : "./MITfull/elev20/L20e105a.wav"
+[ 11, 22 ] = wave (0) : "./MITfull/elev20/L20e110a.wav"
+[ 11, 23 ] = wave (0) : "./MITfull/elev20/L20e115a.wav"
+[ 11, 24 ] = wave (0) : "./MITfull/elev20/L20e120a.wav"
+[ 11, 25 ] = wave (0) : "./MITfull/elev20/L20e125a.wav"
+[ 11, 26 ] = wave (0) : "./MITfull/elev20/L20e130a.wav"
+[ 11, 27 ] = wave (0) : "./MITfull/elev20/L20e135a.wav"
+[ 11, 28 ] = wave (0) : "./MITfull/elev20/L20e140a.wav"
+[ 11, 29 ] = wave (0) : "./MITfull/elev20/L20e145a.wav"
+[ 11, 30 ] = wave (0) : "./MITfull/elev20/L20e150a.wav"
+[ 11, 31 ] = wave (0) : "./MITfull/elev20/L20e155a.wav"
+[ 11, 32 ] = wave (0) : "./MITfull/elev20/L20e160a.wav"
+[ 11, 33 ] = wave (0) : "./MITfull/elev20/L20e165a.wav"
+[ 11, 34 ] = wave (0) : "./MITfull/elev20/L20e170a.wav"
+[ 11, 35 ] = wave (0) : "./MITfull/elev20/L20e175a.wav"
+[ 11, 36 ] = wave (0) : "./MITfull/elev20/L20e180a.wav"
+[ 11, 37 ] = wave (0) : "./MITfull/elev20/L20e185a.wav"
+[ 11, 38 ] = wave (0) : "./MITfull/elev20/L20e190a.wav"
+[ 11, 39 ] = wave (0) : "./MITfull/elev20/L20e195a.wav"
+[ 11, 40 ] = wave (0) : "./MITfull/elev20/L20e200a.wav"
+[ 11, 41 ] = wave (0) : "./MITfull/elev20/L20e205a.wav"
+[ 11, 42 ] = wave (0) : "./MITfull/elev20/L20e210a.wav"
+[ 11, 43 ] = wave (0) : "./MITfull/elev20/L20e215a.wav"
+[ 11, 44 ] = wave (0) : "./MITfull/elev20/L20e220a.wav"
+[ 11, 45 ] = wave (0) : "./MITfull/elev20/L20e225a.wav"
+[ 11, 46 ] = wave (0) : "./MITfull/elev20/L20e230a.wav"
+[ 11, 47 ] = wave (0) : "./MITfull/elev20/L20e235a.wav"
+[ 11, 48 ] = wave (0) : "./MITfull/elev20/L20e240a.wav"
+[ 11, 49 ] = wave (0) : "./MITfull/elev20/L20e245a.wav"
+[ 11, 50 ] = wave (0) : "./MITfull/elev20/L20e250a.wav"
+[ 11, 51 ] = wave (0) : "./MITfull/elev20/L20e255a.wav"
+[ 11, 52 ] = wave (0) : "./MITfull/elev20/L20e260a.wav"
+[ 11, 53 ] = wave (0) : "./MITfull/elev20/L20e265a.wav"
+[ 11, 54 ] = wave (0) : "./MITfull/elev20/L20e270a.wav"
+[ 11, 55 ] = wave (0) : "./MITfull/elev20/L20e275a.wav"
+[ 11, 56 ] = wave (0) : "./MITfull/elev20/L20e280a.wav"
+[ 11, 57 ] = wave (0) : "./MITfull/elev20/L20e285a.wav"
+[ 11, 58 ] = wave (0) : "./MITfull/elev20/L20e290a.wav"
+[ 11, 59 ] = wave (0) : "./MITfull/elev20/L20e295a.wav"
+[ 11, 60 ] = wave (0) : "./MITfull/elev20/L20e300a.wav"
+[ 11, 61 ] = wave (0) : "./MITfull/elev20/L20e305a.wav"
+[ 11, 62 ] = wave (0) : "./MITfull/elev20/L20e310a.wav"
+[ 11, 63 ] = wave (0) : "./MITfull/elev20/L20e315a.wav"
+[ 11, 64 ] = wave (0) : "./MITfull/elev20/L20e320a.wav"
+[ 11, 65 ] = wave (0) : "./MITfull/elev20/L20e325a.wav"
+[ 11, 66 ] = wave (0) : "./MITfull/elev20/L20e330a.wav"
+[ 11, 67 ] = wave (0) : "./MITfull/elev20/L20e335a.wav"
+[ 11, 68 ] = wave (0) : "./MITfull/elev20/L20e340a.wav"
+[ 11, 69 ] = wave (0) : "./MITfull/elev20/L20e345a.wav"
+[ 11, 70 ] = wave (0) : "./MITfull/elev20/L20e350a.wav"
+[ 11, 71 ] = wave (0) : "./MITfull/elev20/L20e355a.wav"
+
+[ 12,  0 ] = wave (0) : "./MITfull/elev30/L30e000a.wav"
+[ 12,  1 ] = wave (0) : "./MITfull/elev30/L30e006a.wav"
+[ 12,  2 ] = wave (0) : "./MITfull/elev30/L30e012a.wav"
+[ 12,  3 ] = wave (0) : "./MITfull/elev30/L30e018a.wav"
+[ 12,  4 ] = wave (0) : "./MITfull/elev30/L30e024a.wav"
+[ 12,  5 ] = wave (0) : "./MITfull/elev30/L30e030a.wav"
+[ 12,  6 ] = wave (0) : "./MITfull/elev30/L30e036a.wav"
+[ 12,  7 ] = wave (0) : "./MITfull/elev30/L30e042a.wav"
+[ 12,  8 ] = wave (0) : "./MITfull/elev30/L30e048a.wav"
+[ 12,  9 ] = wave (0) : "./MITfull/elev30/L30e054a.wav"
+[ 12, 10 ] = wave (0) : "./MITfull/elev30/L30e060a.wav"
+[ 12, 11 ] = wave (0) : "./MITfull/elev30/L30e066a.wav"
+[ 12, 12 ] = wave (0) : "./MITfull/elev30/L30e072a.wav"
+[ 12, 13 ] = wave (0) : "./MITfull/elev30/L30e078a.wav"
+[ 12, 14 ] = wave (0) : "./MITfull/elev30/L30e084a.wav"
+[ 12, 15 ] = wave (0) : "./MITfull/elev30/L30e090a.wav"
+[ 12, 16 ] = wave (0) : "./MITfull/elev30/L30e096a.wav"
+[ 12, 17 ] = wave (0) : "./MITfull/elev30/L30e102a.wav"
+[ 12, 18 ] = wave (0) : "./MITfull/elev30/L30e108a.wav"
+[ 12, 19 ] = wave (0) : "./MITfull/elev30/L30e114a.wav"
+[ 12, 20 ] = wave (0) : "./MITfull/elev30/L30e120a.wav"
+[ 12, 21 ] = wave (0) : "./MITfull/elev30/L30e126a.wav"
+[ 12, 22 ] = wave (0) : "./MITfull/elev30/L30e132a.wav"
+[ 12, 23 ] = wave (0) : "./MITfull/elev30/L30e138a.wav"
+[ 12, 24 ] = wave (0) : "./MITfull/elev30/L30e144a.wav"
+[ 12, 25 ] = wave (0) : "./MITfull/elev30/L30e150a.wav"
+[ 12, 26 ] = wave (0) : "./MITfull/elev30/L30e156a.wav"
+[ 12, 27 ] = wave (0) : "./MITfull/elev30/L30e162a.wav"
+[ 12, 28 ] = wave (0) : "./MITfull/elev30/L30e168a.wav"
+[ 12, 29 ] = wave (0) : "./MITfull/elev30/L30e174a.wav"
+[ 12, 30 ] = wave (0) : "./MITfull/elev30/L30e180a.wav"
+[ 12, 31 ] = wave (0) : "./MITfull/elev30/L30e186a.wav"
+[ 12, 32 ] = wave (0) : "./MITfull/elev30/L30e192a.wav"
+[ 12, 33 ] = wave (0) : "./MITfull/elev30/L30e198a.wav"
+[ 12, 34 ] = wave (0) : "./MITfull/elev30/L30e204a.wav"
+[ 12, 35 ] = wave (0) : "./MITfull/elev30/L30e210a.wav"
+[ 12, 36 ] = wave (0) : "./MITfull/elev30/L30e216a.wav"
+[ 12, 37 ] = wave (0) : "./MITfull/elev30/L30e222a.wav"
+[ 12, 38 ] = wave (0) : "./MITfull/elev30/L30e228a.wav"
+[ 12, 39 ] = wave (0) : "./MITfull/elev30/L30e234a.wav"
+[ 12, 40 ] = wave (0) : "./MITfull/elev30/L30e240a.wav"
+[ 12, 41 ] = wave (0) : "./MITfull/elev30/L30e246a.wav"
+[ 12, 42 ] = wave (0) : "./MITfull/elev30/L30e252a.wav"
+[ 12, 43 ] = wave (0) : "./MITfull/elev30/L30e258a.wav"
+[ 12, 44 ] = wave (0) : "./MITfull/elev30/L30e264a.wav"
+[ 12, 45 ] = wave (0) : "./MITfull/elev30/L30e270a.wav"
+[ 12, 46 ] = wave (0) : "./MITfull/elev30/L30e276a.wav"
+[ 12, 47 ] = wave (0) : "./MITfull/elev30/L30e282a.wav"
+[ 12, 48 ] = wave (0) : "./MITfull/elev30/L30e288a.wav"
+[ 12, 49 ] = wave (0) : "./MITfull/elev30/L30e294a.wav"
+[ 12, 50 ] = wave (0) : "./MITfull/elev30/L30e300a.wav"
+[ 12, 51 ] = wave (0) : "./MITfull/elev30/L30e306a.wav"
+[ 12, 52 ] = wave (0) : "./MITfull/elev30/L30e312a.wav"
+[ 12, 53 ] = wave (0) : "./MITfull/elev30/L30e318a.wav"
+[ 12, 54 ] = wave (0) : "./MITfull/elev30/L30e324a.wav"
+[ 12, 55 ] = wave (0) : "./MITfull/elev30/L30e330a.wav"
+[ 12, 56 ] = wave (0) : "./MITfull/elev30/L30e336a.wav"
+[ 12, 57 ] = wave (0) : "./MITfull/elev30/L30e342a.wav"
+[ 12, 58 ] = wave (0) : "./MITfull/elev30/L30e348a.wav"
+[ 12, 59 ] = wave (0) : "./MITfull/elev30/L30e354a.wav"
+
+[ 13,  0 ] = wave (0) : "./MITfull/elev40/L40e000a.wav"
+[ 13,  1 ] = wave (0) : "./MITfull/elev40/L40e006a.wav"
+[ 13,  2 ] = wave (0) : "./MITfull/elev40/L40e013a.wav"
+[ 13,  3 ] = wave (0) : "./MITfull/elev40/L40e019a.wav"
+[ 13,  4 ] = wave (0) : "./MITfull/elev40/L40e026a.wav"
+[ 13,  5 ] = wave (0) : "./MITfull/elev40/L40e032a.wav"
+[ 13,  6 ] = wave (0) : "./MITfull/elev40/L40e039a.wav"
+[ 13,  7 ] = wave (0) : "./MITfull/elev40/L40e045a.wav"
+[ 13,  8 ] = wave (0) : "./MITfull/elev40/L40e051a.wav"
+[ 13,  9 ] = wave (0) : "./MITfull/elev40/L40e058a.wav"
+[ 13, 10 ] = wave (0) : "./MITfull/elev40/L40e064a.wav"
+[ 13, 11 ] = wave (0) : "./MITfull/elev40/L40e071a.wav"
+[ 13, 12 ] = wave (0) : "./MITfull/elev40/L40e077a.wav"
+[ 13, 13 ] = wave (0) : "./MITfull/elev40/L40e084a.wav"
+[ 13, 14 ] = wave (0) : "./MITfull/elev40/L40e090a.wav"
+[ 13, 15 ] = wave (0) : "./MITfull/elev40/L40e096a.wav"
+[ 13, 16 ] = wave (0) : "./MITfull/elev40/L40e103a.wav"
+[ 13, 17 ] = wave (0) : "./MITfull/elev40/L40e109a.wav"
+[ 13, 18 ] = wave (0) : "./MITfull/elev40/L40e116a.wav"
+[ 13, 19 ] = wave (0) : "./MITfull/elev40/L40e122a.wav"
+[ 13, 20 ] = wave (0) : "./MITfull/elev40/L40e129a.wav"
+[ 13, 21 ] = wave (0) : "./MITfull/elev40/L40e135a.wav"
+[ 13, 22 ] = wave (0) : "./MITfull/elev40/L40e141a.wav"
+[ 13, 23 ] = wave (0) : "./MITfull/elev40/L40e148a.wav"
+[ 13, 24 ] = wave (0) : "./MITfull/elev40/L40e154a.wav"
+[ 13, 25 ] = wave (0) : "./MITfull/elev40/L40e161a.wav"
+[ 13, 26 ] = wave (0) : "./MITfull/elev40/L40e167a.wav"
+[ 13, 27 ] = wave (0) : "./MITfull/elev40/L40e174a.wav"
+[ 13, 28 ] = wave (0) : "./MITfull/elev40/L40e180a.wav"
+[ 13, 29 ] = wave (0) : "./MITfull/elev40/L40e186a.wav"
+[ 13, 30 ] = wave (0) : "./MITfull/elev40/L40e193a.wav"
+[ 13, 31 ] = wave (0) : "./MITfull/elev40/L40e199a.wav"
+[ 13, 32 ] = wave (0) : "./MITfull/elev40/L40e206a.wav"
+[ 13, 33 ] = wave (0) : "./MITfull/elev40/L40e212a.wav"
+[ 13, 34 ] = wave (0) : "./MITfull/elev40/L40e219a.wav"
+[ 13, 35 ] = wave (0) : "./MITfull/elev40/L40e225a.wav"
+[ 13, 36 ] = wave (0) : "./MITfull/elev40/L40e231a.wav"
+[ 13, 37 ] = wave (0) : "./MITfull/elev40/L40e238a.wav"
+[ 13, 38 ] = wave (0) : "./MITfull/elev40/L40e244a.wav"
+[ 13, 39 ] = wave (0) : "./MITfull/elev40/L40e251a.wav"
+[ 13, 40 ] = wave (0) : "./MITfull/elev40/L40e257a.wav"
+[ 13, 41 ] = wave (0) : "./MITfull/elev40/L40e264a.wav"
+[ 13, 42 ] = wave (0) : "./MITfull/elev40/L40e270a.wav"
+[ 13, 43 ] = wave (0) : "./MITfull/elev40/L40e276a.wav"
+[ 13, 44 ] = wave (0) : "./MITfull/elev40/L40e283a.wav"
+[ 13, 45 ] = wave (0) : "./MITfull/elev40/L40e289a.wav"
+[ 13, 46 ] = wave (0) : "./MITfull/elev40/L40e296a.wav"
+[ 13, 47 ] = wave (0) : "./MITfull/elev40/L40e302a.wav"
+[ 13, 48 ] = wave (0) : "./MITfull/elev40/L40e309a.wav"
+[ 13, 49 ] = wave (0) : "./MITfull/elev40/L40e315a.wav"
+[ 13, 50 ] = wave (0) : "./MITfull/elev40/L40e321a.wav"
+[ 13, 51 ] = wave (0) : "./MITfull/elev40/L40e328a.wav"
+[ 13, 52 ] = wave (0) : "./MITfull/elev40/L40e334a.wav"
+[ 13, 53 ] = wave (0) : "./MITfull/elev40/L40e341a.wav"
+[ 13, 54 ] = wave (0) : "./MITfull/elev40/L40e347a.wav"
+[ 13, 55 ] = wave (0) : "./MITfull/elev40/L40e354a.wav"
+
+[ 14,  0 ] = wave (0) : "./MITfull/elev50/L50e000a.wav"
+[ 14,  1 ] = wave (0) : "./MITfull/elev50/L50e008a.wav"
+[ 14,  2 ] = wave (0) : "./MITfull/elev50/L50e016a.wav"
+[ 14,  3 ] = wave (0) : "./MITfull/elev50/L50e024a.wav"
+[ 14,  4 ] = wave (0) : "./MITfull/elev50/L50e032a.wav"
+[ 14,  5 ] = wave (0) : "./MITfull/elev50/L50e040a.wav"
+[ 14,  6 ] = wave (0) : "./MITfull/elev50/L50e048a.wav"
+[ 14,  7 ] = wave (0) : "./MITfull/elev50/L50e056a.wav"
+[ 14,  8 ] = wave (0) : "./MITfull/elev50/L50e064a.wav"
+[ 14,  9 ] = wave (0) : "./MITfull/elev50/L50e072a.wav"
+[ 14, 10 ] = wave (0) : "./MITfull/elev50/L50e080a.wav"
+[ 14, 11 ] = wave (0) : "./MITfull/elev50/L50e088a.wav"
+[ 14, 12 ] = wave (0) : "./MITfull/elev50/L50e096a.wav"
+[ 14, 13 ] = wave (0) : "./MITfull/elev50/L50e104a.wav"
+[ 14, 14 ] = wave (0) : "./MITfull/elev50/L50e112a.wav"
+[ 14, 15 ] = wave (0) : "./MITfull/elev50/L50e120a.wav"
+[ 14, 16 ] = wave (0) : "./MITfull/elev50/L50e128a.wav"
+[ 14, 17 ] = wave (0) : "./MITfull/elev50/L50e136a.wav"
+[ 14, 18 ] = wave (0) : "./MITfull/elev50/L50e144a.wav"
+[ 14, 19 ] = wave (0) : "./MITfull/elev50/L50e152a.wav"
+[ 14, 20 ] = wave (0) : "./MITfull/elev50/L50e160a.wav"
+[ 14, 21 ] = wave (0) : "./MITfull/elev50/L50e168a.wav"
+[ 14, 22 ] = wave (0) : "./MITfull/elev50/L50e176a.wav"
+[ 14, 23 ] = wave (0) : "./MITfull/elev50/L50e184a.wav"
+[ 14, 24 ] = wave (0) : "./MITfull/elev50/L50e192a.wav"
+[ 14, 25 ] = wave (0) : "./MITfull/elev50/L50e200a.wav"
+[ 14, 26 ] = wave (0) : "./MITfull/elev50/L50e208a.wav"
+[ 14, 27 ] = wave (0) : "./MITfull/elev50/L50e216a.wav"
+[ 14, 28 ] = wave (0) : "./MITfull/elev50/L50e224a.wav"
+[ 14, 29 ] = wave (0) : "./MITfull/elev50/L50e232a.wav"
+[ 14, 30 ] = wave (0) : "./MITfull/elev50/L50e240a.wav"
+[ 14, 31 ] = wave (0) : "./MITfull/elev50/L50e248a.wav"
+[ 14, 32 ] = wave (0) : "./MITfull/elev50/L50e256a.wav"
+[ 14, 33 ] = wave (0) : "./MITfull/elev50/L50e264a.wav"
+[ 14, 34 ] = wave (0) : "./MITfull/elev50/L50e272a.wav"
+[ 14, 35 ] = wave (0) : "./MITfull/elev50/L50e280a.wav"
+[ 14, 36 ] = wave (0) : "./MITfull/elev50/L50e288a.wav"
+[ 14, 37 ] = wave (0) : "./MITfull/elev50/L50e296a.wav"
+[ 14, 38 ] = wave (0) : "./MITfull/elev50/L50e304a.wav"
+[ 14, 39 ] = wave (0) : "./MITfull/elev50/L50e312a.wav"
+[ 14, 40 ] = wave (0) : "./MITfull/elev50/L50e320a.wav"
+[ 14, 41 ] = wave (0) : "./MITfull/elev50/L50e328a.wav"
+[ 14, 42 ] = wave (0) : "./MITfull/elev50/L50e336a.wav"
+[ 14, 43 ] = wave (0) : "./MITfull/elev50/L50e344a.wav"
+[ 14, 44 ] = wave (0) : "./MITfull/elev50/L50e352a.wav"
+
+[ 15,  0 ] = wave (0) : "./MITfull/elev60/L60e000a.wav"
+[ 15,  1 ] = wave (0) : "./MITfull/elev60/L60e010a.wav"
+[ 15,  2 ] = wave (0) : "./MITfull/elev60/L60e020a.wav"
+[ 15,  3 ] = wave (0) : "./MITfull/elev60/L60e030a.wav"
+[ 15,  4 ] = wave (0) : "./MITfull/elev60/L60e040a.wav"
+[ 15,  5 ] = wave (0) : "./MITfull/elev60/L60e050a.wav"
+[ 15,  6 ] = wave (0) : "./MITfull/elev60/L60e060a.wav"
+[ 15,  7 ] = wave (0) : "./MITfull/elev60/L60e070a.wav"
+[ 15,  8 ] = wave (0) : "./MITfull/elev60/L60e080a.wav"
+[ 15,  9 ] = wave (0) : "./MITfull/elev60/L60e090a.wav"
+[ 15, 10 ] = wave (0) : "./MITfull/elev60/L60e100a.wav"
+[ 15, 11 ] = wave (0) : "./MITfull/elev60/L60e110a.wav"
+[ 15, 12 ] = wave (0) : "./MITfull/elev60/L60e120a.wav"
+[ 15, 13 ] = wave (0) : "./MITfull/elev60/L60e130a.wav"
+[ 15, 14 ] = wave (0) : "./MITfull/elev60/L60e140a.wav"
+[ 15, 15 ] = wave (0) : "./MITfull/elev60/L60e150a.wav"
+[ 15, 16 ] = wave (0) : "./MITfull/elev60/L60e160a.wav"
+[ 15, 17 ] = wave (0) : "./MITfull/elev60/L60e170a.wav"
+[ 15, 18 ] = wave (0) : "./MITfull/elev60/L60e180a.wav"
+[ 15, 19 ] = wave (0) : "./MITfull/elev60/L60e190a.wav"
+[ 15, 20 ] = wave (0) : "./MITfull/elev60/L60e200a.wav"
+[ 15, 21 ] = wave (0) : "./MITfull/elev60/L60e210a.wav"
+[ 15, 22 ] = wave (0) : "./MITfull/elev60/L60e220a.wav"
+[ 15, 23 ] = wave (0) : "./MITfull/elev60/L60e230a.wav"
+[ 15, 24 ] = wave (0) : "./MITfull/elev60/L60e240a.wav"
+[ 15, 25 ] = wave (0) : "./MITfull/elev60/L60e250a.wav"
+[ 15, 26 ] = wave (0) : "./MITfull/elev60/L60e260a.wav"
+[ 15, 27 ] = wave (0) : "./MITfull/elev60/L60e270a.wav"
+[ 15, 28 ] = wave (0) : "./MITfull/elev60/L60e280a.wav"
+[ 15, 29 ] = wave (0) : "./MITfull/elev60/L60e290a.wav"
+[ 15, 30 ] = wave (0) : "./MITfull/elev60/L60e300a.wav"
+[ 15, 31 ] = wave (0) : "./MITfull/elev60/L60e310a.wav"
+[ 15, 32 ] = wave (0) : "./MITfull/elev60/L60e320a.wav"
+[ 15, 33 ] = wave (0) : "./MITfull/elev60/L60e330a.wav"
+[ 15, 34 ] = wave (0) : "./MITfull/elev60/L60e340a.wav"
+[ 15, 35 ] = wave (0) : "./MITfull/elev60/L60e350a.wav"
+
+[ 16,  0 ] = wave (0) : "./MITfull/elev70/L70e000a.wav"
+[ 16,  1 ] = wave (0) : "./MITfull/elev70/L70e015a.wav"
+[ 16,  2 ] = wave (0) : "./MITfull/elev70/L70e030a.wav"
+[ 16,  3 ] = wave (0) : "./MITfull/elev70/L70e045a.wav"
+[ 16,  4 ] = wave (0) : "./MITfull/elev70/L70e060a.wav"
+[ 16,  5 ] = wave (0) : "./MITfull/elev70/L70e075a.wav"
+[ 16,  6 ] = wave (0) : "./MITfull/elev70/L70e090a.wav"
+[ 16,  7 ] = wave (0) : "./MITfull/elev70/L70e105a.wav"
+[ 16,  8 ] = wave (0) : "./MITfull/elev70/L70e120a.wav"
+[ 16,  9 ] = wave (0) : "./MITfull/elev70/L70e135a.wav"
+[ 16, 10 ] = wave (0) : "./MITfull/elev70/L70e150a.wav"
+[ 16, 11 ] = wave (0) : "./MITfull/elev70/L70e165a.wav"
+[ 16, 12 ] = wave (0) : "./MITfull/elev70/L70e180a.wav"
+[ 16, 13 ] = wave (0) : "./MITfull/elev70/L70e195a.wav"
+[ 16, 14 ] = wave (0) : "./MITfull/elev70/L70e210a.wav"
+[ 16, 15 ] = wave (0) : "./MITfull/elev70/L70e225a.wav"
+[ 16, 16 ] = wave (0) : "./MITfull/elev70/L70e240a.wav"
+[ 16, 17 ] = wave (0) : "./MITfull/elev70/L70e255a.wav"
+[ 16, 18 ] = wave (0) : "./MITfull/elev70/L70e270a.wav"
+[ 16, 19 ] = wave (0) : "./MITfull/elev70/L70e285a.wav"
+[ 16, 20 ] = wave (0) : "./MITfull/elev70/L70e300a.wav"
+[ 16, 21 ] = wave (0) : "./MITfull/elev70/L70e315a.wav"
+[ 16, 22 ] = wave (0) : "./MITfull/elev70/L70e330a.wav"
+[ 16, 23 ] = wave (0) : "./MITfull/elev70/L70e345a.wav"
+
+[ 17,  0 ] = wave (0) : "./MITfull/elev80/L80e000a.wav"
+[ 17,  1 ] = wave (0) : "./MITfull/elev80/L80e030a.wav"
+[ 17,  2 ] = wave (0) : "./MITfull/elev80/L80e060a.wav"
+[ 17,  3 ] = wave (0) : "./MITfull/elev80/L80e090a.wav"
+[ 17,  4 ] = wave (0) : "./MITfull/elev80/L80e120a.wav"
+[ 17,  5 ] = wave (0) : "./MITfull/elev80/L80e150a.wav"
+[ 17,  6 ] = wave (0) : "./MITfull/elev80/L80e180a.wav"
+[ 17,  7 ] = wave (0) : "./MITfull/elev80/L80e210a.wav"
+[ 17,  8 ] = wave (0) : "./MITfull/elev80/L80e240a.wav"
+[ 17,  9 ] = wave (0) : "./MITfull/elev80/L80e270a.wav"
+[ 17, 10 ] = wave (0) : "./MITfull/elev80/L80e300a.wav"
+[ 17, 11 ] = wave (0) : "./MITfull/elev80/L80e330a.wav"
+
+[ 18,  0 ] = wave (0) : "./MITfull/elev90/L90e000a.wav"
+
diff --git a/utils/MIT_KEMAR_sofa.def b/utils/MIT_KEMAR_sofa.def
new file mode 100644 (file)
index 0000000..5f25815
--- /dev/null
@@ -0,0 +1,51 @@
+# This is a makemhr HRIR definition file.  It is used to define the layout and
+# source data to be processed into an OpenAL Soft compatible HRTF.
+#
+# This definition is used to transform the SOFA packaged KEMAR HRIRs
+# originally provided by Bill Gardner <billg@media.mit.edu> and Keith Martin
+# <kdm@media.mit.edu> of MIT Media Laboratory.
+#
+# The SOFA conversion is available from:
+#
+#  http://sofacoustics.org/data/database/mit/
+#
+# The original data is available from:
+#
+#  http://sound.media.mit.edu/resources/KEMAR.html
+#
+# It is copyrighted 1994 by MIT Media Laboratory, and provided free of charge
+# with no restrictions on use so long as the authors (above) are cited.
+
+# Sampling rate of the HRIR data (in hertz).
+rate     = 44100
+
+# The SOFA file is stereo, but the original data was mono.  Channels are just
+# mirrored by azimuth; so save some memory by allowing OpenAL Soft to mirror
+# them at run time.
+type     = mono
+
+points   = 512
+
+radius   = 0.09
+
+# The MIT set has only one field with a distance of 1.4m.
+distance = 1.4
+
+# The MIT set varies the number of azimuths for each elevation to maintain
+# an average distance between them.
+azimuths = 1, 12, 24, 36, 45, 56, 60, 72, 72, 72, 72, 72, 60, 56, 45, 36, 24, 12, 1
+
+# Normally the dataset would be composed manually by listing all necessary
+# 'sofa' sources with the appropriate radius, elevation, azimuth (counter-
+# clockwise for SOFA files) and receiver arguments:
+#
+#   [  5,  0 ] = sofa (1.4, -40.0,   0.0 : 0) : "./mit_kemar_normal_pinna.sofa"
+#   [  5,  1 ] = sofa (1.4, -40.0, 353.6 : 0) : "./mit_kemar_normal_pinna.sofa"
+#   [  5,  2 ] = sofa (1.4, -40.0, 347.1 : 0) : "./mit_kemar_normal_pinna.sofa"
+#   [  5,  3 ] = sofa (1.4, -40.0, 340.7 : 0) : "./mit_kemar_normal_pinna.sofa"
+#   ...
+#
+# If HRIR composition isn't necessary, it's easier to just use the following:
+
+[ * ] = sofa : "./mit_kemar_normal_pinna.sofa" mono
+
diff --git a/utils/SCUT_KEMAR.def b/utils/SCUT_KEMAR.def
new file mode 100644 (file)
index 0000000..e5ae4ff
--- /dev/null
@@ -0,0 +1,48 @@
+# This is a makemhr HRIR definition file.  It is used to define the layout and
+# source data to be processed into an OpenAL Soft compatible HRTF.
+#
+# This definition is used to transform the near-field KEMAR HRIRs provided by
+# Bosun Xie <phbsxie@scut.edu.cn> of the South China University of
+# Technology, Guangzhou, China; and converted from SCUT to SOFA format by
+# Piotr Majdak <piotr@majdak.com> of the Acoustics Research Institute,
+# Austrian Academy of Sciences.
+#
+# A copy of the data (SCUT_KEMAR_radius_all.sofa) is available from:
+#
+#  http://sofacoustics.org/data/database/scut/SCUT_KEMAR_radius_all.sofa
+#
+# It is provided under the Creative Commons CC 3.0 BY-SA-NC license:
+#
+#  https://creativecommons.org/licenses/by-nc-sa/3.0/
+
+rate     = 44100
+
+# While the SOFA file is stereo, doubling the size of the data set will cause
+# the utility to exhaust its address space if compiled 32-bit.  Since the
+# dummy head is symmetric, the same results (ignoring variations caused by
+# measurement error) can be obtained using mono channel processing.
+type     = mono
+
+points   = 512
+
+radius   = 0.09
+
+# This data set has 10 fields ranging from 0.2m to 1m.  The layout was
+# obtained using the sofa-info utility.
+distance = 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0
+
+azimuths = 1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1;
+           1, 24, 36, 72, 72, 72, 72, 72, 72, 72, 36, 24, 1
+
+# Given the above compatible layout, we can automatically process the entire
+# data set.
+[ * ] = sofa : "./SCUT_KEMAR_radius_all.sofa" mono
+
diff --git a/utils/alsoft-config/CMakeLists.txt b/utils/alsoft-config/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c6a4607
--- /dev/null
@@ -0,0 +1,32 @@
+project(alsoft-config)
+
+if(Qt5Widgets_FOUND)
+    qt5_wrap_ui(UIS  mainwindow.ui)
+
+    qt5_wrap_cpp(MOCS  mainwindow.h)
+
+    add_executable(alsoft-config
+        main.cpp
+        mainwindow.cpp
+        mainwindow.h
+        verstr.cpp
+        verstr.h
+        ${UIS} ${RSCS} ${TRS} ${MOCS})
+    target_link_libraries(alsoft-config Qt5::Widgets)
+    target_include_directories(alsoft-config PRIVATE "${alsoft-config_BINARY_DIR}"
+        "${OpenAL_BINARY_DIR}")
+    set_target_properties(alsoft-config PROPERTIES ${DEFAULT_TARGET_PROPS}
+        RUNTIME_OUTPUT_DIRECTORY ${OpenAL_BINARY_DIR})
+    if(TARGET build_version)
+        add_dependencies(alsoft-config build_version)
+    endif()
+
+    message(STATUS "Building configuration program")
+
+    if(ALSOFT_INSTALL_UTILS)
+        install(TARGETS alsoft-config
+            RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+            LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+            ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
+    endif()
+endif()
diff --git a/utils/alsoft-config/main.cpp b/utils/alsoft-config/main.cpp
new file mode 100644 (file)
index 0000000..b48f94e
--- /dev/null
@@ -0,0 +1,11 @@
+#include "mainwindow.h"
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    MainWindow w;
+    w.show();
+
+    return a.exec();
+}
diff --git a/utils/alsoft-config/mainwindow.cpp b/utils/alsoft-config/mainwindow.cpp
new file mode 100644 (file)
index 0000000..bee7022
--- /dev/null
@@ -0,0 +1,1449 @@
+
+#include "config.h"
+
+#include "mainwindow.h"
+
+#include <iostream>
+#include <cmath>
+
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QCloseEvent>
+#include <QSettings>
+#include <QtGlobal>
+#include "ui_mainwindow.h"
+#include "verstr.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#include <shlobj.h>
+#endif
+
+namespace {
+
+const struct {
+    char backend_name[16];
+    char full_string[32];
+} backendList[] = {
+#ifdef HAVE_JACK
+    { "jack", "JACK" },
+#endif
+#ifdef HAVE_PIPEWIRE
+    { "pipewire", "PipeWire" },
+#endif
+#ifdef HAVE_PULSEAUDIO
+    { "pulse", "PulseAudio" },
+#endif
+#ifdef HAVE_ALSA
+    { "alsa", "ALSA" },
+#endif
+#ifdef HAVE_COREAUDIO
+    { "core", "CoreAudio" },
+#endif
+#ifdef HAVE_OSS
+    { "oss", "OSS" },
+#endif
+#ifdef HAVE_SOLARIS
+    { "solaris", "Solaris" },
+#endif
+#ifdef HAVE_SNDIO
+    { "sndio", "SoundIO" },
+#endif
+#ifdef HAVE_QSA
+    { "qsa", "QSA" },
+#endif
+#ifdef HAVE_WASAPI
+    { "wasapi", "WASAPI" },
+#endif
+#ifdef HAVE_DSOUND
+    { "dsound", "DirectSound" },
+#endif
+#ifdef HAVE_WINMM
+    { "winmm", "Windows Multimedia" },
+#endif
+#ifdef HAVE_PORTAUDIO
+    { "port", "PortAudio" },
+#endif
+#ifdef HAVE_OPENSL
+    { "opensl", "OpenSL" },
+#endif
+
+    { "null", "Null Output" },
+#ifdef HAVE_WAVE
+    { "wave", "Wave Writer" },
+#endif
+    { "", "" }
+};
+
+const struct NameValuePair {
+    const char name[64];
+    const char value[16];
+} speakerModeList[] = {
+    { "Autodetect", "" },
+    { "Mono", "mono" },
+    { "Stereo", "stereo" },
+    { "Quadraphonic", "quad" },
+    { "5.1 Surround", "surround51" },
+    { "6.1 Surround", "surround61" },
+    { "7.1 Surround", "surround71" },
+    { "3D7.1 Surround", "surround3d71" },
+
+    { "Ambisonic, 1st Order", "ambi1" },
+    { "Ambisonic, 2nd Order", "ambi2" },
+    { "Ambisonic, 3rd Order", "ambi3" },
+
+    { "", "" }
+}, sampleTypeList[] = {
+    { "Autodetect", "" },
+    { "8-bit int", "int8" },
+    { "8-bit uint", "uint8" },
+    { "16-bit int", "int16" },
+    { "16-bit uint", "uint16" },
+    { "32-bit int", "int32" },
+    { "32-bit uint", "uint32" },
+    { "32-bit float", "float32" },
+
+    { "", "" }
+}, resamplerList[] = {
+    { "Point", "point" },
+    { "Linear", "linear" },
+    { "Cubic Spline", "cubic" },
+    { "Default (Cubic Spline)", "" },
+    { "11th order Sinc (fast)", "fast_bsinc12" },
+    { "11th order Sinc", "bsinc12" },
+    { "23rd order Sinc (fast)", "fast_bsinc24" },
+    { "23rd order Sinc", "bsinc24" },
+
+    { "", "" }
+}, stereoModeList[] = {
+    { "Autodetect", "" },
+    { "Speakers", "speakers" },
+    { "Headphones", "headphones" },
+
+    { "", "" }
+}, stereoEncList[] = {
+    { "Default", "" },
+    { "Basic", "panpot" },
+    { "UHJ", "uhj" },
+    { "Binaural", "hrtf" },
+
+    { "", "" }
+}, ambiFormatList[] = {
+    { "Default", "" },
+    { "AmbiX (ACN, SN3D)", "ambix" },
+    { "Furse-Malham", "fuma" },
+    { "ACN, N3D", "acn+n3d" },
+    { "ACN, FuMa", "acn+fuma" },
+
+    { "", "" }
+}, hrtfModeList[] = {
+    { "1st Order Ambisonic", "ambi1" },
+    { "2nd Order Ambisonic", "ambi2" },
+    { "3rd Order Ambisonic", "ambi3" },
+    { "Default (Full)", "" },
+    { "Full", "full" },
+
+    { "", "" }
+};
+
+QString getDefaultConfigName()
+{
+#ifdef Q_OS_WIN32
+    static const char fname[] = "alsoft.ini";
+    auto get_appdata_path = []() noexcept -> QString
+    {
+        WCHAR buffer[MAX_PATH];
+        if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE)
+            return QString::fromWCharArray(buffer);
+        return QString();
+    };
+    QString base = get_appdata_path();
+#else
+    static const char fname[] = "alsoft.conf";
+    QByteArray base = qgetenv("XDG_CONFIG_HOME");
+    if(base.isEmpty())
+    {
+        base = qgetenv("HOME");
+        if(base.isEmpty() == false)
+            base += "/.config";
+    }
+#endif
+    if(base.isEmpty() == false)
+        return base +'/'+ fname;
+    return fname;
+}
+
+QString getBaseDataPath()
+{
+#ifdef Q_OS_WIN32
+    auto get_appdata_path = []() noexcept -> QString
+    {
+        WCHAR buffer[MAX_PATH];
+        if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE)
+            return QString::fromWCharArray(buffer);
+        return QString();
+    };
+    QString base = get_appdata_path();
+#else
+    QByteArray base = qgetenv("XDG_DATA_HOME");
+    if(base.isEmpty())
+    {
+        base = qgetenv("HOME");
+        if(!base.isEmpty())
+            base += "/.local/share";
+    }
+#endif
+    return base;
+}
+
+QStringList getAllDataPaths(const QString &append)
+{
+    QStringList list;
+    list.append(getBaseDataPath());
+#ifdef Q_OS_WIN32
+    // TODO: Common AppData path
+#else
+    QString paths = qgetenv("XDG_DATA_DIRS");
+    if(paths.isEmpty())
+        paths = "/usr/local/share/:/usr/share/";
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+    list += paths.split(QChar(':'), Qt::SkipEmptyParts);
+#else
+    list += paths.split(QChar(':'), QString::SkipEmptyParts);
+#endif
+#endif
+    QStringList::iterator iter = list.begin();
+    while(iter != list.end())
+    {
+        if(iter->isEmpty())
+            iter = list.erase(iter);
+        else
+        {
+            iter->append(append);
+            iter++;
+        }
+    }
+    return list;
+}
+
+template<size_t N>
+QString getValueFromName(const NameValuePair (&list)[N], const QString &str)
+{
+    for(size_t i = 0;i < N-1;i++)
+    {
+        if(str == list[i].name)
+            return list[i].value;
+    }
+    return QString{};
+}
+
+template<size_t N>
+QString getNameFromValue(const NameValuePair (&list)[N], const QString &str)
+{
+    for(size_t i = 0;i < N-1;i++)
+    {
+        if(str == list[i].value)
+            return list[i].name;
+    }
+    return QString{};
+}
+
+
+Qt::CheckState getCheckState(const QVariant &var)
+{
+    if(var.isNull())
+        return Qt::PartiallyChecked;
+    if(var.toBool())
+        return Qt::Checked;
+    return Qt::Unchecked;
+}
+
+QString getCheckValue(const QCheckBox *checkbox)
+{
+    const Qt::CheckState state{checkbox->checkState()};
+    if(state == Qt::Checked)
+        return QString{"true"};
+    if(state == Qt::Unchecked)
+        return QString{"false"};
+    return QString{};
+}
+
+}
+
+MainWindow::MainWindow(QWidget *parent) :
+    QMainWindow(parent),
+    ui(new Ui::MainWindow),
+    mPeriodSizeValidator(nullptr),
+    mPeriodCountValidator(nullptr),
+    mSourceCountValidator(nullptr),
+    mEffectSlotValidator(nullptr),
+    mSourceSendValidator(nullptr),
+    mSampleRateValidator(nullptr),
+    mJackBufferValidator(nullptr),
+    mNeedsSave(false)
+{
+    ui->setupUi(this);
+
+    for(int i = 0;speakerModeList[i].name[0];i++)
+        ui->channelConfigCombo->addItem(speakerModeList[i].name);
+    ui->channelConfigCombo->adjustSize();
+    for(int i = 0;sampleTypeList[i].name[0];i++)
+        ui->sampleFormatCombo->addItem(sampleTypeList[i].name);
+    ui->sampleFormatCombo->adjustSize();
+    for(int i = 0;stereoModeList[i].name[0];i++)
+        ui->stereoModeCombo->addItem(stereoModeList[i].name);
+    ui->stereoModeCombo->adjustSize();
+    for(int i = 0;stereoEncList[i].name[0];i++)
+        ui->stereoEncodingComboBox->addItem(stereoEncList[i].name);
+    ui->stereoEncodingComboBox->adjustSize();
+    for(int i = 0;ambiFormatList[i].name[0];i++)
+        ui->ambiFormatComboBox->addItem(ambiFormatList[i].name);
+    ui->ambiFormatComboBox->adjustSize();
+
+    int count;
+    for(count = 0;resamplerList[count].name[0];count++) {
+    }
+    ui->resamplerSlider->setRange(0, count-1);
+
+    for(count = 0;hrtfModeList[count].name[0];count++) {
+    }
+    ui->hrtfmodeSlider->setRange(0, count-1);
+
+#if !defined(HAVE_NEON) && !defined(HAVE_SSE)
+    ui->cpuExtDisabledLabel->move(ui->cpuExtDisabledLabel->x(), ui->cpuExtDisabledLabel->y() - 60);
+#else
+    ui->cpuExtDisabledLabel->setVisible(false);
+#endif
+
+#ifndef HAVE_NEON
+
+#ifndef HAVE_SSE4_1
+#ifndef HAVE_SSE3
+#ifndef HAVE_SSE2
+#ifndef HAVE_SSE
+    ui->enableSSECheckBox->setVisible(false);
+#endif /* !SSE */
+    ui->enableSSE2CheckBox->setVisible(false);
+#endif /* !SSE2 */
+    ui->enableSSE3CheckBox->setVisible(false);
+#endif /* !SSE3 */
+    ui->enableSSE41CheckBox->setVisible(false);
+#endif /* !SSE4.1 */
+    ui->enableNeonCheckBox->setVisible(false);
+
+#else /* !Neon */
+
+#ifndef HAVE_SSE4_1
+#ifndef HAVE_SSE3
+#ifndef HAVE_SSE2
+#ifndef HAVE_SSE
+    ui->enableNeonCheckBox->move(ui->enableNeonCheckBox->x(), ui->enableNeonCheckBox->y() - 30);
+    ui->enableSSECheckBox->setVisible(false);
+#endif /* !SSE */
+    ui->enableSSE2CheckBox->setVisible(false);
+#endif /* !SSE2 */
+    ui->enableSSE3CheckBox->setVisible(false);
+#endif /* !SSE3 */
+    ui->enableSSE41CheckBox->setVisible(false);
+#endif /* !SSE4.1 */
+
+#endif
+
+#ifndef ALSOFT_EAX
+    ui->enableEaxCheck->setChecked(Qt::Unchecked);
+    ui->enableEaxCheck->setEnabled(false);
+    ui->enableEaxCheck->setVisible(false);
+#endif
+
+    mPeriodSizeValidator = new QIntValidator{64, 8192, this};
+    ui->periodSizeEdit->setValidator(mPeriodSizeValidator);
+    mPeriodCountValidator = new QIntValidator{2, 16, this};
+    ui->periodCountEdit->setValidator(mPeriodCountValidator);
+
+    mSourceCountValidator = new QIntValidator{0, 4096, this};
+    ui->srcCountLineEdit->setValidator(mSourceCountValidator);
+    mEffectSlotValidator = new QIntValidator{0, 64, this};
+    ui->effectSlotLineEdit->setValidator(mEffectSlotValidator);
+    mSourceSendValidator = new QIntValidator{0, 16, this};
+    ui->srcSendLineEdit->setValidator(mSourceSendValidator);
+    mSampleRateValidator = new QIntValidator{8000, 192000, this};
+    ui->sampleRateCombo->lineEdit()->setValidator(mSampleRateValidator);
+
+    mJackBufferValidator = new QIntValidator{0, 8192, this};
+    ui->jackBufferSizeLine->setValidator(mJackBufferValidator);
+
+    connect(ui->actionLoad, &QAction::triggered, this, &MainWindow::loadConfigFromFile);
+    connect(ui->actionSave_As, &QAction::triggered, this, &MainWindow::saveConfigAsFile);
+
+    connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAboutPage);
+
+    connect(ui->closeCancelButton, &QPushButton::clicked, this, &MainWindow::cancelCloseAction);
+    connect(ui->applyButton, &QPushButton::clicked, this, &MainWindow::saveCurrentConfig);
+
+    auto qcb_cicint = static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged);
+    connect(ui->channelConfigCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->sampleFormatCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->stereoModeCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->sampleRateCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->sampleRateCombo, &QComboBox::editTextChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->resamplerSlider, &QSlider::valueChanged, this, &MainWindow::updateResamplerLabel);
+
+    connect(ui->periodSizeSlider, &QSlider::valueChanged, this, &MainWindow::updatePeriodSizeEdit);
+    connect(ui->periodSizeEdit, &QLineEdit::editingFinished, this, &MainWindow::updatePeriodSizeSlider);
+    connect(ui->periodCountSlider, &QSlider::valueChanged, this, &MainWindow::updatePeriodCountEdit);
+    connect(ui->periodCountEdit, &QLineEdit::editingFinished, this, &MainWindow::updatePeriodCountSlider);
+
+    connect(ui->stereoEncodingComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->ambiFormatComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->outputLimiterCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->outputDitherCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->decoderHQModeCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->decoderDistCompCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->decoderNFEffectsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    auto qdsb_vcd = static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged);
+    connect(ui->decoderSpeakerDistSpinBox, qdsb_vcd, this, &MainWindow::enableApplyButton);
+    connect(ui->decoderQuadLineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->decoderQuadButton, &QPushButton::clicked, this, &MainWindow::selectQuadDecoderFile);
+    connect(ui->decoder51LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->decoder51Button, &QPushButton::clicked, this, &MainWindow::select51DecoderFile);
+    connect(ui->decoder61LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->decoder61Button, &QPushButton::clicked, this, &MainWindow::select61DecoderFile);
+    connect(ui->decoder71LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->decoder71Button, &QPushButton::clicked, this, &MainWindow::select71DecoderFile);
+    connect(ui->decoder3D71LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->decoder3D71Button, &QPushButton::clicked, this, &MainWindow::select3D71DecoderFile);
+
+    connect(ui->preferredHrtfComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->hrtfmodeSlider, &QSlider::valueChanged, this, &MainWindow::updateHrtfModeLabel);
+
+    connect(ui->hrtfAddButton, &QPushButton::clicked, this, &MainWindow::addHrtfFile);
+    connect(ui->hrtfRemoveButton, &QPushButton::clicked, this, &MainWindow::removeHrtfFile);
+    connect(ui->hrtfFileList, &QListWidget::itemSelectionChanged, this, &MainWindow::updateHrtfRemoveButton);
+    connect(ui->defaultHrtfPathsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->srcCountLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton);
+    connect(ui->srcSendLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton);
+    connect(ui->effectSlotLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton);
+
+    connect(ui->enableSSECheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableSSE2CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableSSE3CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableSSE41CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableNeonCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    ui->enabledBackendList->setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(ui->enabledBackendList, &QListWidget::customContextMenuRequested, this, &MainWindow::showEnabledBackendMenu);
+
+    ui->disabledBackendList->setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(ui->disabledBackendList, &QListWidget::customContextMenuRequested, this, &MainWindow::showDisabledBackendMenu);
+    connect(ui->backendCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->defaultReverbComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
+    connect(ui->enableEaxReverbCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableStdReverbCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableAutowahCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableChorusCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableCompressorCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableDistortionCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableEchoCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableEqualizerCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableFlangerCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableFrequencyShifterCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableModulatorCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableDedicatedCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enablePitchShifterCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableVocalMorpherCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->enableEaxCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->pulseAutospawnCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->pulseAllowMovesCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->pulseFixRateCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->pulseAdjLatencyCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->pwireAssumeAudioCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->pwireRtMixCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->wasapiResamplerCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->jackAutospawnCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->jackConnectPortsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->jackRtMixCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->jackBufferSizeSlider, &QSlider::valueChanged, this, &MainWindow::updateJackBufferSizeEdit);
+    connect(ui->jackBufferSizeLine, &QLineEdit::editingFinished, this, &MainWindow::updateJackBufferSizeSlider);
+
+    connect(ui->alsaDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->alsaDefaultCaptureLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->alsaResamplerCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->alsaMmapCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    connect(ui->ossDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->ossPlaybackPushButton, &QPushButton::clicked, this, &MainWindow::selectOSSPlayback);
+    connect(ui->ossDefaultCaptureLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->ossCapturePushButton, &QPushButton::clicked, this, &MainWindow::selectOSSCapture);
+
+    connect(ui->solarisDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->solarisPlaybackPushButton, &QPushButton::clicked, this, &MainWindow::selectSolarisPlayback);
+
+    connect(ui->waveOutputLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
+    connect(ui->waveOutputButton, &QPushButton::clicked, this, &MainWindow::selectWaveOutput);
+    connect(ui->waveBFormatCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
+
+    ui->backendListWidget->setCurrentRow(0);
+    ui->tabWidget->setCurrentIndex(0);
+
+    for(int i = 1;i < ui->backendListWidget->count();i++)
+        ui->backendListWidget->setRowHidden(i, true);
+    for(int i = 0;backendList[i].backend_name[0];i++)
+    {
+        QList<QListWidgetItem*> items = ui->backendListWidget->findItems(
+            backendList[i].full_string, Qt::MatchFixedString);
+        foreach(QListWidgetItem *item, items)
+            item->setHidden(false);
+    }
+
+    loadConfig(getDefaultConfigName());
+}
+
+MainWindow::~MainWindow()
+{
+    delete ui;
+    delete mPeriodSizeValidator;
+    delete mPeriodCountValidator;
+    delete mSourceCountValidator;
+    delete mEffectSlotValidator;
+    delete mSourceSendValidator;
+    delete mSampleRateValidator;
+    delete mJackBufferValidator;
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+    if(!mNeedsSave)
+        event->accept();
+    else
+    {
+        QMessageBox::StandardButton btn = QMessageBox::warning(this,
+            tr("Apply changes?"), tr("Save changes before quitting?"),
+            QMessageBox::Save | QMessageBox::No | QMessageBox::Cancel);
+        if(btn == QMessageBox::Save)
+            saveCurrentConfig();
+        if(btn == QMessageBox::Cancel)
+            event->ignore();
+        else
+            event->accept();
+    }
+}
+
+void MainWindow::cancelCloseAction()
+{
+    mNeedsSave = false;
+    close();
+}
+
+
+void MainWindow::showAboutPage()
+{
+    QMessageBox::information(this, tr("About"),
+        tr("OpenAL Soft Configuration Utility.\nBuilt for OpenAL Soft library version ") +
+        GetVersionString());
+}
+
+
+QStringList MainWindow::collectHrtfs()
+{
+    QStringList ret;
+    QStringList processed;
+
+    for(int i = 0;i < ui->hrtfFileList->count();i++)
+    {
+        QDir dir(ui->hrtfFileList->item(i)->text());
+        QStringList fnames = dir.entryList(QDir::Files | QDir::Readable, QDir::Name);
+        foreach(const QString &fname, fnames)
+        {
+            if(!fname.endsWith(".mhr", Qt::CaseInsensitive))
+                continue;
+            QString fullname{dir.absoluteFilePath(fname)};
+            if(processed.contains(fullname))
+                continue;
+            processed.push_back(fullname);
+
+            QString name{fname.left(fname.length()-4)};
+            if(!ret.contains(name))
+                ret.push_back(name);
+            else
+            {
+                size_t i{2};
+                do {
+                    QString s = name+" #"+QString::number(i);
+                    if(!ret.contains(s))
+                    {
+                        ret.push_back(s);
+                        break;
+                    }
+                    ++i;
+                } while(1);
+            }
+        }
+    }
+
+    if(ui->defaultHrtfPathsCheckBox->isChecked())
+    {
+        QStringList paths = getAllDataPaths("/openal/hrtf");
+        foreach(const QString &name, paths)
+        {
+            QDir dir{name};
+            QStringList fnames{dir.entryList(QDir::Files | QDir::Readable, QDir::Name)};
+            foreach(const QString &fname, fnames)
+            {
+                if(!fname.endsWith(".mhr", Qt::CaseInsensitive))
+                    continue;
+                QString fullname{dir.absoluteFilePath(fname)};
+                if(processed.contains(fullname))
+                    continue;
+                processed.push_back(fullname);
+
+                QString name{fname.left(fname.length()-4)};
+                if(!ret.contains(name))
+                    ret.push_back(name);
+                else
+                {
+                    size_t i{2};
+                    do {
+                        QString s{name+" #"+QString::number(i)};
+                        if(!ret.contains(s))
+                        {
+                            ret.push_back(s);
+                            break;
+                        }
+                        ++i;
+                    } while(1);
+                }
+            }
+        }
+
+#ifdef ALSOFT_EMBED_HRTF_DATA
+        ret.push_back("Built-In HRTF");
+#endif
+    }
+    return ret;
+}
+
+
+void MainWindow::loadConfigFromFile()
+{
+    QString fname = QFileDialog::getOpenFileName(this, tr("Select Files"));
+    if(fname.isEmpty() == false)
+        loadConfig(fname);
+}
+
+void MainWindow::loadConfig(const QString &fname)
+{
+    QSettings settings{fname, QSettings::IniFormat};
+
+    QString sampletype = settings.value("sample-type").toString();
+    ui->sampleFormatCombo->setCurrentIndex(0);
+    if(sampletype.isEmpty() == false)
+    {
+        QString str{getNameFromValue(sampleTypeList, sampletype)};
+        if(!str.isEmpty())
+        {
+            const int j{ui->sampleFormatCombo->findText(str)};
+            if(j > 0) ui->sampleFormatCombo->setCurrentIndex(j);
+        }
+    }
+
+    QString channelconfig{settings.value("channels").toString()};
+    ui->channelConfigCombo->setCurrentIndex(0);
+    if(channelconfig.isEmpty() == false)
+    {
+        if(channelconfig == "surround51rear")
+            channelconfig = "surround51";
+        QString str{getNameFromValue(speakerModeList, channelconfig)};
+        if(!str.isEmpty())
+        {
+            const int j{ui->channelConfigCombo->findText(str)};
+            if(j > 0) ui->channelConfigCombo->setCurrentIndex(j);
+        }
+    }
+
+    QString srate{settings.value("frequency").toString()};
+    if(srate.isEmpty())
+        ui->sampleRateCombo->setCurrentIndex(0);
+    else
+    {
+        ui->sampleRateCombo->lineEdit()->clear();
+        ui->sampleRateCombo->lineEdit()->insert(srate);
+    }
+
+    ui->srcCountLineEdit->clear();
+    ui->srcCountLineEdit->insert(settings.value("sources").toString());
+    ui->effectSlotLineEdit->clear();
+    ui->effectSlotLineEdit->insert(settings.value("slots").toString());
+    ui->srcSendLineEdit->clear();
+    ui->srcSendLineEdit->insert(settings.value("sends").toString());
+
+    QString resampler = settings.value("resampler").toString().trimmed();
+    ui->resamplerSlider->setValue(2);
+    ui->resamplerLabel->setText(resamplerList[2].name);
+    /* The "sinc4" and "sinc8" resamplers are no longer supported. Use "cubic"
+     * as a fallback.
+     */
+    if(resampler == "sinc4" || resampler == "sinc8")
+        resampler = "cubic";
+    /* The "bsinc" resampler name is an alias for "bsinc12". */
+    else if(resampler == "bsinc")
+        resampler = "bsinc12";
+    for(int i = 0;resamplerList[i].name[0];i++)
+    {
+        if(resampler == resamplerList[i].value)
+        {
+            ui->resamplerSlider->setValue(i);
+            ui->resamplerLabel->setText(resamplerList[i].name);
+            break;
+        }
+    }
+
+    QString stereomode = settings.value("stereo-mode").toString().trimmed();
+    ui->stereoModeCombo->setCurrentIndex(0);
+    if(stereomode.isEmpty() == false)
+    {
+        QString str{getNameFromValue(stereoModeList, stereomode)};
+        if(!str.isEmpty())
+        {
+            const int j{ui->stereoModeCombo->findText(str)};
+            if(j > 0) ui->stereoModeCombo->setCurrentIndex(j);
+        }
+    }
+
+    int periodsize{settings.value("period_size").toInt()};
+    ui->periodSizeEdit->clear();
+    if(periodsize >= 64)
+    {
+        ui->periodSizeEdit->insert(QString::number(periodsize));
+        updatePeriodSizeSlider();
+    }
+
+    int periodcount{settings.value("periods").toInt()};
+    ui->periodCountEdit->clear();
+    if(periodcount >= 2)
+    {
+        ui->periodCountEdit->insert(QString::number(periodcount));
+        updatePeriodCountSlider();
+    }
+
+    ui->outputLimiterCheckBox->setCheckState(getCheckState(settings.value("output-limiter")));
+    ui->outputDitherCheckBox->setCheckState(getCheckState(settings.value("dither")));
+
+    QString stereopan{settings.value("stereo-encoding").toString()};
+    ui->stereoEncodingComboBox->setCurrentIndex(0);
+    if(stereopan.isEmpty() == false)
+    {
+        QString str{getNameFromValue(stereoEncList, stereopan)};
+        if(!str.isEmpty())
+        {
+            const int j{ui->stereoEncodingComboBox->findText(str)};
+            if(j > 0) ui->stereoEncodingComboBox->setCurrentIndex(j);
+        }
+    }
+
+    QString ambiformat{settings.value("ambi-format").toString()};
+    ui->ambiFormatComboBox->setCurrentIndex(0);
+    if(ambiformat.isEmpty() == false)
+    {
+        QString str{getNameFromValue(ambiFormatList, ambiformat)};
+        if(!str.isEmpty())
+        {
+            const int j{ui->ambiFormatComboBox->findText(str)};
+            if(j > 0) ui->ambiFormatComboBox->setCurrentIndex(j);
+        }
+    }
+
+    ui->decoderHQModeCheckBox->setChecked(getCheckState(settings.value("decoder/hq-mode")));
+    ui->decoderDistCompCheckBox->setCheckState(getCheckState(settings.value("decoder/distance-comp")));
+    ui->decoderNFEffectsCheckBox->setCheckState(getCheckState(settings.value("decoder/nfc")));
+    double speakerdist{settings.value("decoder/speaker-dist", 1.0).toDouble()};
+    ui->decoderSpeakerDistSpinBox->setValue(speakerdist);
+
+    ui->decoderQuadLineEdit->setText(settings.value("decoder/quad").toString());
+    ui->decoder51LineEdit->setText(settings.value("decoder/surround51").toString());
+    ui->decoder61LineEdit->setText(settings.value("decoder/surround61").toString());
+    ui->decoder71LineEdit->setText(settings.value("decoder/surround71").toString());
+    ui->decoder3D71LineEdit->setText(settings.value("decoder/surround3d71").toString());
+
+    QStringList disabledCpuExts{settings.value("disable-cpu-exts").toStringList()};
+    if(disabledCpuExts.size() == 1)
+        disabledCpuExts = disabledCpuExts[0].split(QChar(','));
+    for(QString &name : disabledCpuExts)
+        name = name.trimmed();
+    ui->enableSSECheckBox->setChecked(!disabledCpuExts.contains("sse", Qt::CaseInsensitive));
+    ui->enableSSE2CheckBox->setChecked(!disabledCpuExts.contains("sse2", Qt::CaseInsensitive));
+    ui->enableSSE3CheckBox->setChecked(!disabledCpuExts.contains("sse3", Qt::CaseInsensitive));
+    ui->enableSSE41CheckBox->setChecked(!disabledCpuExts.contains("sse4.1", Qt::CaseInsensitive));
+    ui->enableNeonCheckBox->setChecked(!disabledCpuExts.contains("neon", Qt::CaseInsensitive));
+
+    QString hrtfmode{settings.value("hrtf-mode").toString().trimmed()};
+    ui->hrtfmodeSlider->setValue(2);
+    ui->hrtfmodeLabel->setText(hrtfModeList[3].name);
+    /* The "basic" mode name is no longer supported. Use "ambi2" instead. */
+    if(hrtfmode == "basic")
+        hrtfmode = "ambi2";
+    for(int i = 0;hrtfModeList[i].name[0];i++)
+    {
+        if(hrtfmode == hrtfModeList[i].value)
+        {
+            ui->hrtfmodeSlider->setValue(i);
+            ui->hrtfmodeLabel->setText(hrtfModeList[i].name);
+            break;
+        }
+    }
+
+    QStringList hrtf_paths{settings.value("hrtf-paths").toStringList()};
+    if(hrtf_paths.size() == 1)
+        hrtf_paths = hrtf_paths[0].split(QChar(','));
+    for(QString &name : hrtf_paths)
+        name = name.trimmed();
+    if(!hrtf_paths.empty() && !hrtf_paths.back().isEmpty())
+        ui->defaultHrtfPathsCheckBox->setCheckState(Qt::Unchecked);
+    else
+    {
+        hrtf_paths.removeAll(QString());
+        ui->defaultHrtfPathsCheckBox->setCheckState(Qt::Checked);
+    }
+    hrtf_paths.removeDuplicates();
+    ui->hrtfFileList->clear();
+    ui->hrtfFileList->addItems(hrtf_paths);
+    updateHrtfRemoveButton();
+
+    ui->preferredHrtfComboBox->clear();
+    ui->preferredHrtfComboBox->addItem("- Any -");
+    if(ui->defaultHrtfPathsCheckBox->isChecked())
+    {
+        QStringList hrtfs{collectHrtfs()};
+        foreach(const QString &name, hrtfs)
+            ui->preferredHrtfComboBox->addItem(name);
+    }
+
+    QString defaulthrtf{settings.value("default-hrtf").toString()};
+    ui->preferredHrtfComboBox->setCurrentIndex(0);
+    if(defaulthrtf.isEmpty() == false)
+    {
+        int i{ui->preferredHrtfComboBox->findText(defaulthrtf)};
+        if(i > 0)
+            ui->preferredHrtfComboBox->setCurrentIndex(i);
+        else
+        {
+            i = ui->preferredHrtfComboBox->count();
+            ui->preferredHrtfComboBox->addItem(defaulthrtf);
+            ui->preferredHrtfComboBox->setCurrentIndex(i);
+        }
+    }
+    ui->preferredHrtfComboBox->adjustSize();
+
+    ui->enabledBackendList->clear();
+    ui->disabledBackendList->clear();
+    QStringList drivers{settings.value("drivers").toStringList()};
+    if(drivers.empty())
+        ui->backendCheckBox->setChecked(true);
+    else
+    {
+        if(drivers.size() == 1)
+            drivers = drivers[0].split(QChar(','));
+        for(QString &name : drivers)
+        {
+            name = name.trimmed();
+            /* Convert "mmdevapi" references to "wasapi" for backwards
+             * compatibility.
+             */
+            if(name == "-mmdevapi")
+                name = "-wasapi";
+            else if(name == "mmdevapi")
+                name = "wasapi";
+        }
+
+        bool lastWasEmpty = false;
+        foreach(const QString &backend, drivers)
+        {
+            lastWasEmpty = backend.isEmpty();
+            if(lastWasEmpty) continue;
+
+            if(!backend.startsWith(QChar('-')))
+                for(int j = 0;backendList[j].backend_name[0];j++)
+                {
+                    if(backend == backendList[j].backend_name)
+                    {
+                        ui->enabledBackendList->addItem(backendList[j].full_string);
+                        break;
+                    }
+                }
+            else if(backend.size() > 1)
+            {
+                QStringRef backendref{backend.rightRef(backend.size()-1)};
+                for(int j = 0;backendList[j].backend_name[0];j++)
+                {
+                    if(backendref == backendList[j].backend_name)
+                    {
+                        ui->disabledBackendList->addItem(backendList[j].full_string);
+                        break;
+                    }
+                }
+            }
+        }
+        ui->backendCheckBox->setChecked(lastWasEmpty);
+    }
+
+    QString defaultreverb{settings.value("default-reverb").toString().toLower()};
+    ui->defaultReverbComboBox->setCurrentIndex(0);
+    if(defaultreverb.isEmpty() == false)
+    {
+        for(int i = 0;i < ui->defaultReverbComboBox->count();i++)
+        {
+            if(defaultreverb.compare(ui->defaultReverbComboBox->itemText(i).toLower()) == 0)
+            {
+                ui->defaultReverbComboBox->setCurrentIndex(i);
+                break;
+            }
+        }
+    }
+
+    QStringList excludefx{settings.value("excludefx").toStringList()};
+    if(excludefx.size() == 1)
+        excludefx = excludefx[0].split(QChar(','));
+    for(QString &name : excludefx)
+        name = name.trimmed();
+    ui->enableEaxReverbCheck->setChecked(!excludefx.contains("eaxreverb", Qt::CaseInsensitive));
+    ui->enableStdReverbCheck->setChecked(!excludefx.contains("reverb", Qt::CaseInsensitive));
+    ui->enableAutowahCheck->setChecked(!excludefx.contains("autowah", Qt::CaseInsensitive));
+    ui->enableChorusCheck->setChecked(!excludefx.contains("chorus", Qt::CaseInsensitive));
+    ui->enableCompressorCheck->setChecked(!excludefx.contains("compressor", Qt::CaseInsensitive));
+    ui->enableDistortionCheck->setChecked(!excludefx.contains("distortion", Qt::CaseInsensitive));
+    ui->enableEchoCheck->setChecked(!excludefx.contains("echo", Qt::CaseInsensitive));
+    ui->enableEqualizerCheck->setChecked(!excludefx.contains("equalizer", Qt::CaseInsensitive));
+    ui->enableFlangerCheck->setChecked(!excludefx.contains("flanger", Qt::CaseInsensitive));
+    ui->enableFrequencyShifterCheck->setChecked(!excludefx.contains("fshifter", Qt::CaseInsensitive));
+    ui->enableModulatorCheck->setChecked(!excludefx.contains("modulator", Qt::CaseInsensitive));
+    ui->enableDedicatedCheck->setChecked(!excludefx.contains("dedicated", Qt::CaseInsensitive));
+    ui->enablePitchShifterCheck->setChecked(!excludefx.contains("pshifter", Qt::CaseInsensitive));
+    ui->enableVocalMorpherCheck->setChecked(!excludefx.contains("vmorpher", Qt::CaseInsensitive));
+    if(ui->enableEaxCheck->isEnabled())
+        ui->enableEaxCheck->setChecked(getCheckState(settings.value("eax/enable")) != Qt::Unchecked);
+
+    ui->pulseAutospawnCheckBox->setCheckState(getCheckState(settings.value("pulse/spawn-server")));
+    ui->pulseAllowMovesCheckBox->setCheckState(getCheckState(settings.value("pulse/allow-moves")));
+    ui->pulseFixRateCheckBox->setCheckState(getCheckState(settings.value("pulse/fix-rate")));
+    ui->pulseAdjLatencyCheckBox->setCheckState(getCheckState(settings.value("pulse/adjust-latency")));
+
+    ui->pwireAssumeAudioCheckBox->setCheckState(getCheckState(settings.value("pipewire/assume-audio")));
+    ui->pwireRtMixCheckBox->setCheckState(getCheckState(settings.value("pipewire/rt-mix")));
+
+    ui->wasapiResamplerCheckBox->setCheckState(getCheckState(settings.value("wasapi/allow-resampler")));
+
+    ui->jackAutospawnCheckBox->setCheckState(getCheckState(settings.value("jack/spawn-server")));
+    ui->jackConnectPortsCheckBox->setCheckState(getCheckState(settings.value("jack/connect-ports")));
+    ui->jackRtMixCheckBox->setCheckState(getCheckState(settings.value("jack/rt-mix")));
+    ui->jackBufferSizeLine->setText(settings.value("jack/buffer-size", QString()).toString());
+    updateJackBufferSizeSlider();
+
+    ui->alsaDefaultDeviceLine->setText(settings.value("alsa/device", QString()).toString());
+    ui->alsaDefaultCaptureLine->setText(settings.value("alsa/capture", QString()).toString());
+    ui->alsaResamplerCheckBox->setCheckState(getCheckState(settings.value("alsa/allow-resampler")));
+    ui->alsaMmapCheckBox->setCheckState(getCheckState(settings.value("alsa/mmap")));
+
+    ui->ossDefaultDeviceLine->setText(settings.value("oss/device", QString()).toString());
+    ui->ossDefaultCaptureLine->setText(settings.value("oss/capture", QString()).toString());
+
+    ui->solarisDefaultDeviceLine->setText(settings.value("solaris/device", QString()).toString());
+
+    ui->waveOutputLine->setText(settings.value("wave/file", QString()).toString());
+    ui->waveBFormatCheckBox->setChecked(settings.value("wave/bformat", false).toBool());
+
+    ui->applyButton->setEnabled(false);
+    ui->closeCancelButton->setText(tr("Close"));
+    mNeedsSave = false;
+}
+
+void MainWindow::saveCurrentConfig()
+{
+    saveConfig(getDefaultConfigName());
+    ui->applyButton->setEnabled(false);
+    ui->closeCancelButton->setText(tr("Close"));
+    mNeedsSave = false;
+    QMessageBox::information(this, tr("Information"),
+        tr("Applications using OpenAL need to be restarted for changes to take effect."));
+}
+
+void MainWindow::saveConfigAsFile()
+{
+    QString fname{QFileDialog::getOpenFileName(this, tr("Select Files"))};
+    if(fname.isEmpty() == false)
+    {
+        saveConfig(fname);
+        ui->applyButton->setEnabled(false);
+        mNeedsSave = false;
+    }
+}
+
+void MainWindow::saveConfig(const QString &fname) const
+{
+    QSettings settings{fname, QSettings::IniFormat};
+
+    /* HACK: Compound any stringlist values into a comma-separated string. */
+    QStringList allkeys{settings.allKeys()};
+    foreach(const QString &key, allkeys)
+    {
+        QStringList vals{settings.value(key).toStringList()};
+        if(vals.size() > 1)
+            settings.setValue(key, vals.join(QChar(',')));
+    }
+
+    settings.setValue("sample-type", getValueFromName(sampleTypeList, ui->sampleFormatCombo->currentText()));
+    settings.setValue("channels", getValueFromName(speakerModeList, ui->channelConfigCombo->currentText()));
+
+    uint rate{ui->sampleRateCombo->currentText().toUInt()};
+    if(rate <= 0)
+        settings.setValue("frequency", QString{});
+    else
+        settings.setValue("frequency", rate);
+
+    settings.setValue("period_size", ui->periodSizeEdit->text());
+    settings.setValue("periods", ui->periodCountEdit->text());
+
+    settings.setValue("sources", ui->srcCountLineEdit->text());
+    settings.setValue("slots", ui->effectSlotLineEdit->text());
+
+    settings.setValue("resampler", resamplerList[ui->resamplerSlider->value()].value);
+
+    settings.setValue("stereo-mode", getValueFromName(stereoModeList, ui->stereoModeCombo->currentText()));
+    settings.setValue("stereo-encoding", getValueFromName(stereoEncList, ui->stereoEncodingComboBox->currentText()));
+    settings.setValue("ambi-format", getValueFromName(ambiFormatList, ui->ambiFormatComboBox->currentText()));
+
+    settings.setValue("output-limiter", getCheckValue(ui->outputLimiterCheckBox));
+    settings.setValue("dither", getCheckValue(ui->outputDitherCheckBox));
+
+    settings.setValue("decoder/hq-mode", getCheckValue(ui->decoderHQModeCheckBox));
+    settings.setValue("decoder/distance-comp", getCheckValue(ui->decoderDistCompCheckBox));
+    settings.setValue("decoder/nfc", getCheckValue(ui->decoderNFEffectsCheckBox));
+    double speakerdist{ui->decoderSpeakerDistSpinBox->value()};
+    settings.setValue("decoder/speaker-dist",
+        (speakerdist != 1.0) ? QString::number(speakerdist) : QString{}
+    );
+
+    settings.setValue("decoder/quad", ui->decoderQuadLineEdit->text());
+    settings.setValue("decoder/surround51", ui->decoder51LineEdit->text());
+    settings.setValue("decoder/surround61", ui->decoder61LineEdit->text());
+    settings.setValue("decoder/surround71", ui->decoder71LineEdit->text());
+    settings.setValue("decoder/surround3d71", ui->decoder3D71LineEdit->text());
+
+    QStringList strlist;
+    if(!ui->enableSSECheckBox->isChecked())
+        strlist.append("sse");
+    if(!ui->enableSSE2CheckBox->isChecked())
+        strlist.append("sse2");
+    if(!ui->enableSSE3CheckBox->isChecked())
+        strlist.append("sse3");
+    if(!ui->enableSSE41CheckBox->isChecked())
+        strlist.append("sse4.1");
+    if(!ui->enableNeonCheckBox->isChecked())
+        strlist.append("neon");
+    settings.setValue("disable-cpu-exts", strlist.join(QChar(',')));
+
+    settings.setValue("hrtf-mode", hrtfModeList[ui->hrtfmodeSlider->value()].value);
+
+    if(ui->preferredHrtfComboBox->currentIndex() == 0)
+        settings.setValue("default-hrtf", QString{});
+    else
+    {
+        QString str{ui->preferredHrtfComboBox->currentText()};
+        settings.setValue("default-hrtf", str);
+    }
+
+    strlist.clear();
+    strlist.reserve(ui->hrtfFileList->count());
+    for(int i = 0;i < ui->hrtfFileList->count();i++)
+        strlist.append(ui->hrtfFileList->item(i)->text());
+    if(!strlist.empty() && ui->defaultHrtfPathsCheckBox->isChecked())
+        strlist.append(QString{});
+    settings.setValue("hrtf-paths", strlist.join(QChar{','}));
+
+    strlist.clear();
+    for(int i = 0;i < ui->enabledBackendList->count();i++)
+    {
+        QString label{ui->enabledBackendList->item(i)->text()};
+        for(int j = 0;backendList[j].backend_name[0];j++)
+        {
+            if(label == backendList[j].full_string)
+            {
+                strlist.append(backendList[j].backend_name);
+                break;
+            }
+        }
+    }
+    for(int i = 0;i < ui->disabledBackendList->count();i++)
+    {
+        QString label{ui->disabledBackendList->item(i)->text()};
+        for(int j = 0;backendList[j].backend_name[0];j++)
+        {
+            if(label == backendList[j].full_string)
+            {
+                strlist.append(QChar{'-'}+QString{backendList[j].backend_name});
+                break;
+            }
+        }
+    }
+    if(strlist.empty() && !ui->backendCheckBox->isChecked())
+        strlist.append("-all");
+    else if(ui->backendCheckBox->isChecked())
+        strlist.append(QString{});
+    settings.setValue("drivers", strlist.join(QChar(',')));
+
+    // TODO: Remove check when we can properly match global values.
+    if(ui->defaultReverbComboBox->currentIndex() == 0)
+        settings.setValue("default-reverb", QString{});
+    else
+    {
+        QString str{ui->defaultReverbComboBox->currentText().toLower()};
+        settings.setValue("default-reverb", str);
+    }
+
+    strlist.clear();
+    if(!ui->enableEaxReverbCheck->isChecked())
+        strlist.append("eaxreverb");
+    if(!ui->enableStdReverbCheck->isChecked())
+        strlist.append("reverb");
+    if(!ui->enableAutowahCheck->isChecked())
+        strlist.append("autowah");
+    if(!ui->enableChorusCheck->isChecked())
+        strlist.append("chorus");
+    if(!ui->enableDistortionCheck->isChecked())
+        strlist.append("distortion");
+    if(!ui->enableCompressorCheck->isChecked())
+        strlist.append("compressor");
+    if(!ui->enableEchoCheck->isChecked())
+        strlist.append("echo");
+    if(!ui->enableEqualizerCheck->isChecked())
+        strlist.append("equalizer");
+    if(!ui->enableFlangerCheck->isChecked())
+        strlist.append("flanger");
+    if(!ui->enableFrequencyShifterCheck->isChecked())
+        strlist.append("fshifter");
+    if(!ui->enableModulatorCheck->isChecked())
+        strlist.append("modulator");
+    if(!ui->enableDedicatedCheck->isChecked())
+        strlist.append("dedicated");
+    if(!ui->enablePitchShifterCheck->isChecked())
+        strlist.append("pshifter");
+    if(!ui->enableVocalMorpherCheck->isChecked())
+        strlist.append("vmorpher");
+    settings.setValue("excludefx", strlist.join(QChar{','}));
+    settings.setValue("eax/enable",
+        (!ui->enableEaxCheck->isEnabled() || ui->enableEaxCheck->isChecked())
+        ? QString{/*"true"*/} : QString{"false"});
+
+    settings.setValue("pipewire/assume-audio", getCheckValue(ui->pwireAssumeAudioCheckBox));
+    settings.setValue("pipewire/rt-mix", getCheckValue(ui->pwireRtMixCheckBox));
+
+    settings.setValue("wasapi/allow-resampler", getCheckValue(ui->wasapiResamplerCheckBox));
+
+    settings.setValue("pulse/spawn-server", getCheckValue(ui->pulseAutospawnCheckBox));
+    settings.setValue("pulse/allow-moves", getCheckValue(ui->pulseAllowMovesCheckBox));
+    settings.setValue("pulse/fix-rate", getCheckValue(ui->pulseFixRateCheckBox));
+    settings.setValue("pulse/adjust-latency", getCheckValue(ui->pulseAdjLatencyCheckBox));
+
+    settings.setValue("jack/spawn-server", getCheckValue(ui->jackAutospawnCheckBox));
+    settings.setValue("jack/connect-ports", getCheckValue(ui->jackConnectPortsCheckBox));
+    settings.setValue("jack/rt-mix", getCheckValue(ui->jackRtMixCheckBox));
+    settings.setValue("jack/buffer-size", ui->jackBufferSizeLine->text());
+
+    settings.setValue("alsa/device", ui->alsaDefaultDeviceLine->text());
+    settings.setValue("alsa/capture", ui->alsaDefaultCaptureLine->text());
+    settings.setValue("alsa/allow-resampler", getCheckValue(ui->alsaResamplerCheckBox));
+    settings.setValue("alsa/mmap", getCheckValue(ui->alsaMmapCheckBox));
+
+    settings.setValue("oss/device", ui->ossDefaultDeviceLine->text());
+    settings.setValue("oss/capture", ui->ossDefaultCaptureLine->text());
+
+    settings.setValue("solaris/device", ui->solarisDefaultDeviceLine->text());
+
+    settings.setValue("wave/file", ui->waveOutputLine->text());
+    settings.setValue("wave/bformat",
+        ui->waveBFormatCheckBox->isChecked() ? QString{"true"} : QString{/*"false"*/}
+    );
+
+    /* Remove empty keys
+     * FIXME: Should only remove keys whose value matches the globally-specified value.
+     */
+    allkeys = settings.allKeys();
+    foreach(const QString &key, allkeys)
+    {
+        QString str{settings.value(key).toString()};
+        if(str == QString{})
+            settings.remove(key);
+    }
+}
+
+
+void MainWindow::enableApplyButton()
+{
+    if(!mNeedsSave)
+        ui->applyButton->setEnabled(true);
+    mNeedsSave = true;
+    ui->closeCancelButton->setText(tr("Cancel"));
+}
+
+
+void MainWindow::updateResamplerLabel(int num)
+{
+    ui->resamplerLabel->setText(resamplerList[num].name);
+    enableApplyButton();
+}
+
+
+void MainWindow::updatePeriodSizeEdit(int size)
+{
+    ui->periodSizeEdit->clear();
+    if(size >= 64)
+        ui->periodSizeEdit->insert(QString::number(size));
+    enableApplyButton();
+}
+
+void MainWindow::updatePeriodSizeSlider()
+{
+    int pos = ui->periodSizeEdit->text().toInt();
+    if(pos >= 64)
+    {
+        if(pos > 8192)
+            pos = 8192;
+        ui->periodSizeSlider->setSliderPosition(pos);
+    }
+    enableApplyButton();
+}
+
+void MainWindow::updatePeriodCountEdit(int count)
+{
+    ui->periodCountEdit->clear();
+    if(count >= 2)
+        ui->periodCountEdit->insert(QString::number(count));
+    enableApplyButton();
+}
+
+void MainWindow::updatePeriodCountSlider()
+{
+    int pos = ui->periodCountEdit->text().toInt();
+    if(pos < 2)
+        pos = 0;
+    else if(pos > 16)
+        pos = 16;
+    ui->periodCountSlider->setSliderPosition(pos);
+    enableApplyButton();
+}
+
+
+void MainWindow::selectQuadDecoderFile()
+{ selectDecoderFile(ui->decoderQuadLineEdit, "Select Quadraphonic Decoder");}
+void MainWindow::select51DecoderFile()
+{ selectDecoderFile(ui->decoder51LineEdit, "Select 5.1 Surround Decoder");}
+void MainWindow::select61DecoderFile()
+{ selectDecoderFile(ui->decoder61LineEdit, "Select 6.1 Surround Decoder");}
+void MainWindow::select71DecoderFile()
+{ selectDecoderFile(ui->decoder71LineEdit, "Select 7.1 Surround Decoder");}
+void MainWindow::select3D71DecoderFile()
+{ selectDecoderFile(ui->decoder3D71LineEdit, "Select 3D7.1 Surround Decoder");}
+void MainWindow::selectDecoderFile(QLineEdit *line, const char *caption)
+{
+    QString dir{line->text()};
+    if(dir.isEmpty() || QDir::isRelativePath(dir))
+    {
+        QStringList paths{getAllDataPaths("/openal/presets")};
+        while(!paths.isEmpty())
+        {
+            if(QDir{paths.last()}.exists())
+            {
+                dir = paths.last();
+                break;
+            }
+            paths.removeLast();
+        }
+    }
+    QString fname{QFileDialog::getOpenFileName(this, tr(caption),
+        dir, tr("AmbDec Files (*.ambdec);;All Files (*.*)"))};
+    if(!fname.isEmpty())
+    {
+        line->setText(fname);
+        enableApplyButton();
+    }
+}
+
+
+void MainWindow::updateJackBufferSizeEdit(int size)
+{
+    ui->jackBufferSizeLine->clear();
+    if(size > 0)
+        ui->jackBufferSizeLine->insert(QString::number(1<<size));
+    enableApplyButton();
+}
+
+void MainWindow::updateJackBufferSizeSlider()
+{
+    int value{ui->jackBufferSizeLine->text().toInt()};
+    auto pos = static_cast<int>(floor(log2(value) + 0.5));
+    ui->jackBufferSizeSlider->setSliderPosition(pos);
+    enableApplyButton();
+}
+
+
+void MainWindow::updateHrtfModeLabel(int num)
+{
+    ui->hrtfmodeLabel->setText(hrtfModeList[num].name);
+    enableApplyButton();
+}
+
+
+void MainWindow::addHrtfFile()
+{
+    QString path{QFileDialog::getExistingDirectory(this, tr("Select HRTF Path"))};
+    if(path.isEmpty() == false && !getAllDataPaths("/openal/hrtf").contains(path))
+    {
+        ui->hrtfFileList->addItem(path);
+        enableApplyButton();
+    }
+}
+
+void MainWindow::removeHrtfFile()
+{
+    QList<QListWidgetItem*> selected{ui->hrtfFileList->selectedItems()};
+    if(!selected.isEmpty())
+    {
+        foreach(QListWidgetItem *item, selected)
+            delete item;
+        enableApplyButton();
+    }
+}
+
+void MainWindow::updateHrtfRemoveButton()
+{
+    ui->hrtfRemoveButton->setEnabled(!ui->hrtfFileList->selectedItems().empty());
+}
+
+void MainWindow::showEnabledBackendMenu(QPoint pt)
+{
+    QHash<QAction*,QString> actionMap;
+
+    pt = ui->enabledBackendList->mapToGlobal(pt);
+
+    QMenu ctxmenu;
+    QAction *removeAction{ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove")};
+    if(ui->enabledBackendList->selectedItems().empty())
+        removeAction->setEnabled(false);
+    ctxmenu.addSeparator();
+    for(size_t i = 0;backendList[i].backend_name[0];i++)
+    {
+        QString backend{backendList[i].full_string};
+        QAction *action{ctxmenu.addAction(QString("Add ")+backend)};
+        actionMap[action] = backend;
+        if(!ui->enabledBackendList->findItems(backend, Qt::MatchFixedString).empty() ||
+           !ui->disabledBackendList->findItems(backend, Qt::MatchFixedString).empty())
+            action->setEnabled(false);
+    }
+
+    QAction *gotAction{ctxmenu.exec(pt)};
+    if(gotAction == removeAction)
+    {
+        QList<QListWidgetItem*> selected{ui->enabledBackendList->selectedItems()};
+        foreach(QListWidgetItem *item, selected)
+            delete item;
+        enableApplyButton();
+    }
+    else if(gotAction != nullptr)
+    {
+        auto iter = actionMap.constFind(gotAction);
+        if(iter != actionMap.cend())
+            ui->enabledBackendList->addItem(iter.value());
+        enableApplyButton();
+    }
+}
+
+void MainWindow::showDisabledBackendMenu(QPoint pt)
+{
+    QHash<QAction*,QString> actionMap;
+
+    pt = ui->disabledBackendList->mapToGlobal(pt);
+
+    QMenu ctxmenu;
+    QAction *removeAction{ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove")};
+    if(ui->disabledBackendList->selectedItems().empty())
+        removeAction->setEnabled(false);
+    ctxmenu.addSeparator();
+    for(size_t i = 0;backendList[i].backend_name[0];i++)
+    {
+        QString backend{backendList[i].full_string};
+        QAction *action{ctxmenu.addAction(QString("Add ")+backend)};
+        actionMap[action] = backend;
+        if(!ui->disabledBackendList->findItems(backend, Qt::MatchFixedString).empty() ||
+           !ui->enabledBackendList->findItems(backend, Qt::MatchFixedString).empty())
+            action->setEnabled(false);
+    }
+
+    QAction *gotAction{ctxmenu.exec(pt)};
+    if(gotAction == removeAction)
+    {
+        QList<QListWidgetItem*> selected{ui->disabledBackendList->selectedItems()};
+        foreach(QListWidgetItem *item, selected)
+            delete item;
+        enableApplyButton();
+    }
+    else if(gotAction != nullptr)
+    {
+        auto iter = actionMap.constFind(gotAction);
+        if(iter != actionMap.cend())
+            ui->disabledBackendList->addItem(iter.value());
+        enableApplyButton();
+    }
+}
+
+void MainWindow::selectOSSPlayback()
+{
+    QString current{ui->ossDefaultDeviceLine->text()};
+    if(current.isEmpty()) current = ui->ossDefaultDeviceLine->placeholderText();
+    QString fname{QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current)};
+    if(!fname.isEmpty())
+    {
+        ui->ossDefaultDeviceLine->setText(fname);
+        enableApplyButton();
+    }
+}
+
+void MainWindow::selectOSSCapture()
+{
+    QString current{ui->ossDefaultCaptureLine->text()};
+    if(current.isEmpty()) current = ui->ossDefaultCaptureLine->placeholderText();
+    QString fname{QFileDialog::getOpenFileName(this, tr("Select Capture Device"), current)};
+    if(!fname.isEmpty())
+    {
+        ui->ossDefaultCaptureLine->setText(fname);
+        enableApplyButton();
+    }
+}
+
+void MainWindow::selectSolarisPlayback()
+{
+    QString current{ui->solarisDefaultDeviceLine->text()};
+    if(current.isEmpty()) current = ui->solarisDefaultDeviceLine->placeholderText();
+    QString fname{QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current)};
+    if(!fname.isEmpty())
+    {
+        ui->solarisDefaultDeviceLine->setText(fname);
+        enableApplyButton();
+    }
+}
+
+void MainWindow::selectWaveOutput()
+{
+    QString fname{QFileDialog::getSaveFileName(this, tr("Select Wave File Output"),
+        ui->waveOutputLine->text(), tr("Wave Files (*.wav *.amb);;All Files (*.*)"))};
+    if(!fname.isEmpty())
+    {
+        ui->waveOutputLine->setText(fname);
+        enableApplyButton();
+    }
+}
diff --git a/utils/alsoft-config/mainwindow.h b/utils/alsoft-config/mainwindow.h
new file mode 100644 (file)
index 0000000..f7af8ea
--- /dev/null
@@ -0,0 +1,86 @@
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QListWidget>
+
+namespace Ui {
+class MainWindow;
+}
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    explicit MainWindow(QWidget *parent = 0);
+    ~MainWindow();
+
+private slots:
+    void cancelCloseAction();
+
+    void saveCurrentConfig();
+
+    void saveConfigAsFile();
+    void loadConfigFromFile();
+
+    void showAboutPage();
+
+    void enableApplyButton();
+
+    void updateResamplerLabel(int num);
+
+    void updatePeriodSizeEdit(int size);
+    void updatePeriodSizeSlider();
+    void updatePeriodCountEdit(int size);
+    void updatePeriodCountSlider();
+
+    void selectQuadDecoderFile();
+    void select51DecoderFile();
+    void select61DecoderFile();
+    void select71DecoderFile();
+    void select3D71DecoderFile();
+
+    void updateJackBufferSizeEdit(int size);
+    void updateJackBufferSizeSlider();
+
+    void updateHrtfModeLabel(int num);
+    void addHrtfFile();
+    void removeHrtfFile();
+
+    void updateHrtfRemoveButton();
+
+    void showEnabledBackendMenu(QPoint pt);
+    void showDisabledBackendMenu(QPoint pt);
+
+    void selectOSSPlayback();
+    void selectOSSCapture();
+
+    void selectSolarisPlayback();
+
+    void selectWaveOutput();
+
+private:
+    Ui::MainWindow *ui;
+
+    QValidator *mPeriodSizeValidator;
+    QValidator *mPeriodCountValidator;
+    QValidator *mSourceCountValidator;
+    QValidator *mEffectSlotValidator;
+    QValidator *mSourceSendValidator;
+    QValidator *mSampleRateValidator;
+    QValidator *mJackBufferValidator;
+
+    bool mNeedsSave;
+
+    void closeEvent(QCloseEvent *event);
+
+    void selectDecoderFile(QLineEdit *line, const char *name);
+
+    QStringList collectHrtfs();
+
+    void loadConfig(const QString &fname);
+    void saveConfig(const QString &fname) const;
+};
+
+#endif // MAINWINDOW_H
diff --git a/utils/alsoft-config/mainwindow.ui b/utils/alsoft-config/mainwindow.ui
new file mode 100644 (file)
index 0000000..b959cf7
--- /dev/null
@@ -0,0 +1,2693 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>564</width>
+    <height>469</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>564</width>
+    <height>460</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>OpenAL Soft Configuration</string>
+  </property>
+  <property name="windowIcon">
+   <iconset theme="preferences-desktop-sound">
+    <normaloff>.</normaloff>.</iconset>
+  </property>
+  <widget class="QWidget" name="centralWidget">
+   <widget class="QPushButton" name="applyButton">
+    <property name="geometry">
+     <rect>
+      <x>470</x>
+      <y>405</y>
+      <width>81</width>
+      <height>31</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Apply</string>
+    </property>
+    <property name="icon">
+     <iconset theme="dialog-ok-apply">
+      <normaloff>.</normaloff>.</iconset>
+    </property>
+   </widget>
+   <widget class="QTabWidget" name="tabWidget">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>0</y>
+      <width>541</width>
+      <height>401</height>
+     </rect>
+    </property>
+    <property name="currentIndex">
+     <number>0</number>
+    </property>
+    <widget class="QWidget" name="tab_3">
+     <attribute name="title">
+      <string>Playback</string>
+     </attribute>
+     <widget class="QComboBox" name="sampleFormatCombo">
+      <property name="geometry">
+       <rect>
+        <x>110</x>
+        <y>50</y>
+        <width>80</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>The output sample type. Currently, all mixing is done with 32-bit
+float and converted to the output sample type as needed.</string>
+      </property>
+      <property name="sizeAdjustPolicy">
+       <enum>QComboBox::AdjustToContents</enum>
+      </property>
+     </widget>
+     <widget class="QLabel" name="label_5">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>50</y>
+        <width>101</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Sample Format:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QLabel" name="label_6">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>20</y>
+        <width>101</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Channels:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QComboBox" name="channelConfigCombo">
+      <property name="geometry">
+       <rect>
+        <x>110</x>
+        <y>20</y>
+        <width>80</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>The default output channel configuration. Note that not all
+backends can properly detect the channel configuration and
+may default to stereo output.</string>
+      </property>
+      <property name="sizeAdjustPolicy">
+       <enum>QComboBox::AdjustToContents</enum>
+      </property>
+     </widget>
+     <widget class="QComboBox" name="sampleRateCombo">
+      <property name="geometry">
+       <rect>
+        <x>380</x>
+        <y>20</y>
+        <width>100</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>The playback/mixing sample rate.</string>
+      </property>
+      <property name="editable">
+       <bool>true</bool>
+      </property>
+      <property name="insertPolicy">
+       <enum>QComboBox::NoInsert</enum>
+      </property>
+      <property name="sizeAdjustPolicy">
+       <enum>QComboBox::AdjustToContents</enum>
+      </property>
+      <item>
+       <property name="text">
+        <string>Autodetect</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>8000</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>11025</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>16000</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>22050</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>32000</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>44100</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>48000</string>
+       </property>
+      </item>
+     </widget>
+     <widget class="QLabel" name="label_7">
+      <property name="geometry">
+       <rect>
+        <x>290</x>
+        <y>20</y>
+        <width>81</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Sample Rate:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QLabel" name="label_14">
+      <property name="geometry">
+       <rect>
+        <x>290</x>
+        <y>50</y>
+        <width>81</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Stereo Mode:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QComboBox" name="stereoModeCombo">
+      <property name="geometry">
+       <rect>
+        <x>380</x>
+        <y>50</y>
+        <width>101</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>How to treat stereo output. As headphones, HRTF or crossfeed
+filters may be used to improve binaural quality, which may not
+otherwise be suitable for speakers.</string>
+      </property>
+     </widget>
+     <widget class="QGroupBox" name="groupBox">
+      <property name="geometry">
+       <rect>
+        <x>-11</x>
+        <y>180</y>
+        <width>551</width>
+        <height>201</height>
+       </rect>
+      </property>
+      <property name="title">
+       <string>Advanced Settings</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignCenter</set>
+      </property>
+      <widget class="QGroupBox" name="groupBox_3">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>30</y>
+         <width>511</width>
+         <height>81</height>
+        </rect>
+       </property>
+       <property name="title">
+        <string>Buffer Metrics</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+       <widget class="QWidget" name="widget" native="true">
+        <property name="geometry">
+         <rect>
+          <x>260</x>
+          <y>20</y>
+          <width>241</width>
+          <height>51</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>The number of update periods. Higher values create a larger
+mix ahead, which helps protect against skips when the CPU is
+under load, but increases the delay between a sound getting
+mixed and being heard.</string>
+        </property>
+        <widget class="QLabel" name="label_11">
+         <property name="geometry">
+          <rect>
+           <x>60</x>
+           <y>0</y>
+           <width>161</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="text">
+          <string>Period Count</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignCenter</set>
+         </property>
+        </widget>
+        <widget class="QSlider" name="periodCountSlider">
+         <property name="geometry">
+          <rect>
+           <x>99</x>
+           <y>20</y>
+           <width>141</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="minimum">
+          <number>1</number>
+         </property>
+         <property name="maximum">
+          <number>16</number>
+         </property>
+         <property name="singleStep">
+          <number>1</number>
+         </property>
+         <property name="pageStep">
+          <number>2</number>
+         </property>
+         <property name="value">
+          <number>1</number>
+         </property>
+         <property name="tracking">
+          <bool>true</bool>
+         </property>
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="tickPosition">
+          <enum>QSlider::TicksBelow</enum>
+         </property>
+         <property name="tickInterval">
+          <number>1</number>
+         </property>
+        </widget>
+        <widget class="QLineEdit" name="periodCountEdit">
+         <property name="geometry">
+          <rect>
+           <x>40</x>
+           <y>20</y>
+           <width>51</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="placeholderText">
+          <string>3</string>
+         </property>
+        </widget>
+       </widget>
+       <widget class="QWidget" name="widget_2" native="true">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>20</y>
+          <width>241</width>
+          <height>51</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>The update period size, in sample frames. This is the number of
+frames needed for each mixing update.</string>
+        </property>
+        <widget class="QSlider" name="periodSizeSlider">
+         <property name="geometry">
+          <rect>
+           <x>60</x>
+           <y>20</y>
+           <width>191</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="minimum">
+          <number>63</number>
+         </property>
+         <property name="maximum">
+          <number>8192</number>
+         </property>
+         <property name="singleStep">
+          <number>1</number>
+         </property>
+         <property name="pageStep">
+          <number>1024</number>
+         </property>
+         <property name="value">
+          <number>63</number>
+         </property>
+         <property name="tracking">
+          <bool>true</bool>
+         </property>
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="tickPosition">
+          <enum>QSlider::TicksBelow</enum>
+         </property>
+         <property name="tickInterval">
+          <number>512</number>
+         </property>
+        </widget>
+        <widget class="QLabel" name="label_10">
+         <property name="geometry">
+          <rect>
+           <x>10</x>
+           <y>0</y>
+           <width>201</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="text">
+          <string>Period Samples</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignCenter</set>
+         </property>
+        </widget>
+        <widget class="QLineEdit" name="periodSizeEdit">
+         <property name="geometry">
+          <rect>
+           <x>0</x>
+           <y>20</y>
+           <width>51</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="placeholderText">
+          <string>20ms</string>
+         </property>
+        </widget>
+       </widget>
+      </widget>
+      <widget class="QComboBox" name="stereoEncodingComboBox">
+       <property name="geometry">
+        <rect>
+         <x>130</x>
+         <y>120</y>
+         <width>111</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="toolTip">
+        <string>Basic uses standard amplitude panning (aka
+pair-wise, stereo pair, etc).
+
+UHJ creates a stereo-compatible two-channel
+UHJ mix, which encodes some surround sound
+information into stereo output that can be
+decoded with a surround sound receiver.
+
+Binaural applies HRTF filters to create an
+illusion of 3D placement with headphones.</string>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_19">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>120</y>
+         <width>101</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Stereo Encoding:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_30">
+       <property name="geometry">
+        <rect>
+         <x>260</x>
+         <y>120</y>
+         <width>121</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Ambisonic Format:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QComboBox" name="ambiFormatComboBox">
+       <property name="geometry">
+        <rect>
+         <x>390</x>
+         <y>120</y>
+         <width>131</width>
+         <height>31</height>
+        </rect>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="outputLimiterCheckBox">
+       <property name="geometry">
+        <rect>
+         <x>30</x>
+         <y>160</y>
+         <width>231</width>
+         <height>20</height>
+        </rect>
+       </property>
+       <property name="toolTip">
+        <string>Applies a gain limiter on the final mixed output. This reduces the
+volume when the output samples would otherwise be clamped,
+avoiding excessive clipping noise.</string>
+       </property>
+       <property name="text">
+        <string>Enable Gain Limiter</string>
+       </property>
+       <property name="tristate">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="outputDitherCheckBox">
+       <property name="geometry">
+        <rect>
+         <x>270</x>
+         <y>160</y>
+         <width>261</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="toolTip">
+        <string>Applies dithering on the final mix for 8- and 16-bit output.
+This replaces the distortion created by nearest-value
+quantization with low-level whitenoise.</string>
+       </property>
+       <property name="text">
+        <string>Enable Dithering</string>
+       </property>
+       <property name="tristate">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </widget>
+     <widget class="QGroupBox" name="groupBox_4">
+      <property name="geometry">
+       <rect>
+        <x>60</x>
+        <y>90</y>
+        <width>421</width>
+        <height>81</height>
+       </rect>
+      </property>
+      <property name="title">
+       <string>Resampler Quality</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignCenter</set>
+      </property>
+      <widget class="QLabel" name="resamplerLabel">
+       <property name="geometry">
+        <rect>
+         <x>50</x>
+         <y>50</y>
+         <width>321</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Default</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+      <widget class="QSlider" name="resamplerSlider">
+       <property name="geometry">
+        <rect>
+         <x>80</x>
+         <y>30</y>
+         <width>251</width>
+         <height>23</height>
+        </rect>
+       </property>
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_9">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>30</y>
+         <width>51</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Speed</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_15">
+       <property name="geometry">
+        <rect>
+         <x>340</x>
+         <y>30</y>
+         <width>51</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Quality</string>
+       </property>
+      </widget>
+     </widget>
+    </widget>
+    <widget class="QWidget" name="tab_6">
+     <attribute name="title">
+      <string>Renderer</string>
+     </attribute>
+     <widget class="QCheckBox" name="decoderHQModeCheckBox">
+      <property name="geometry">
+       <rect>
+        <x>30</x>
+        <y>20</y>
+        <width>181</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>Enables high-quality ambisonic rendering. This mode is
+capable of frequency-dependent processing, creating a
+better reproduction of 3D sound rendering over
+surround sound speakers.</string>
+      </property>
+      <property name="layoutDirection">
+       <enum>Qt::RightToLeft</enum>
+      </property>
+      <property name="text">
+       <string>High Quality Mode:</string>
+      </property>
+      <property name="tristate">
+       <bool>true</bool>
+      </property>
+     </widget>
+     <widget class="QCheckBox" name="decoderDistCompCheckBox">
+      <property name="geometry">
+       <rect>
+        <x>30</x>
+        <y>50</y>
+        <width>181</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>This applies the necessary delays and attenuation
+to make the speakers behave as though they are
+all equidistant, which is important for proper
+playback of 3D sound rendering. Requires the
+proper distances to be specified in the decoder
+configuration file.</string>
+      </property>
+      <property name="layoutDirection">
+       <enum>Qt::RightToLeft</enum>
+      </property>
+      <property name="text">
+       <string>Distance Compensation:</string>
+      </property>
+      <property name="tristate">
+       <bool>true</bool>
+      </property>
+     </widget>
+     <widget class="QGroupBox" name="groupBox_8">
+      <property name="geometry">
+       <rect>
+        <x>-10</x>
+        <y>140</y>
+        <width>551</width>
+        <height>231</height>
+       </rect>
+      </property>
+      <property name="title">
+       <string>Decoder Configurations</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignCenter</set>
+      </property>
+      <widget class="QLineEdit" name="decoderQuadLineEdit">
+       <property name="geometry">
+        <rect>
+         <x>130</x>
+         <y>30</y>
+         <width>301</width>
+         <height>25</height>
+        </rect>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_25">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>30</y>
+         <width>101</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Quadraphonic:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QPushButton" name="decoderQuadButton">
+       <property name="geometry">
+        <rect>
+         <x>440</x>
+         <y>30</y>
+         <width>91</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Browse...</string>
+       </property>
+      </widget>
+      <widget class="QLineEdit" name="decoder51LineEdit">
+       <property name="geometry">
+        <rect>
+         <x>130</x>
+         <y>70</y>
+         <width>301</width>
+         <height>25</height>
+        </rect>
+       </property>
+      </widget>
+      <widget class="QPushButton" name="decoder51Button">
+       <property name="geometry">
+        <rect>
+         <x>440</x>
+         <y>70</y>
+         <width>91</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Browse...</string>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_26">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>70</y>
+         <width>101</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>5.1 Surround:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_28">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>110</y>
+         <width>101</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>6.1 Surround:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QLineEdit" name="decoder61LineEdit">
+       <property name="geometry">
+        <rect>
+         <x>130</x>
+         <y>110</y>
+         <width>301</width>
+         <height>25</height>
+        </rect>
+       </property>
+      </widget>
+      <widget class="QPushButton" name="decoder61Button">
+       <property name="geometry">
+        <rect>
+         <x>440</x>
+         <y>110</y>
+         <width>91</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Browse...</string>
+       </property>
+      </widget>
+      <widget class="QPushButton" name="decoder71Button">
+       <property name="geometry">
+        <rect>
+         <x>440</x>
+         <y>150</y>
+         <width>91</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Browse...</string>
+       </property>
+      </widget>
+      <widget class="QLineEdit" name="decoder71LineEdit">
+       <property name="geometry">
+        <rect>
+         <x>130</x>
+         <y>150</y>
+         <width>301</width>
+         <height>25</height>
+        </rect>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_29">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>150</y>
+         <width>101</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>7.1 Surround:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_33">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>190</y>
+         <width>101</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>3D7.1 Surround:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QLineEdit" name="decoder3D71LineEdit">
+       <property name="geometry">
+        <rect>
+         <x>130</x>
+         <y>190</y>
+         <width>301</width>
+         <height>25</height>
+        </rect>
+       </property>
+      </widget>
+      <widget class="QPushButton" name="decoder3D71Button">
+       <property name="geometry">
+        <rect>
+         <x>440</x>
+         <y>190</y>
+         <width>91</width>
+         <height>25</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Browse...</string>
+       </property>
+      </widget>
+     </widget>
+     <widget class="QCheckBox" name="decoderNFEffectsCheckBox">
+      <property name="geometry">
+       <rect>
+        <x>30</x>
+        <y>80</y>
+        <width>181</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>Simulates and compensates for low-frequency effects
+caused by the curvature of nearby sound-waves, which
+creates a more realistic perception of sound distance.
+Note that the effect may be stronger or weaker than
+intended if the application doesn't use or specify an
+appropriate unit scale, or if incorrect speaker distances
+are set in the decoder configuration file.</string>
+      </property>
+      <property name="layoutDirection">
+       <enum>Qt::RightToLeft</enum>
+      </property>
+      <property name="text">
+       <string>Near-Field Effects:</string>
+      </property>
+      <property name="tristate">
+       <bool>true</bool>
+      </property>
+     </widget>
+     <widget class="QWidget" name="widget_3" native="true">
+      <property name="geometry">
+       <rect>
+        <x>30</x>
+        <y>110</y>
+        <width>471</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>Specifies the speaker distance in meters, used by the near-field control
+filters with surround sound output. For ambisonic output modes, this value
+is the basis for the NFC-HOA Reference Delay parameter (calculated as
+delay_seconds = speaker_dist/343.3). This value is not used when a decoder
+configuration is set for the output mode (since they specify the per-speaker
+distances, overriding this setting), or when the NFC filters are off.</string>
+      </property>
+      <widget class="QLabel" name="label_27">
+       <property name="geometry">
+        <rect>
+         <x>45</x>
+         <y>0</y>
+         <width>111</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Speaker Distance:</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QDoubleSpinBox" name="decoderSpeakerDistSpinBox">
+       <property name="geometry">
+        <rect>
+         <x>165</x>
+         <y>0</y>
+         <width>101</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="suffix">
+        <string> meters</string>
+       </property>
+       <property name="decimals">
+        <number>2</number>
+       </property>
+       <property name="minimum">
+        <double>0.100000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10.000000000000000</double>
+       </property>
+       <property name="singleStep">
+        <double>0.010000000000000</double>
+       </property>
+       <property name="value">
+        <double>1.000000000000000</double>
+       </property>
+      </widget>
+     </widget>
+    </widget>
+    <widget class="QWidget" name="tab_5">
+     <attribute name="title">
+      <string>HRTF</string>
+     </attribute>
+     <widget class="QGroupBox" name="groupBox_2">
+      <property name="geometry">
+       <rect>
+        <x>-10</x>
+        <y>200</y>
+        <width>551</width>
+        <height>181</height>
+       </rect>
+      </property>
+      <property name="title">
+       <string>Advanced Settings</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignCenter</set>
+      </property>
+      <property name="checkable">
+       <bool>false</bool>
+      </property>
+      <property name="checked">
+       <bool>false</bool>
+      </property>
+      <widget class="QGroupBox" name="groupBox_6">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>30</y>
+         <width>511</width>
+         <height>141</height>
+        </rect>
+       </property>
+       <property name="title">
+        <string>HRTF Profile Paths</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+       <widget class="QListWidget" name="hrtfFileList">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>20</y>
+          <width>391</width>
+          <height>81</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>A list of additional paths containing HRTF data sets.</string>
+        </property>
+        <property name="dragDropMode">
+         <enum>QAbstractItemView::InternalMove</enum>
+        </property>
+        <property name="alternatingRowColors">
+         <bool>true</bool>
+        </property>
+        <property name="selectionMode">
+         <enum>QAbstractItemView::ExtendedSelection</enum>
+        </property>
+        <property name="textElideMode">
+         <enum>Qt::ElideNone</enum>
+        </property>
+       </widget>
+       <widget class="QPushButton" name="hrtfAddButton">
+        <property name="geometry">
+         <rect>
+          <x>420</x>
+          <y>20</y>
+          <width>81</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Add...</string>
+        </property>
+        <property name="icon">
+         <iconset theme="list-add">
+          <normaloff>.</normaloff>.</iconset>
+        </property>
+        <property name="flat">
+         <bool>false</bool>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="defaultHrtfPathsCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>180</x>
+          <y>110</y>
+          <width>151</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Include the default system paths in addition to any
+listed above.</string>
+        </property>
+        <property name="text">
+         <string>Include Default Paths</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QPushButton" name="hrtfRemoveButton">
+        <property name="geometry">
+         <rect>
+          <x>420</x>
+          <y>50</y>
+          <width>81</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Remove</string>
+        </property>
+        <property name="icon">
+         <iconset theme="list-remove">
+          <normaloff>.</normaloff>.</iconset>
+        </property>
+       </widget>
+      </widget>
+     </widget>
+     <widget class="QLabel" name="label_12">
+      <property name="geometry">
+       <rect>
+        <x>30</x>
+        <y>20</y>
+        <width>91</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Preferred HRTF:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QComboBox" name="preferredHrtfComboBox">
+      <property name="geometry">
+       <rect>
+        <x>130</x>
+        <y>20</y>
+        <width>161</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>The default HRTF to use if the application doesn't request one.</string>
+      </property>
+     </widget>
+     <widget class="QGroupBox" name="groupBox_9">
+      <property name="geometry">
+       <rect>
+        <x>50</x>
+        <y>100</y>
+        <width>441</width>
+        <height>81</height>
+       </rect>
+      </property>
+      <property name="title">
+       <string>HRTF Render Method</string>
+      </property>
+      <widget class="QLabel" name="label_31">
+       <property name="geometry">
+        <rect>
+         <x>20</x>
+         <y>30</y>
+         <width>51</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Speed</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+       </property>
+      </widget>
+      <widget class="QLabel" name="label_32">
+       <property name="geometry">
+        <rect>
+         <x>340</x>
+         <y>30</y>
+         <width>51</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Quality</string>
+       </property>
+      </widget>
+      <widget class="QSlider" name="hrtfmodeSlider">
+       <property name="geometry">
+        <rect>
+         <x>80</x>
+         <y>30</y>
+         <width>251</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+      </widget>
+      <widget class="QLabel" name="hrtfmodeLabel">
+       <property name="geometry">
+        <rect>
+         <x>50</x>
+         <y>50</y>
+         <width>321</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Default</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </widget>
+    </widget>
+    <widget class="QWidget" name="tab">
+     <attribute name="title">
+      <string>Backends</string>
+     </attribute>
+     <widget class="QListWidget" name="backendListWidget">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>11</y>
+        <width>111</width>
+        <height>361</height>
+       </rect>
+      </property>
+      <property name="alternatingRowColors">
+       <bool>true</bool>
+      </property>
+      <item>
+       <property name="text">
+        <string>General</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>PipeWire</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>WASAPI</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>PulseAudio</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>JACK</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>ALSA</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>OSS</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Solaris</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Wave Writer</string>
+       </property>
+      </item>
+     </widget>
+     <widget class="QStackedWidget" name="backendStackedWidget">
+      <property name="geometry">
+       <rect>
+        <x>110</x>
+        <y>10</y>
+        <width>421</width>
+        <height>361</height>
+       </rect>
+      </property>
+      <property name="currentIndex">
+       <number>0</number>
+      </property>
+      <widget class="QWidget" name="page">
+       <widget class="QCheckBox" name="backendCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>190</y>
+          <width>391</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>When checked, allows all other available backends not listed in the priority or disabled lists.</string>
+        </property>
+        <property name="text">
+         <string>Allow Other Backends</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QListWidget" name="disabledBackendList">
+        <property name="geometry">
+         <rect>
+          <x>220</x>
+          <y>30</y>
+          <width>191</width>
+          <height>151</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Disabled backend driver list.</string>
+        </property>
+       </widget>
+       <widget class="QListWidget" name="enabledBackendList">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>30</y>
+          <width>191</width>
+          <height>151</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>The backend driver list order. Unknown backends and
+duplicated names are ignored.</string>
+        </property>
+        <property name="dragDropMode">
+         <enum>QAbstractItemView::InternalMove</enum>
+        </property>
+       </widget>
+       <widget class="QLabel" name="label_2">
+        <property name="geometry">
+         <rect>
+          <x>230</x>
+          <y>10</y>
+          <width>171</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Disabled Backends:</string>
+        </property>
+       </widget>
+       <widget class="QLabel" name="label">
+        <property name="geometry">
+         <rect>
+          <x>30</x>
+          <y>10</y>
+          <width>171</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Priority Backends:</string>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_1">
+       <widget class="QCheckBox" name="pwireAssumeAudioCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>10</y>
+          <width>161</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Assumes PipeWire has support for audio, allowing
+the backend to initialize even when no audio devices
+are reported.</string>
+        </property>
+        <property name="text">
+         <string>Assume audio support</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="pwireRtMixCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>40</y>
+          <width>161</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Renders samples directly in the real-time
+processing callback. This allows for lower
+latency and less overall CPU utilization, but
+can increase the risk of underruns when
+increasing the amount of processing the
+mixer needs to do.</string>
+        </property>
+        <property name="text">
+         <string>Real-time Mixing</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_2">
+       <widget class="QCheckBox" name="wasapiResamplerCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>10</y>
+          <width>191</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Specifies whether to allow an extra resampler pass on the output. Enabling
+this will allow the playback device to be set to a different sample rate
+than the actual output can accept, causing the backend to apply its own
+resampling pass after OpenAL Soft mixes the sources and processes effects
+for output.</string>
+        </property>
+        <property name="text">
+         <string>Allow Resampler</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_8">
+       <widget class="QCheckBox" name="pulseAutospawnCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>10</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Automatically spawn a PulseAudio server if one
+is not already running.</string>
+        </property>
+        <property name="text">
+         <string>AutoSpawn Server</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="pulseAllowMovesCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>40</y>
+          <width>161</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Allows moving PulseAudio streams to different
+devices during playback or capture. Note that the
+device specifier and device format will not change
+to match the new device.</string>
+        </property>
+        <property name="text">
+         <string>Allow Moving Streams</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="pulseFixRateCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>70</y>
+          <width>121</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>When checked, fix the OpenAL device's sample
+rate to match the PulseAudio device.</string>
+        </property>
+        <property name="text">
+         <string>Fix Sample Rate</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="pulseAdjLatencyCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>100</y>
+          <width>111</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Attempts to adjust the overall latency of device
+playback. Note that this may have adverse effects
+on the resulting internal buffer sizes and mixing
+updates, leading to performance problems and
+drop-outs.</string>
+        </property>
+        <property name="text">
+         <string>Adjust Latency</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_7">
+       <widget class="QCheckBox" name="jackAutospawnCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>10</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>AutoSpawn Server</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QGroupBox" name="groupBox_7">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>110</y>
+          <width>401</width>
+          <height>80</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>The update buffer size, in samples, that the backend
+will keep buffered to handle the server's real-time
+processing requests. Must be a power of 2. Ignored
+when Real-time Mixing is used.</string>
+        </property>
+        <property name="title">
+         <string>Buffer Size</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignCenter</set>
+        </property>
+        <widget class="QLineEdit" name="jackBufferSizeLine">
+         <property name="geometry">
+          <rect>
+           <x>320</x>
+           <y>30</y>
+           <width>71</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="placeholderText">
+          <string>0</string>
+         </property>
+        </widget>
+        <widget class="QSlider" name="jackBufferSizeSlider">
+         <property name="geometry">
+          <rect>
+           <x>10</x>
+           <y>30</y>
+           <width>301</width>
+           <height>21</height>
+          </rect>
+         </property>
+         <property name="maximum">
+          <number>13</number>
+         </property>
+         <property name="singleStep">
+          <number>1</number>
+         </property>
+         <property name="pageStep">
+          <number>4</number>
+         </property>
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="tickPosition">
+          <enum>QSlider::TicksBelow</enum>
+         </property>
+         <property name="tickInterval">
+          <number>1</number>
+         </property>
+        </widget>
+       </widget>
+       <widget class="QCheckBox" name="jackConnectPortsCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>40</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>AutoConnect Ports</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="jackRtMixCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>70</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Renders samples directly in the real-time
+processing callback. This allows for lower
+latency and less overall CPU utilization, but
+can increase the risk of underruns when
+increasing the amount of processing the
+mixer needs to do.</string>
+        </property>
+        <property name="text">
+         <string>Real-time Mixing</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_3">
+       <widget class="QLabel" name="label_17">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>30</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Default Playback Device:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLineEdit" name="alsaDefaultDeviceLine">
+        <property name="geometry">
+         <rect>
+          <x>160</x>
+          <y>30</y>
+          <width>231</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="placeholderText">
+         <string>default</string>
+        </property>
+       </widget>
+       <widget class="QLabel" name="label_18">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>60</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Default Capture Device:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLineEdit" name="alsaDefaultCaptureLine">
+        <property name="geometry">
+         <rect>
+          <x>160</x>
+          <y>60</y>
+          <width>231</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="placeholderText">
+         <string>default</string>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="alsaResamplerCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>20</x>
+          <y>100</y>
+          <width>191</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Allow use of ALSA's software resampler. This lets
+the OpenAL device to be set to a different sample
+rate than the backend device, but incurs another
+resample pass on top of OpenAL's resampler.</string>
+        </property>
+        <property name="text">
+         <string>Allow Resampler</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="alsaMmapCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>210</x>
+          <y>100</y>
+          <width>191</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="toolTip">
+         <string>Accesses the audio device buffer through an mmap,
+potentially avoiding an extra sample buffer copy
+during updates.</string>
+        </property>
+        <property name="text">
+         <string>MMap Buffer</string>
+        </property>
+        <property name="tristate">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_4">
+       <widget class="QLabel" name="label_20">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>30</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Default Playback Device:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLineEdit" name="ossDefaultDeviceLine">
+        <property name="geometry">
+         <rect>
+          <x>160</x>
+          <y>30</y>
+          <width>151</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="placeholderText">
+         <string>/dev/dsp</string>
+        </property>
+       </widget>
+       <widget class="QLabel" name="label_21">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>60</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Default Capture Device:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLineEdit" name="ossDefaultCaptureLine">
+        <property name="geometry">
+         <rect>
+          <x>160</x>
+          <y>60</y>
+          <width>151</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="placeholderText">
+         <string>/dev/dsp</string>
+        </property>
+       </widget>
+       <widget class="QPushButton" name="ossPlaybackPushButton">
+        <property name="geometry">
+         <rect>
+          <x>320</x>
+          <y>30</y>
+          <width>91</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Browse...</string>
+        </property>
+       </widget>
+       <widget class="QPushButton" name="ossCapturePushButton">
+        <property name="geometry">
+         <rect>
+          <x>320</x>
+          <y>60</y>
+          <width>91</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Browse...</string>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_5">
+       <widget class="QLineEdit" name="solarisDefaultDeviceLine">
+        <property name="geometry">
+         <rect>
+          <x>160</x>
+          <y>30</y>
+          <width>151</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="placeholderText">
+         <string>/dev/audio</string>
+        </property>
+       </widget>
+       <widget class="QLabel" name="label_22">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>30</y>
+          <width>141</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Default Playback Device:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QPushButton" name="solarisPlaybackPushButton">
+        <property name="geometry">
+         <rect>
+          <x>320</x>
+          <y>30</y>
+          <width>91</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Browse...</string>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="page_6">
+       <widget class="QLabel" name="label_23">
+        <property name="geometry">
+         <rect>
+          <x>10</x>
+          <y>30</y>
+          <width>71</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Output File:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLineEdit" name="waveOutputLine">
+        <property name="geometry">
+         <rect>
+          <x>90</x>
+          <y>30</y>
+          <width>221</width>
+          <height>21</height>
+         </rect>
+        </property>
+       </widget>
+       <widget class="QLabel" name="label_24">
+        <property name="geometry">
+         <rect>
+          <x>0</x>
+          <y>90</y>
+          <width>421</width>
+          <height>71</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;Warning: The specified output file will be OVERWRITTEN WITHOUT&lt;/span&gt;&lt;/p&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;QUESTION when the Wave Writer device is opened.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+        </property>
+       </widget>
+       <widget class="QPushButton" name="waveOutputButton">
+        <property name="geometry">
+         <rect>
+          <x>320</x>
+          <y>30</y>
+          <width>91</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Browse...</string>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="waveBFormatCheckBox">
+        <property name="geometry">
+         <rect>
+          <x>120</x>
+          <y>60</y>
+          <width>191</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Create .amb (B-Format) files</string>
+        </property>
+       </widget>
+      </widget>
+     </widget>
+    </widget>
+    <widget class="QWidget" name="tab_2">
+     <attribute name="title">
+      <string>Resources</string>
+     </attribute>
+     <widget class="QLineEdit" name="srcCountLineEdit">
+      <property name="geometry">
+       <rect>
+        <x>190</x>
+        <y>20</y>
+        <width>51</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>The maximum number of allocatable sources. Lower values may
+help for systems with apps that try to play more sounds than
+the CPU can handle.</string>
+      </property>
+      <property name="maxLength">
+       <number>4</number>
+      </property>
+      <property name="placeholderText">
+       <string>256</string>
+      </property>
+     </widget>
+     <widget class="QLabel" name="label_3">
+      <property name="geometry">
+       <rect>
+        <x>10</x>
+        <y>20</y>
+        <width>171</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Number of Sound Sources:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QLabel" name="label_4">
+      <property name="geometry">
+       <rect>
+        <x>10</x>
+        <y>50</y>
+        <width>171</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Number of Effect Slots:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QLineEdit" name="effectSlotLineEdit">
+      <property name="geometry">
+       <rect>
+        <x>190</x>
+        <y>50</y>
+        <width>51</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>The maximum number of Auxiliary Effect Slots an app can
+create. A slot can use a non-negligible amount of CPU time if
+an effect is set on it even if no sources are feeding it, so this
+may help when apps use more than the system can handle.</string>
+      </property>
+      <property name="maxLength">
+       <number>3</number>
+      </property>
+      <property name="placeholderText">
+       <string>64</string>
+      </property>
+     </widget>
+     <widget class="QLabel" name="label_8">
+      <property name="geometry">
+       <rect>
+        <x>10</x>
+        <y>80</y>
+        <width>171</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Number of Source Sends:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QLineEdit" name="srcSendLineEdit">
+      <property name="geometry">
+       <rect>
+        <x>190</x>
+        <y>80</y>
+        <width>51</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>Limits the number of auxiliary sends allowed per source.
+Setting this higher than the default has no effect.</string>
+      </property>
+      <property name="maxLength">
+       <number>2</number>
+      </property>
+      <property name="placeholderText">
+       <string>16</string>
+      </property>
+     </widget>
+     <widget class="QGroupBox" name="cpuExtGroupBox">
+      <property name="geometry">
+       <rect>
+        <x>10</x>
+        <y>120</y>
+        <width>511</width>
+        <height>121</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>Enables use of specific CPU extensions. Certain methods may
+utilize CPU extensions when detected, and disabling these can
+be useful for preventing those extensions from being used.</string>
+      </property>
+      <property name="title">
+       <string>CPU Extensions</string>
+      </property>
+      <widget class="QCheckBox" name="enableSSECheckBox">
+       <property name="geometry">
+        <rect>
+         <x>100</x>
+         <y>20</y>
+         <width>71</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>SSE</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableSSE2CheckBox">
+       <property name="geometry">
+        <rect>
+         <x>180</x>
+         <y>20</y>
+         <width>71</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>SSE2</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableNeonCheckBox">
+       <property name="geometry">
+        <rect>
+         <x>100</x>
+         <y>50</y>
+         <width>71</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Neon</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableSSE41CheckBox">
+       <property name="geometry">
+        <rect>
+         <x>340</x>
+         <y>20</y>
+         <width>71</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>SSE4.1</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableSSE3CheckBox">
+       <property name="geometry">
+        <rect>
+         <x>260</x>
+         <y>20</y>
+         <width>71</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>SSE3</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QLabel" name="cpuExtDisabledLabel">
+       <property name="geometry">
+        <rect>
+         <x>101</x>
+         <y>80</y>
+         <width>311</width>
+         <height>31</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;No support enabled for CPU Extensions&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+       </property>
+      </widget>
+     </widget>
+    </widget>
+    <widget class="QWidget" name="tab_4">
+     <attribute name="title">
+      <string>Effects</string>
+     </attribute>
+     <widget class="QGroupBox" name="groupBox_5">
+      <property name="geometry">
+       <rect>
+        <x>10</x>
+        <y>60</y>
+        <width>511</width>
+        <height>241</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>Specifies which effects apps can recognize. Disabling effects
+can help for apps that try to use ones that are too intensive
+for the system to handle.</string>
+      </property>
+      <property name="title">
+       <string>Enabled Effects</string>
+      </property>
+      <widget class="QCheckBox" name="enableEaxReverbCheck">
+       <property name="geometry">
+        <rect>
+         <x>70</x>
+         <y>30</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>EAX Reverb</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableStdReverbCheck">
+       <property name="geometry">
+        <rect>
+         <x>70</x>
+         <y>60</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Standard Reverb</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableChorusCheck">
+       <property name="geometry">
+        <rect>
+         <x>70</x>
+         <y>90</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Chorus</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableDistortionCheck">
+       <property name="geometry">
+        <rect>
+         <x>70</x>
+         <y>150</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Distortion</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableEchoCheck">
+       <property name="geometry">
+        <rect>
+         <x>70</x>
+         <y>180</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Echo</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableEqualizerCheck">
+       <property name="geometry">
+        <rect>
+         <x>320</x>
+         <y>30</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Equalizer</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableFlangerCheck">
+       <property name="geometry">
+        <rect>
+         <x>320</x>
+         <y>90</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Flanger</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableModulatorCheck">
+       <property name="geometry">
+        <rect>
+         <x>320</x>
+         <y>150</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Ring Modulator</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableDedicatedCheck">
+       <property name="geometry">
+        <rect>
+         <x>320</x>
+         <y>180</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="toolTip">
+        <string>Enables both the Dedicated Dialog and Dedicated LFE effects
+added by the ALC_EXT_DEDICATED extension.</string>
+       </property>
+       <property name="text">
+        <string>Dedicated ...</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableCompressorCheck">
+       <property name="geometry">
+        <rect>
+         <x>70</x>
+         <y>120</y>
+         <width>111</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Compressor</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enablePitchShifterCheck">
+       <property name="geometry">
+        <rect>
+         <x>320</x>
+         <y>120</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Pitch Shifter</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableFrequencyShifterCheck">
+       <property name="geometry">
+        <rect>
+         <x>320</x>
+         <y>60</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Frequency Shifter</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableAutowahCheck">
+       <property name="geometry">
+        <rect>
+         <x>70</x>
+         <y>210</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Autowah</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+      <widget class="QCheckBox" name="enableVocalMorpherCheck">
+       <property name="geometry">
+        <rect>
+         <x>320</x>
+         <y>210</y>
+         <width>131</width>
+         <height>21</height>
+        </rect>
+       </property>
+       <property name="text">
+        <string>Vocal morpher</string>
+       </property>
+       <property name="checked">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </widget>
+     <widget class="QLabel" name="label_13">
+      <property name="geometry">
+       <rect>
+        <x>10</x>
+        <y>20</y>
+        <width>141</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Default Reverb Effect:</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+      </property>
+     </widget>
+     <widget class="QComboBox" name="defaultReverbComboBox">
+      <property name="geometry">
+       <rect>
+        <x>160</x>
+        <y>20</y>
+        <width>135</width>
+        <height>31</height>
+       </rect>
+      </property>
+      <property name="sizeAdjustPolicy">
+       <enum>QComboBox::AdjustToContents</enum>
+      </property>
+      <item>
+       <property name="text">
+        <string>None</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Generic</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>PaddedCell</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Room</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Bathroom</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Livingroom</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Stoneroom</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Auditorium</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>ConcertHall</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Cave</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Arena</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Hangar</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>CarpetedHallway</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Hallway</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>StoneCorridor</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Alley</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Forest</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>City</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Mountains</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Quarry</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Plain</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>ParkingLot</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>SewerPipe</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Underwater</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Drugged</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Dizzy</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Psychotic</string>
+       </property>
+      </item>
+     </widget>
+     <widget class="QCheckBox" name="enableEaxCheck">
+      <property name="geometry">
+       <rect>
+        <x>30</x>
+        <y>320</y>
+        <width>231</width>
+        <height>21</height>
+       </rect>
+      </property>
+      <property name="toolTip">
+       <string>Enables legacy EAX API support.</string>
+      </property>
+      <property name="text">
+       <string>Enable EAX API support</string>
+      </property>
+     </widget>
+    </widget>
+   </widget>
+   <widget class="QPushButton" name="closeCancelButton">
+    <property name="geometry">
+     <rect>
+      <x>370</x>
+      <y>405</y>
+      <width>91</width>
+      <height>31</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Cancel</string>
+    </property>
+    <property name="icon">
+     <iconset theme="window-close">
+      <normaloff>.</normaloff>.</iconset>
+    </property>
+   </widget>
+  </widget>
+  <widget class="QMenuBar" name="menuBar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>564</width>
+     <height>29</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuFile">
+    <property name="title">
+     <string>&amp;File</string>
+    </property>
+    <addaction name="actionLoad"/>
+    <addaction name="actionSave_As"/>
+    <addaction name="separator"/>
+    <addaction name="actionQuit"/>
+   </widget>
+   <widget class="QMenu" name="menuHelp">
+    <property name="title">
+     <string>&amp;Help</string>
+    </property>
+    <addaction name="actionAbout"/>
+   </widget>
+   <addaction name="menuFile"/>
+   <addaction name="menuHelp"/>
+  </widget>
+  <action name="actionQuit">
+   <property name="icon">
+    <iconset theme="application-exit">
+     <normaloff>.</normaloff>.</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Quit</string>
+   </property>
+  </action>
+  <action name="actionSave_As">
+   <property name="icon">
+    <iconset theme="document-save-as">
+     <normaloff>.</normaloff>.</iconset>
+   </property>
+   <property name="text">
+    <string>Save &amp;As...</string>
+   </property>
+   <property name="toolTip">
+    <string>Save Configuration As</string>
+   </property>
+  </action>
+  <action name="actionLoad">
+   <property name="icon">
+    <iconset theme="document-open">
+     <normaloff>.</normaloff>.</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Load...</string>
+   </property>
+   <property name="toolTip">
+    <string>Load Configuration File</string>
+   </property>
+  </action>
+  <action name="actionAbout">
+   <property name="text">
+    <string>&amp;About...</string>
+   </property>
+  </action>
+ </widget>
+ <layoutdefault spacing="6" margin="11"/>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>backendListWidget</sender>
+   <signal>currentRowChanged(int)</signal>
+   <receiver>backendStackedWidget</receiver>
+   <slot>setCurrentIndex(int)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>69</x>
+     <y>233</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>329</x>
+     <y>232</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+ <slots>
+  <slot>ShowHRTFContextMenu(QPoint)</slot>
+ </slots>
+</ui>
diff --git a/utils/alsoft-config/verstr.cpp b/utils/alsoft-config/verstr.cpp
new file mode 100644 (file)
index 0000000..42b1aea
--- /dev/null
@@ -0,0 +1,10 @@
+
+#include "verstr.h"
+
+#include "version.h"
+
+
+QString GetVersionString()
+{
+    return QStringLiteral(ALSOFT_VERSION "-" ALSOFT_GIT_COMMIT_HASH " (" ALSOFT_GIT_BRANCH " branch).");
+}
diff --git a/utils/alsoft-config/verstr.h b/utils/alsoft-config/verstr.h
new file mode 100644 (file)
index 0000000..73e3ecb
--- /dev/null
@@ -0,0 +1,8 @@
+#ifndef VERSTR_H
+#define VERSTR_H
+
+#include <QString>
+
+QString GetVersionString();
+
+#endif /* VERSTR_H */
diff --git a/utils/getopt.c b/utils/getopt.c
new file mode 100644 (file)
index 0000000..ab1a246
--- /dev/null
@@ -0,0 +1,137 @@
+/*     $NetBSD: getopt.c,v 1.26 2003/08/07 16:43:40 agc Exp $  */
+
+/*
+ * Copyright (c) 1987, 1993, 1994
+ *     The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 4. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#if defined(LIBC_SCCS) && !defined(lint)
+static char sccsid[] = "@(#)getopt.c   8.3 (Berkeley) 4/27/95";
+#endif /* LIBC_SCCS and not lint */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "getopt.h"
+
+int    opterr = 1,             /* if error message should be printed */
+       optind = 1,             /* index into parent argv vector */
+       optopt,                 /* character checked for validity */
+       optreset;               /* reset getopt */
+char   *optarg;                /* argument associated with option */
+
+#define        BADCH   (int)'?'
+#define        BADARG  (int)':'
+#define        EMSG    ""
+
+/*
+ * Get program name in Windows
+ */
+const char * _getprogname(void);
+
+/*
+ * getopt --
+ *     Parse argc/argv argument vector.
+ */
+int
+getopt(int nargc, char * const nargv[], const char *ostr)
+{
+       static char *place = EMSG;              /* option letter processing */
+       char *oli;                              /* option letter list index */
+
+       if (optreset || *place == 0) {          /* update scanning pointer */
+               optreset = 0;
+               place = nargv[optind];
+               if (optind >= nargc || *place++ != '-') {
+                       /* Argument is absent or is not an option */
+                       place = EMSG;
+                       return (-1);
+               }
+               optopt = *place++;
+               if (optopt == '-' && *place == 0) {
+                       /* "--" => end of options */
+                       ++optind;
+                       place = EMSG;
+                       return (-1);
+               }
+               if (optopt == 0) {
+                       /* Solitary '-', treat as a '-' option
+                          if the program (eg su) is looking for it. */
+                       place = EMSG;
+                       if (strchr(ostr, '-') == NULL)
+                               return (-1);
+                       optopt = '-';
+               }
+       } else
+               optopt = *place++;
+
+       /* See if option letter is one the caller wanted... */
+       if (optopt == ':' || (oli = strchr(ostr, optopt)) == NULL) {
+               if (*place == 0)
+                       ++optind;
+               if (opterr && *ostr != ':')
+                       (void)fprintf(stderr,
+                           "%s: illegal option -- %c\n", _getprogname(),
+                           optopt);
+               return (BADCH);
+       }
+
+       /* Does this option need an argument? */
+       if (oli[1] != ':') {
+               /* don't need argument */
+               optarg = NULL;
+               if (*place == 0)
+                       ++optind;
+       } else {
+               /* Option-argument is either the rest of this argument or the
+                  entire next argument. */
+               if (*place)
+                       optarg = place;
+               else if (nargc > ++optind)
+                       optarg = nargv[optind];
+               else {
+                       /* option-argument absent */
+                       place = EMSG;
+                       if (*ostr == ':')
+                               return (BADARG);
+                       if (opterr)
+                               (void)fprintf(stderr,
+                                   "%s: option requires an argument -- %c\n",
+                                   _getprogname(), optopt);
+                       return (BADCH);
+               }
+               place = EMSG;
+               ++optind;
+       }
+       return (optopt);                        /* return option letter */
+}
+
+const char * _getprogname() {
+       char *pgmptr = NULL;
+       _get_pgmptr(&pgmptr);
+       return strrchr(pgmptr,'\\')+1;
+}
+
diff --git a/utils/getopt.h b/utils/getopt.h
new file mode 100644 (file)
index 0000000..0218c42
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef GETOPT_H
+#define GETOPT_H
+
+#ifndef _WIN32
+
+#include <unistd.h>
+
+#else /* _WIN32 */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+extern char *optarg;
+extern int optind, opterr, optopt, optreset;
+
+int getopt(int nargc, char * const nargv[], const char *ostr);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !_WIN32 */
+
+#endif /* !GETOPT_H */
+
diff --git a/utils/makemhr/loaddef.cpp b/utils/makemhr/loaddef.cpp
new file mode 100644 (file)
index 0000000..e809236
--- /dev/null
@@ -0,0 +1,2058 @@
+/*
+ * HRTF utility for producing and demonstrating the process of creating an
+ * OpenAL Soft compatible HRIR data set.
+ *
+ * Copyright (C) 2011-2019  Christopher Fitzgerald
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Or visit:  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ */
+
+#include "loaddef.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cmath>
+#include <cstdarg>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <cstdarg>
+#include <vector>
+
+#include "alfstream.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "makemhr.h"
+#include "polyphase_resampler.h"
+
+#include "mysofa.h"
+
+// Constants for accessing the token reader's ring buffer.
+#define TR_RING_BITS                 (16)
+#define TR_RING_SIZE                 (1 << TR_RING_BITS)
+#define TR_RING_MASK                 (TR_RING_SIZE - 1)
+
+// The token reader's load interval in bytes.
+#define TR_LOAD_SIZE                 (TR_RING_SIZE >> 2)
+
+// Token reader state for parsing the data set definition.
+struct TokenReaderT {
+    std::istream &mIStream;
+    const char *mName{};
+    uint        mLine{};
+    uint        mColumn{};
+    char mRing[TR_RING_SIZE]{};
+    std::streamsize mIn{};
+    std::streamsize mOut{};
+
+    TokenReaderT(std::istream &istream) noexcept : mIStream{istream} { }
+    TokenReaderT(const TokenReaderT&) = default;
+};
+
+
+// The maximum identifier length used when processing the data set
+// definition.
+#define MAX_IDENT_LEN                (16)
+
+// The limits for the listener's head 'radius' in the data set definition.
+#define MIN_RADIUS                   (0.05)
+#define MAX_RADIUS                   (0.15)
+
+// The maximum number of channels that can be addressed for a WAVE file
+// source listed in the data set definition.
+#define MAX_WAVE_CHANNELS            (65535)
+
+// The limits to the byte size for a binary source listed in the definition
+// file.
+#define MIN_BIN_SIZE                 (2)
+#define MAX_BIN_SIZE                 (4)
+
+// The minimum number of significant bits for binary sources listed in the
+// data set definition.  The maximum is calculated from the byte size.
+#define MIN_BIN_BITS                 (16)
+
+// The limits to the number of significant bits for an ASCII source listed in
+// the data set definition.
+#define MIN_ASCII_BITS               (16)
+#define MAX_ASCII_BITS               (32)
+
+// The four-character-codes for RIFF/RIFX WAVE file chunks.
+#define FOURCC_RIFF                  (0x46464952) // 'RIFF'
+#define FOURCC_RIFX                  (0x58464952) // 'RIFX'
+#define FOURCC_WAVE                  (0x45564157) // 'WAVE'
+#define FOURCC_FMT                   (0x20746D66) // 'fmt '
+#define FOURCC_DATA                  (0x61746164) // 'data'
+#define FOURCC_LIST                  (0x5453494C) // 'LIST'
+#define FOURCC_WAVL                  (0x6C766177) // 'wavl'
+#define FOURCC_SLNT                  (0x746E6C73) // 'slnt'
+
+// The supported wave formats.
+#define WAVE_FORMAT_PCM              (0x0001)
+#define WAVE_FORMAT_IEEE_FLOAT       (0x0003)
+#define WAVE_FORMAT_EXTENSIBLE       (0xFFFE)
+
+
+enum ByteOrderT {
+    BO_NONE,
+    BO_LITTLE,
+    BO_BIG
+};
+
+// Source format for the references listed in the data set definition.
+enum SourceFormatT {
+    SF_NONE,
+    SF_ASCII,  // ASCII text file.
+    SF_BIN_LE, // Little-endian binary file.
+    SF_BIN_BE, // Big-endian binary file.
+    SF_WAVE,   // RIFF/RIFX WAVE file.
+    SF_SOFA    // Spatially Oriented Format for Accoustics (SOFA) file.
+};
+
+// Element types for the references listed in the data set definition.
+enum ElementTypeT {
+    ET_NONE,
+    ET_INT,  // Integer elements.
+    ET_FP    // Floating-point elements.
+};
+
+// Source reference state used when loading sources.
+struct SourceRefT {
+    SourceFormatT mFormat;
+    ElementTypeT  mType;
+    uint mSize;
+    int  mBits;
+    uint mChannel;
+    double mAzimuth;
+    double mElevation;
+    double mRadius;
+    uint mSkip;
+    uint mOffset;
+    char mPath[MAX_PATH_LEN+1];
+};
+
+
+/* Whitespace is not significant. It can process tokens as identifiers, numbers
+ * (integer and floating-point), strings, and operators. Strings must be
+ * encapsulated by double-quotes and cannot span multiple lines.
+ */
+
+// Setup the reader on the given file.  The filename can be NULL if no error
+// output is desired.
+static void TrSetup(const char *startbytes, std::streamsize startbytecount, const char *filename,
+    TokenReaderT *tr)
+{
+    const char *name = nullptr;
+
+    if(filename)
+    {
+        const char *slash = strrchr(filename, '/');
+        if(slash)
+        {
+            const char *bslash = strrchr(slash+1, '\\');
+            if(bslash) name = bslash+1;
+            else name = slash+1;
+        }
+        else
+        {
+            const char *bslash = strrchr(filename, '\\');
+            if(bslash) name = bslash+1;
+            else name = filename;
+        }
+    }
+
+    tr->mName = name;
+    tr->mLine = 1;
+    tr->mColumn = 1;
+    tr->mIn = 0;
+    tr->mOut = 0;
+
+    if(startbytecount > 0)
+    {
+        std::copy_n(startbytes, startbytecount, std::begin(tr->mRing));
+        tr->mIn += startbytecount;
+    }
+}
+
+// Prime the reader's ring buffer, and return a result indicating that there
+// is text to process.
+static int TrLoad(TokenReaderT *tr)
+{
+    std::istream &istream = tr->mIStream;
+
+    std::streamsize toLoad{TR_RING_SIZE - static_cast<std::streamsize>(tr->mIn - tr->mOut)};
+    if(toLoad >= TR_LOAD_SIZE && istream.good())
+    {
+        // Load TR_LOAD_SIZE (or less if at the end of the file) per read.
+        toLoad = TR_LOAD_SIZE;
+        std::streamsize in{tr->mIn&TR_RING_MASK};
+        std::streamsize count{TR_RING_SIZE - in};
+        if(count < toLoad)
+        {
+            istream.read(&tr->mRing[in], count);
+            tr->mIn += istream.gcount();
+            istream.read(&tr->mRing[0], toLoad-count);
+            tr->mIn += istream.gcount();
+        }
+        else
+        {
+            istream.read(&tr->mRing[in], toLoad);
+            tr->mIn += istream.gcount();
+        }
+
+        if(tr->mOut >= TR_RING_SIZE)
+        {
+            tr->mOut -= TR_RING_SIZE;
+            tr->mIn -= TR_RING_SIZE;
+        }
+    }
+    if(tr->mIn > tr->mOut)
+        return 1;
+    return 0;
+}
+
+// Error display routine.  Only displays when the base name is not NULL.
+static void TrErrorVA(const TokenReaderT *tr, uint line, uint column, const char *format, va_list argPtr)
+{
+    if(!tr->mName)
+        return;
+    fprintf(stderr, "\nError (%s:%u:%u): ", tr->mName, line, column);
+    vfprintf(stderr, format, argPtr);
+}
+
+// Used to display an error at a saved line/column.
+static void TrErrorAt(const TokenReaderT *tr, uint line, uint column, const char *format, ...)
+{
+    va_list argPtr;
+
+    va_start(argPtr, format);
+    TrErrorVA(tr, line, column, format, argPtr);
+    va_end(argPtr);
+}
+
+// Used to display an error at the current line/column.
+static void TrError(const TokenReaderT *tr, const char *format, ...)
+{
+    va_list argPtr;
+
+    va_start(argPtr, format);
+    TrErrorVA(tr, tr->mLine, tr->mColumn, format, argPtr);
+    va_end(argPtr);
+}
+
+// Skips to the next line.
+static void TrSkipLine(TokenReaderT *tr)
+{
+    char ch;
+
+    while(TrLoad(tr))
+    {
+        ch = tr->mRing[tr->mOut&TR_RING_MASK];
+        tr->mOut++;
+        if(ch == '\n')
+        {
+            tr->mLine++;
+            tr->mColumn = 1;
+            break;
+        }
+        tr->mColumn ++;
+    }
+}
+
+// Skips to the next token.
+static int TrSkipWhitespace(TokenReaderT *tr)
+{
+    while(TrLoad(tr))
+    {
+        char ch{tr->mRing[tr->mOut&TR_RING_MASK]};
+        if(isspace(ch))
+        {
+            tr->mOut++;
+            if(ch == '\n')
+            {
+                tr->mLine++;
+                tr->mColumn = 1;
+            }
+            else
+                tr->mColumn++;
+        }
+        else if(ch == '#')
+            TrSkipLine(tr);
+        else
+            return 1;
+    }
+    return 0;
+}
+
+// Get the line and/or column of the next token (or the end of input).
+static void TrIndication(TokenReaderT *tr, uint *line, uint *column)
+{
+    TrSkipWhitespace(tr);
+    if(line) *line = tr->mLine;
+    if(column) *column = tr->mColumn;
+}
+
+// Checks to see if a token is (likely to be) an identifier.  It does not
+// display any errors and will not proceed to the next token.
+static int TrIsIdent(TokenReaderT *tr)
+{
+    if(!TrSkipWhitespace(tr))
+        return 0;
+    char ch{tr->mRing[tr->mOut&TR_RING_MASK]};
+    return ch == '_' || isalpha(ch);
+}
+
+
+// Checks to see if a token is the given operator.  It does not display any
+// errors and will not proceed to the next token.
+static int TrIsOperator(TokenReaderT *tr, const char *op)
+{
+    std::streamsize out;
+    size_t len;
+    char ch;
+
+    if(!TrSkipWhitespace(tr))
+        return 0;
+    out = tr->mOut;
+    len = 0;
+    while(op[len] != '\0' && out < tr->mIn)
+    {
+        ch = tr->mRing[out&TR_RING_MASK];
+        if(ch != op[len]) break;
+        len++;
+        out++;
+    }
+    if(op[len] == '\0')
+        return 1;
+    return 0;
+}
+
+/* The TrRead*() routines obtain the value of a matching token type.  They
+ * display type, form, and boundary errors and will proceed to the next
+ * token.
+ */
+
+// Reads and validates an identifier token.
+static int TrReadIdent(TokenReaderT *tr, const uint maxLen, char *ident)
+{
+    uint col, len;
+    char ch;
+
+    col = tr->mColumn;
+    if(TrSkipWhitespace(tr))
+    {
+        col = tr->mColumn;
+        ch = tr->mRing[tr->mOut&TR_RING_MASK];
+        if(ch == '_' || isalpha(ch))
+        {
+            len = 0;
+            do {
+                if(len < maxLen)
+                    ident[len] = ch;
+                len++;
+                tr->mOut++;
+                if(!TrLoad(tr))
+                    break;
+                ch = tr->mRing[tr->mOut&TR_RING_MASK];
+            } while(ch == '_' || isdigit(ch) || isalpha(ch));
+
+            tr->mColumn += len;
+            if(len < maxLen)
+            {
+                ident[len] = '\0';
+                return 1;
+            }
+            TrErrorAt(tr, tr->mLine, col, "Identifier is too long.\n");
+            return 0;
+        }
+    }
+    TrErrorAt(tr, tr->mLine, col, "Expected an identifier.\n");
+    return 0;
+}
+
+// Reads and validates (including bounds) an integer token.
+static int TrReadInt(TokenReaderT *tr, const int loBound, const int hiBound, int *value)
+{
+    uint col, digis, len;
+    char ch, temp[64+1];
+
+    col = tr->mColumn;
+    if(TrSkipWhitespace(tr))
+    {
+        col = tr->mColumn;
+        len = 0;
+        ch = tr->mRing[tr->mOut&TR_RING_MASK];
+        if(ch == '+' || ch == '-')
+        {
+            temp[len] = ch;
+            len++;
+            tr->mOut++;
+        }
+        digis = 0;
+        while(TrLoad(tr))
+        {
+            ch = tr->mRing[tr->mOut&TR_RING_MASK];
+            if(!isdigit(ch)) break;
+            if(len < 64)
+                temp[len] = ch;
+            len++;
+            digis++;
+            tr->mOut++;
+        }
+        tr->mColumn += len;
+        if(digis > 0 && ch != '.' && !isalpha(ch))
+        {
+            if(len > 64)
+            {
+                TrErrorAt(tr, tr->mLine, col, "Integer is too long.");
+                return 0;
+            }
+            temp[len] = '\0';
+            *value = static_cast<int>(strtol(temp, nullptr, 10));
+            if(*value < loBound || *value > hiBound)
+            {
+                TrErrorAt(tr, tr->mLine, col, "Expected a value from %d to %d.\n", loBound, hiBound);
+                return 0;
+            }
+            return 1;
+        }
+    }
+    TrErrorAt(tr, tr->mLine, col, "Expected an integer.\n");
+    return 0;
+}
+
+// Reads and validates (including bounds) a float token.
+static int TrReadFloat(TokenReaderT *tr, const double loBound, const double hiBound, double *value)
+{
+    uint col, digis, len;
+    char ch, temp[64+1];
+
+    col = tr->mColumn;
+    if(TrSkipWhitespace(tr))
+    {
+        col = tr->mColumn;
+        len = 0;
+        ch = tr->mRing[tr->mOut&TR_RING_MASK];
+        if(ch == '+' || ch == '-')
+        {
+            temp[len] = ch;
+            len++;
+            tr->mOut++;
+        }
+
+        digis = 0;
+        while(TrLoad(tr))
+        {
+            ch = tr->mRing[tr->mOut&TR_RING_MASK];
+            if(!isdigit(ch)) break;
+            if(len < 64)
+                temp[len] = ch;
+            len++;
+            digis++;
+            tr->mOut++;
+        }
+        if(ch == '.')
+        {
+            if(len < 64)
+                temp[len] = ch;
+            len++;
+            tr->mOut++;
+        }
+        while(TrLoad(tr))
+        {
+            ch = tr->mRing[tr->mOut&TR_RING_MASK];
+            if(!isdigit(ch)) break;
+            if(len < 64)
+                temp[len] = ch;
+            len++;
+            digis++;
+            tr->mOut++;
+        }
+        if(digis > 0)
+        {
+            if(ch == 'E' || ch == 'e')
+            {
+                if(len < 64)
+                    temp[len] = ch;
+                len++;
+                digis = 0;
+                tr->mOut++;
+                if(ch == '+' || ch == '-')
+                {
+                    if(len < 64)
+                        temp[len] = ch;
+                    len++;
+                    tr->mOut++;
+                }
+                while(TrLoad(tr))
+                {
+                    ch = tr->mRing[tr->mOut&TR_RING_MASK];
+                    if(!isdigit(ch)) break;
+                    if(len < 64)
+                        temp[len] = ch;
+                    len++;
+                    digis++;
+                    tr->mOut++;
+                }
+            }
+            tr->mColumn += len;
+            if(digis > 0 && ch != '.' && !isalpha(ch))
+            {
+                if(len > 64)
+                {
+                    TrErrorAt(tr, tr->mLine, col, "Float is too long.");
+                    return 0;
+                }
+                temp[len] = '\0';
+                *value = strtod(temp, nullptr);
+                if(*value < loBound || *value > hiBound)
+                {
+                    TrErrorAt(tr, tr->mLine, col, "Expected a value from %f to %f.\n", loBound, hiBound);
+                    return 0;
+                }
+                return 1;
+            }
+        }
+        else
+            tr->mColumn += len;
+    }
+    TrErrorAt(tr, tr->mLine, col, "Expected a float.\n");
+    return 0;
+}
+
+// Reads and validates a string token.
+static int TrReadString(TokenReaderT *tr, const uint maxLen, char *text)
+{
+    uint col, len;
+    char ch;
+
+    col = tr->mColumn;
+    if(TrSkipWhitespace(tr))
+    {
+        col = tr->mColumn;
+        ch = tr->mRing[tr->mOut&TR_RING_MASK];
+        if(ch == '\"')
+        {
+            tr->mOut++;
+            len = 0;
+            while(TrLoad(tr))
+            {
+                ch = tr->mRing[tr->mOut&TR_RING_MASK];
+                tr->mOut++;
+                if(ch == '\"')
+                    break;
+                if(ch == '\n')
+                {
+                    TrErrorAt(tr, tr->mLine, col, "Unterminated string at end of line.\n");
+                    return 0;
+                }
+                if(len < maxLen)
+                    text[len] = ch;
+                len++;
+            }
+            if(ch != '\"')
+            {
+                tr->mColumn += 1 + len;
+                TrErrorAt(tr, tr->mLine, col, "Unterminated string at end of input.\n");
+                return 0;
+            }
+            tr->mColumn += 2 + len;
+            if(len > maxLen)
+            {
+                TrErrorAt(tr, tr->mLine, col, "String is too long.\n");
+                return 0;
+            }
+            text[len] = '\0';
+            return 1;
+        }
+    }
+    TrErrorAt(tr, tr->mLine, col, "Expected a string.\n");
+    return 0;
+}
+
+// Reads and validates the given operator.
+static int TrReadOperator(TokenReaderT *tr, const char *op)
+{
+    uint col, len;
+    char ch;
+
+    col = tr->mColumn;
+    if(TrSkipWhitespace(tr))
+    {
+        col = tr->mColumn;
+        len = 0;
+        while(op[len] != '\0' && TrLoad(tr))
+        {
+            ch = tr->mRing[tr->mOut&TR_RING_MASK];
+            if(ch != op[len]) break;
+            len++;
+            tr->mOut++;
+        }
+        tr->mColumn += len;
+        if(op[len] == '\0')
+            return 1;
+    }
+    TrErrorAt(tr, tr->mLine, col, "Expected '%s' operator.\n", op);
+    return 0;
+}
+
+
+/*************************
+ *** File source input ***
+ *************************/
+
+// Read a binary value of the specified byte order and byte size from a file,
+// storing it as a 32-bit unsigned integer.
+static int ReadBin4(std::istream &istream, const char *filename, const ByteOrderT order, const uint bytes, uint32_t *out)
+{
+    uint8_t in[4];
+    istream.read(reinterpret_cast<char*>(in), static_cast<int>(bytes));
+    if(istream.gcount() != bytes)
+    {
+        fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename);
+        return 0;
+    }
+    uint32_t accum{0};
+    switch(order)
+    {
+        case BO_LITTLE:
+            for(uint i = 0;i < bytes;i++)
+                accum = (accum<<8) | in[bytes - i - 1];
+            break;
+        case BO_BIG:
+            for(uint i = 0;i < bytes;i++)
+                accum = (accum<<8) | in[i];
+            break;
+        default:
+            break;
+    }
+    *out = accum;
+    return 1;
+}
+
+// Read a binary value of the specified byte order from a file, storing it as
+// a 64-bit unsigned integer.
+static int ReadBin8(std::istream &istream, const char *filename, const ByteOrderT order, uint64_t *out)
+{
+    uint8_t in[8];
+    uint64_t accum;
+    uint i;
+
+    istream.read(reinterpret_cast<char*>(in), 8);
+    if(istream.gcount() != 8)
+    {
+        fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename);
+        return 0;
+    }
+    accum = 0;
+    switch(order)
+    {
+        case BO_LITTLE:
+            for(i = 0;i < 8;i++)
+                accum = (accum<<8) | in[8 - i - 1];
+            break;
+        case BO_BIG:
+            for(i = 0;i < 8;i++)
+                accum = (accum<<8) | in[i];
+            break;
+        default:
+            break;
+    }
+    *out = accum;
+    return 1;
+}
+
+/* Read a binary value of the specified type, byte order, and byte size from
+ * a file, converting it to a double.  For integer types, the significant
+ * bits are used to normalize the result.  The sign of bits determines
+ * whether they are padded toward the MSB (negative) or LSB (positive).
+ * Floating-point types are not normalized.
+ */
+static int ReadBinAsDouble(std::istream &istream, const char *filename, const ByteOrderT order,
+    const ElementTypeT type, const uint bytes, const int bits, double *out)
+{
+    union {
+        uint32_t ui;
+        int32_t i;
+        float f;
+    } v4;
+    union {
+        uint64_t ui;
+        double f;
+    } v8;
+
+    *out = 0.0;
+    if(bytes > 4)
+    {
+        if(!ReadBin8(istream, filename, order, &v8.ui))
+            return 0;
+        if(type == ET_FP)
+            *out = v8.f;
+    }
+    else
+    {
+        if(!ReadBin4(istream, filename, order, bytes, &v4.ui))
+            return 0;
+        if(type == ET_FP)
+            *out = v4.f;
+        else
+        {
+            if(bits > 0)
+                v4.ui >>= (8*bytes) - (static_cast<uint>(bits));
+            else
+                v4.ui &= (0xFFFFFFFF >> (32+bits));
+
+            if(v4.ui&static_cast<uint>(1<<(std::abs(bits)-1)))
+                v4.ui |= (0xFFFFFFFF << std::abs(bits));
+            *out = v4.i / static_cast<double>(1<<(std::abs(bits)-1));
+        }
+    }
+    return 1;
+}
+
+/* Read an ascii value of the specified type from a file, converting it to a
+ * double.  For integer types, the significant bits are used to normalize the
+ * result.  The sign of the bits should always be positive.  This also skips
+ * up to one separator character before the element itself.
+ */
+static int ReadAsciiAsDouble(TokenReaderT *tr, const char *filename, const ElementTypeT type, const uint bits, double *out)
+{
+    if(TrIsOperator(tr, ","))
+        TrReadOperator(tr, ",");
+    else if(TrIsOperator(tr, ":"))
+        TrReadOperator(tr, ":");
+    else if(TrIsOperator(tr, ";"))
+        TrReadOperator(tr, ";");
+    else if(TrIsOperator(tr, "|"))
+        TrReadOperator(tr, "|");
+
+    if(type == ET_FP)
+    {
+        if(!TrReadFloat(tr, -std::numeric_limits<double>::infinity(),
+            std::numeric_limits<double>::infinity(), out))
+        {
+            fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename);
+            return 0;
+        }
+    }
+    else
+    {
+        int v;
+        if(!TrReadInt(tr, -(1<<(bits-1)), (1<<(bits-1))-1, &v))
+        {
+            fprintf(stderr, "\nError: Bad read from file '%s'.\n", filename);
+            return 0;
+        }
+        *out = v / static_cast<double>((1<<(bits-1))-1);
+    }
+    return 1;
+}
+
+// Read the RIFF/RIFX WAVE format chunk from a file, validating it against
+// the source parameters and data set metrics.
+static int ReadWaveFormat(std::istream &istream, const ByteOrderT order, const uint hrirRate,
+    SourceRefT *src)
+{
+    uint32_t fourCC, chunkSize;
+    uint32_t format, channels, rate, dummy, block, size, bits;
+
+    chunkSize = 0;
+    do {
+        if(chunkSize > 0)
+            istream.seekg(static_cast<int>(chunkSize), std::ios::cur);
+        if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC)
+            || !ReadBin4(istream, src->mPath, order, 4, &chunkSize))
+            return 0;
+    } while(fourCC != FOURCC_FMT);
+    if(!ReadBin4(istream, src->mPath, order, 2, &format)
+        || !ReadBin4(istream, src->mPath, order, 2, &channels)
+        || !ReadBin4(istream, src->mPath, order, 4, &rate)
+        || !ReadBin4(istream, src->mPath, order, 4, &dummy)
+        || !ReadBin4(istream, src->mPath, order, 2, &block))
+        return 0;
+    block /= channels;
+    if(chunkSize > 14)
+    {
+        if(!ReadBin4(istream, src->mPath, order, 2, &size))
+            return 0;
+        size /= 8;
+        if(block > size)
+            size = block;
+    }
+    else
+        size = block;
+    if(format == WAVE_FORMAT_EXTENSIBLE)
+    {
+        istream.seekg(2, std::ios::cur);
+        if(!ReadBin4(istream, src->mPath, order, 2, &bits))
+            return 0;
+        if(bits == 0)
+            bits = 8 * size;
+        istream.seekg(4, std::ios::cur);
+        if(!ReadBin4(istream, src->mPath, order, 2, &format))
+            return 0;
+        istream.seekg(static_cast<int>(chunkSize - 26), std::ios::cur);
+    }
+    else
+    {
+        bits = 8 * size;
+        if(chunkSize > 14)
+            istream.seekg(static_cast<int>(chunkSize - 16), std::ios::cur);
+        else
+            istream.seekg(static_cast<int>(chunkSize - 14), std::ios::cur);
+    }
+    if(format != WAVE_FORMAT_PCM && format != WAVE_FORMAT_IEEE_FLOAT)
+    {
+        fprintf(stderr, "\nError: Unsupported WAVE format in file '%s'.\n", src->mPath);
+        return 0;
+    }
+    if(src->mChannel >= channels)
+    {
+        fprintf(stderr, "\nError: Missing source channel in WAVE file '%s'.\n", src->mPath);
+        return 0;
+    }
+    if(rate != hrirRate)
+    {
+        fprintf(stderr, "\nError: Mismatched source sample rate in WAVE file '%s'.\n", src->mPath);
+        return 0;
+    }
+    if(format == WAVE_FORMAT_PCM)
+    {
+        if(size < 2 || size > 4)
+        {
+            fprintf(stderr, "\nError: Unsupported sample size in WAVE file '%s'.\n", src->mPath);
+            return 0;
+        }
+        if(bits < 16 || bits > (8*size))
+        {
+            fprintf(stderr, "\nError: Bad significant bits in WAVE file '%s'.\n", src->mPath);
+            return 0;
+        }
+        src->mType = ET_INT;
+    }
+    else
+    {
+        if(size != 4 && size != 8)
+        {
+            fprintf(stderr, "\nError: Unsupported sample size in WAVE file '%s'.\n", src->mPath);
+            return 0;
+        }
+        src->mType = ET_FP;
+    }
+    src->mSize = size;
+    src->mBits = static_cast<int>(bits);
+    src->mSkip = channels;
+    return 1;
+}
+
+// Read a RIFF/RIFX WAVE data chunk, converting all elements to doubles.
+static int ReadWaveData(std::istream &istream, const SourceRefT *src, const ByteOrderT order,
+    const uint n, double *hrir)
+{
+    int pre, post, skip;
+    uint i;
+
+    pre = static_cast<int>(src->mSize * src->mChannel);
+    post = static_cast<int>(src->mSize * (src->mSkip - src->mChannel - 1));
+    skip = 0;
+    for(i = 0;i < n;i++)
+    {
+        skip += pre;
+        if(skip > 0)
+            istream.seekg(skip, std::ios::cur);
+        if(!ReadBinAsDouble(istream, src->mPath, order, src->mType, src->mSize, src->mBits, &hrir[i]))
+            return 0;
+        skip = post;
+    }
+    if(skip > 0)
+        istream.seekg(skip, std::ios::cur);
+    return 1;
+}
+
+// Read the RIFF/RIFX WAVE list or data chunk, converting all elements to
+// doubles.
+static int ReadWaveList(std::istream &istream, const SourceRefT *src, const ByteOrderT order,
+    const uint n, double *hrir)
+{
+    uint32_t fourCC, chunkSize, listSize, count;
+    uint block, skip, offset, i;
+    double lastSample;
+
+    for(;;)
+    {
+        if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC)
+            || !ReadBin4(istream, src->mPath, order, 4, &chunkSize))
+            return 0;
+
+        if(fourCC == FOURCC_DATA)
+        {
+            block = src->mSize * src->mSkip;
+            count = chunkSize / block;
+            if(count < (src->mOffset + n))
+            {
+                fprintf(stderr, "\nError: Bad read from file '%s'.\n", src->mPath);
+                return 0;
+            }
+            istream.seekg(static_cast<long>(src->mOffset * block), std::ios::cur);
+            if(!ReadWaveData(istream, src, order, n, &hrir[0]))
+                return 0;
+            return 1;
+        }
+        else if(fourCC == FOURCC_LIST)
+        {
+            if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC))
+                return 0;
+            chunkSize -= 4;
+            if(fourCC == FOURCC_WAVL)
+                break;
+        }
+        if(chunkSize > 0)
+            istream.seekg(static_cast<long>(chunkSize), std::ios::cur);
+    }
+    listSize = chunkSize;
+    block = src->mSize * src->mSkip;
+    skip = src->mOffset;
+    offset = 0;
+    lastSample = 0.0;
+    while(offset < n && listSize > 8)
+    {
+        if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC)
+            || !ReadBin4(istream, src->mPath, order, 4, &chunkSize))
+            return 0;
+        listSize -= 8 + chunkSize;
+        if(fourCC == FOURCC_DATA)
+        {
+            count = chunkSize / block;
+            if(count > skip)
+            {
+                istream.seekg(static_cast<long>(skip * block), std::ios::cur);
+                chunkSize -= skip * block;
+                count -= skip;
+                skip = 0;
+                if(count > (n - offset))
+                    count = n - offset;
+                if(!ReadWaveData(istream, src, order, count, &hrir[offset]))
+                    return 0;
+                chunkSize -= count * block;
+                offset += count;
+                lastSample = hrir[offset - 1];
+            }
+            else
+            {
+                skip -= count;
+                count = 0;
+            }
+        }
+        else if(fourCC == FOURCC_SLNT)
+        {
+            if(!ReadBin4(istream, src->mPath, order, 4, &count))
+                return 0;
+            chunkSize -= 4;
+            if(count > skip)
+            {
+                count -= skip;
+                skip = 0;
+                if(count > (n - offset))
+                    count = n - offset;
+                for(i = 0; i < count; i ++)
+                    hrir[offset + i] = lastSample;
+                offset += count;
+            }
+            else
+            {
+                skip -= count;
+                count = 0;
+            }
+        }
+        if(chunkSize > 0)
+            istream.seekg(static_cast<long>(chunkSize), std::ios::cur);
+    }
+    if(offset < n)
+    {
+        fprintf(stderr, "\nError: Bad read from file '%s'.\n", src->mPath);
+        return 0;
+    }
+    return 1;
+}
+
+// Load a source HRIR from an ASCII text file containing a list of elements
+// separated by whitespace or common list operators (',', ';', ':', '|').
+static int LoadAsciiSource(std::istream &istream, const SourceRefT *src,
+    const uint n, double *hrir)
+{
+    TokenReaderT tr{istream};
+    uint i, j;
+    double dummy;
+
+    TrSetup(nullptr, 0, nullptr, &tr);
+    for(i = 0;i < src->mOffset;i++)
+    {
+        if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, static_cast<uint>(src->mBits), &dummy))
+            return 0;
+    }
+    for(i = 0;i < n;i++)
+    {
+        if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, static_cast<uint>(src->mBits), &hrir[i]))
+            return 0;
+        for(j = 0;j < src->mSkip;j++)
+        {
+            if(!ReadAsciiAsDouble(&tr, src->mPath, src->mType, static_cast<uint>(src->mBits), &dummy))
+                return 0;
+        }
+    }
+    return 1;
+}
+
+// Load a source HRIR from a binary file.
+static int LoadBinarySource(std::istream &istream, const SourceRefT *src, const ByteOrderT order,
+    const uint n, double *hrir)
+{
+    istream.seekg(static_cast<long>(src->mOffset), std::ios::beg);
+    for(uint i{0};i < n;i++)
+    {
+        if(!ReadBinAsDouble(istream, src->mPath, order, src->mType, src->mSize, src->mBits, &hrir[i]))
+            return 0;
+        if(src->mSkip > 0)
+            istream.seekg(static_cast<long>(src->mSkip), std::ios::cur);
+    }
+    return 1;
+}
+
+// Load a source HRIR from a RIFF/RIFX WAVE file.
+static int LoadWaveSource(std::istream &istream, SourceRefT *src, const uint hrirRate,
+    const uint n, double *hrir)
+{
+    uint32_t fourCC, dummy;
+    ByteOrderT order;
+
+    if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC)
+        || !ReadBin4(istream, src->mPath, BO_LITTLE, 4, &dummy))
+        return 0;
+    if(fourCC == FOURCC_RIFF)
+        order = BO_LITTLE;
+    else if(fourCC == FOURCC_RIFX)
+        order = BO_BIG;
+    else
+    {
+        fprintf(stderr, "\nError: No RIFF/RIFX chunk in file '%s'.\n", src->mPath);
+        return 0;
+    }
+
+    if(!ReadBin4(istream, src->mPath, BO_LITTLE, 4, &fourCC))
+        return 0;
+    if(fourCC != FOURCC_WAVE)
+    {
+        fprintf(stderr, "\nError: Not a RIFF/RIFX WAVE file '%s'.\n", src->mPath);
+        return 0;
+    }
+    if(!ReadWaveFormat(istream, order, hrirRate, src))
+        return 0;
+    if(!ReadWaveList(istream, src, order, n, hrir))
+        return 0;
+    return 1;
+}
+
+
+
+// Load a Spatially Oriented Format for Accoustics (SOFA) file.
+static MYSOFA_EASY* LoadSofaFile(SourceRefT *src, const uint hrirRate, const uint n)
+{
+    struct MYSOFA_EASY *sofa{mysofa_cache_lookup(src->mPath, static_cast<float>(hrirRate))};
+    if(sofa) return sofa;
+
+    sofa = static_cast<MYSOFA_EASY*>(calloc(1, sizeof(*sofa)));
+    if(sofa == nullptr)
+    {
+        fprintf(stderr, "\nError:  Out of memory.\n");
+        return nullptr;
+    }
+    sofa->lookup = nullptr;
+    sofa->neighborhood = nullptr;
+
+    int err;
+    sofa->hrtf = mysofa_load(src->mPath, &err);
+    if(!sofa->hrtf)
+    {
+        mysofa_close(sofa);
+        fprintf(stderr, "\nError: Could not load source file '%s'.\n", src->mPath);
+        return nullptr;
+    }
+    /* NOTE: Some valid SOFA files are failing this check. */
+    err = mysofa_check(sofa->hrtf);
+    if(err != MYSOFA_OK)
+        fprintf(stderr, "\nWarning: Supposedly malformed source file '%s'.\n", src->mPath);
+    if((src->mOffset + n) > sofa->hrtf->N)
+    {
+        mysofa_close(sofa);
+        fprintf(stderr, "\nError: Not enough samples in SOFA file '%s'.\n", src->mPath);
+        return nullptr;
+    }
+    if(src->mChannel >= sofa->hrtf->R)
+    {
+        mysofa_close(sofa);
+        fprintf(stderr, "\nError: Missing source receiver in SOFA file '%s'.\n", src->mPath);
+        return nullptr;
+    }
+    mysofa_tocartesian(sofa->hrtf);
+    sofa->lookup = mysofa_lookup_init(sofa->hrtf);
+    if(sofa->lookup == nullptr)
+    {
+        mysofa_close(sofa);
+        fprintf(stderr, "\nError:  Out of memory.\n");
+        return nullptr;
+    }
+    return mysofa_cache_store(sofa, src->mPath, static_cast<float>(hrirRate));
+}
+
+// Copies the HRIR data from a particular SOFA measurement.
+static void ExtractSofaHrir(const MYSOFA_EASY *sofa, const uint index, const uint channel, const uint offset, const uint n, double *hrir)
+{
+    for(uint i{0u};i < n;i++)
+        hrir[i] = sofa->hrtf->DataIR.values[(index*sofa->hrtf->R + channel)*sofa->hrtf->N + offset + i];
+}
+
+// Load a source HRIR from a Spatially Oriented Format for Accoustics (SOFA)
+// file.
+static int LoadSofaSource(SourceRefT *src, const uint hrirRate, const uint n, double *hrir)
+{
+    struct MYSOFA_EASY *sofa;
+    float target[3];
+    int nearest;
+    float *coords;
+
+    sofa = LoadSofaFile(src, hrirRate, n);
+    if(sofa == nullptr)
+        return 0;
+
+    /* NOTE: At some point it may be benficial or necessary to consider the
+             various coordinate systems, listener/source orientations, and
+             direciontal vectors defined in the SOFA file.
+    */
+    target[0] = static_cast<float>(src->mAzimuth);
+    target[1] = static_cast<float>(src->mElevation);
+    target[2] = static_cast<float>(src->mRadius);
+    mysofa_s2c(target);
+
+    nearest = mysofa_lookup(sofa->lookup, target);
+    if(nearest < 0)
+    {
+        fprintf(stderr, "\nError: Lookup failed in source file '%s'.\n", src->mPath);
+        return 0;
+    }
+
+    coords = &sofa->hrtf->SourcePosition.values[3 * nearest];
+    if(std::abs(coords[0] - target[0]) > 0.001 || std::abs(coords[1] - target[1]) > 0.001 || std::abs(coords[2] - target[2]) > 0.001)
+    {
+        fprintf(stderr, "\nError: No impulse response at coordinates (%.3fr, %.1fev, %.1faz) in file '%s'.\n", src->mRadius, src->mElevation, src->mAzimuth, src->mPath);
+        target[0] = coords[0];
+        target[1] = coords[1];
+        target[2] = coords[2];
+        mysofa_c2s(target);
+        fprintf(stderr, "       Nearest candidate at (%.3fr, %.1fev, %.1faz).\n", target[2], target[1], target[0]);
+        return 0;
+    }
+
+    ExtractSofaHrir(sofa, static_cast<uint>(nearest), src->mChannel, src->mOffset, n, hrir);
+
+    return 1;
+}
+
+// Load a source HRIR from a supported file type.
+static int LoadSource(SourceRefT *src, const uint hrirRate, const uint n, double *hrir)
+{
+    std::unique_ptr<al::ifstream> istream;
+    if(src->mFormat != SF_SOFA)
+    {
+        if(src->mFormat == SF_ASCII)
+            istream.reset(new al::ifstream{src->mPath});
+        else
+            istream.reset(new al::ifstream{src->mPath, std::ios::binary});
+        if(!istream->good())
+        {
+            fprintf(stderr, "\nError: Could not open source file '%s'.\n", src->mPath);
+            return 0;
+        }
+    }
+    int result{0};
+    switch(src->mFormat)
+    {
+        case SF_ASCII:
+            result = LoadAsciiSource(*istream, src, n, hrir);
+            break;
+        case SF_BIN_LE:
+            result = LoadBinarySource(*istream, src, BO_LITTLE, n, hrir);
+            break;
+        case SF_BIN_BE:
+            result = LoadBinarySource(*istream, src, BO_BIG, n, hrir);
+            break;
+        case SF_WAVE:
+            result = LoadWaveSource(*istream, src, hrirRate, n, hrir);
+            break;
+        case SF_SOFA:
+            result = LoadSofaSource(src, hrirRate, n, hrir);
+            break;
+        case SF_NONE:
+            break;
+    }
+    return result;
+}
+
+
+// Match the channel type from a given identifier.
+static ChannelTypeT MatchChannelType(const char *ident)
+{
+    if(al::strcasecmp(ident, "mono") == 0)
+        return CT_MONO;
+    if(al::strcasecmp(ident, "stereo") == 0)
+        return CT_STEREO;
+    return CT_NONE;
+}
+
+
+// Process the data set definition to read and validate the data set metrics.
+static int ProcessMetrics(TokenReaderT *tr, const uint fftSize, const uint truncSize, const ChannelModeT chanMode, HrirDataT *hData)
+{
+    int hasRate = 0, hasType = 0, hasPoints = 0, hasRadius = 0;
+    int hasDistance = 0, hasAzimuths = 0;
+    char ident[MAX_IDENT_LEN+1];
+    uint line, col;
+    double fpVal;
+    uint points;
+    int intVal;
+    double distances[MAX_FD_COUNT];
+    uint fdCount = 0;
+    uint evCounts[MAX_FD_COUNT];
+    auto azCounts = std::vector<std::array<uint,MAX_EV_COUNT>>(MAX_FD_COUNT);
+    for(auto &azs : azCounts) azs.fill(0u);
+
+    TrIndication(tr, &line, &col);
+    while(TrIsIdent(tr))
+    {
+        TrIndication(tr, &line, &col);
+        if(!TrReadIdent(tr, MAX_IDENT_LEN, ident))
+            return 0;
+        if(al::strcasecmp(ident, "rate") == 0)
+        {
+            if(hasRate)
+            {
+                TrErrorAt(tr, line, col, "Redefinition of 'rate'.\n");
+                return 0;
+            }
+            if(!TrReadOperator(tr, "="))
+                return 0;
+            if(!TrReadInt(tr, MIN_RATE, MAX_RATE, &intVal))
+                return 0;
+            hData->mIrRate = static_cast<uint>(intVal);
+            hasRate = 1;
+        }
+        else if(al::strcasecmp(ident, "type") == 0)
+        {
+            char type[MAX_IDENT_LEN+1];
+
+            if(hasType)
+            {
+                TrErrorAt(tr, line, col, "Redefinition of 'type'.\n");
+                return 0;
+            }
+            if(!TrReadOperator(tr, "="))
+                return 0;
+
+            if(!TrReadIdent(tr, MAX_IDENT_LEN, type))
+                return 0;
+            hData->mChannelType = MatchChannelType(type);
+            if(hData->mChannelType == CT_NONE)
+            {
+                TrErrorAt(tr, line, col, "Expected a channel type.\n");
+                return 0;
+            }
+            else if(hData->mChannelType == CT_STEREO)
+            {
+                if(chanMode == CM_ForceMono)
+                    hData->mChannelType = CT_MONO;
+            }
+            hasType = 1;
+        }
+        else if(al::strcasecmp(ident, "points") == 0)
+        {
+            if(hasPoints)
+            {
+                TrErrorAt(tr, line, col, "Redefinition of 'points'.\n");
+                return 0;
+            }
+            if(!TrReadOperator(tr, "="))
+                return 0;
+            TrIndication(tr, &line, &col);
+            if(!TrReadInt(tr, MIN_POINTS, MAX_POINTS, &intVal))
+                return 0;
+            points = static_cast<uint>(intVal);
+            if(fftSize > 0 && points > fftSize)
+            {
+                TrErrorAt(tr, line, col, "Value exceeds the overridden FFT size.\n");
+                return 0;
+            }
+            if(points < truncSize)
+            {
+                TrErrorAt(tr, line, col, "Value is below the truncation size.\n");
+                return 0;
+            }
+            hData->mIrPoints = points;
+            hData->mFftSize = fftSize;
+            hData->mIrSize = 1 + (fftSize / 2);
+            if(points > hData->mIrSize)
+                hData->mIrSize = points;
+            hasPoints = 1;
+        }
+        else if(al::strcasecmp(ident, "radius") == 0)
+        {
+            if(hasRadius)
+            {
+                TrErrorAt(tr, line, col, "Redefinition of 'radius'.\n");
+                return 0;
+            }
+            if(!TrReadOperator(tr, "="))
+                return 0;
+            if(!TrReadFloat(tr, MIN_RADIUS, MAX_RADIUS, &fpVal))
+                return 0;
+            hData->mRadius = fpVal;
+            hasRadius = 1;
+        }
+        else if(al::strcasecmp(ident, "distance") == 0)
+        {
+            uint count = 0;
+
+            if(hasDistance)
+            {
+                TrErrorAt(tr, line, col, "Redefinition of 'distance'.\n");
+                return 0;
+            }
+            if(!TrReadOperator(tr, "="))
+                return 0;
+
+            for(;;)
+            {
+                if(!TrReadFloat(tr, MIN_DISTANCE, MAX_DISTANCE, &fpVal))
+                    return 0;
+                if(count > 0 && fpVal <= distances[count - 1])
+                {
+                    TrError(tr, "Distances are not ascending.\n");
+                    return 0;
+                }
+                distances[count++] = fpVal;
+                if(!TrIsOperator(tr, ","))
+                    break;
+                if(count >= MAX_FD_COUNT)
+                {
+                    TrError(tr, "Exceeded the maximum of %d fields.\n", MAX_FD_COUNT);
+                    return 0;
+                }
+                TrReadOperator(tr, ",");
+            }
+            if(fdCount != 0 && count != fdCount)
+            {
+                TrError(tr, "Did not match the specified number of %d fields.\n", fdCount);
+                return 0;
+            }
+            fdCount = count;
+            hasDistance = 1;
+        }
+        else if(al::strcasecmp(ident, "azimuths") == 0)
+        {
+            uint count = 0;
+
+            if(hasAzimuths)
+            {
+                TrErrorAt(tr, line, col, "Redefinition of 'azimuths'.\n");
+                return 0;
+            }
+            if(!TrReadOperator(tr, "="))
+                return 0;
+
+            evCounts[0] = 0;
+            for(;;)
+            {
+                if(!TrReadInt(tr, MIN_AZ_COUNT, MAX_AZ_COUNT, &intVal))
+                    return 0;
+                azCounts[count][evCounts[count]++] = static_cast<uint>(intVal);
+                if(TrIsOperator(tr, ","))
+                {
+                    if(evCounts[count] >= MAX_EV_COUNT)
+                    {
+                        TrError(tr, "Exceeded the maximum of %d elevations.\n", MAX_EV_COUNT);
+                        return 0;
+                    }
+                    TrReadOperator(tr, ",");
+                }
+                else
+                {
+                    if(evCounts[count] < MIN_EV_COUNT)
+                    {
+                        TrErrorAt(tr, line, col, "Did not reach the minimum of %d azimuth counts.\n", MIN_EV_COUNT);
+                        return 0;
+                    }
+                    if(azCounts[count][0] != 1 || azCounts[count][evCounts[count] - 1] != 1)
+                    {
+                        TrError(tr, "Poles are not singular for field %d.\n", count - 1);
+                        return 0;
+                    }
+                    count++;
+                    if(!TrIsOperator(tr, ";"))
+                        break;
+
+                    if(count >= MAX_FD_COUNT)
+                    {
+                        TrError(tr, "Exceeded the maximum number of %d fields.\n", MAX_FD_COUNT);
+                        return 0;
+                    }
+                    evCounts[count] = 0;
+                    TrReadOperator(tr, ";");
+                }
+            }
+            if(fdCount != 0 && count != fdCount)
+            {
+                TrError(tr, "Did not match the specified number of %d fields.\n", fdCount);
+                return 0;
+            }
+            fdCount = count;
+            hasAzimuths = 1;
+        }
+        else
+        {
+            TrErrorAt(tr, line, col, "Expected a metric name.\n");
+            return 0;
+        }
+        TrSkipWhitespace(tr);
+    }
+    if(!(hasRate && hasPoints && hasRadius && hasDistance && hasAzimuths))
+    {
+        TrErrorAt(tr, line, col, "Expected a metric name.\n");
+        return 0;
+    }
+    if(distances[0] < hData->mRadius)
+    {
+        TrError(tr, "Distance cannot start below head radius.\n");
+        return 0;
+    }
+    if(hData->mChannelType == CT_NONE)
+        hData->mChannelType = CT_MONO;
+    const auto azs = al::as_span(azCounts).first<MAX_FD_COUNT>();
+    if(!PrepareHrirData({distances, fdCount}, evCounts, azs, hData))
+    {
+        fprintf(stderr, "Error:  Out of memory.\n");
+        exit(-1);
+    }
+    return 1;
+}
+
+// Parse an index triplet from the data set definition.
+static int ReadIndexTriplet(TokenReaderT *tr, const HrirDataT *hData, uint *fi, uint *ei, uint *ai)
+{
+    int intVal;
+
+    if(hData->mFds.size() > 1)
+    {
+        if(!TrReadInt(tr, 0, static_cast<int>(hData->mFds.size()-1), &intVal))
+            return 0;
+        *fi = static_cast<uint>(intVal);
+        if(!TrReadOperator(tr, ","))
+            return 0;
+    }
+    else
+    {
+        *fi = 0;
+    }
+    if(!TrReadInt(tr, 0, static_cast<int>(hData->mFds[*fi].mEvs.size()-1), &intVal))
+        return 0;
+    *ei = static_cast<uint>(intVal);
+    if(!TrReadOperator(tr, ","))
+        return 0;
+    if(!TrReadInt(tr, 0, static_cast<int>(hData->mFds[*fi].mEvs[*ei].mAzs.size()-1), &intVal))
+        return 0;
+    *ai = static_cast<uint>(intVal);
+    return 1;
+}
+
+// Match the source format from a given identifier.
+static SourceFormatT MatchSourceFormat(const char *ident)
+{
+    if(al::strcasecmp(ident, "ascii") == 0)
+        return SF_ASCII;
+    if(al::strcasecmp(ident, "bin_le") == 0)
+        return SF_BIN_LE;
+    if(al::strcasecmp(ident, "bin_be") == 0)
+        return SF_BIN_BE;
+    if(al::strcasecmp(ident, "wave") == 0)
+        return SF_WAVE;
+    if(al::strcasecmp(ident, "sofa") == 0)
+        return SF_SOFA;
+    return SF_NONE;
+}
+
+// Match the source element type from a given identifier.
+static ElementTypeT MatchElementType(const char *ident)
+{
+    if(al::strcasecmp(ident, "int") == 0)
+        return ET_INT;
+    if(al::strcasecmp(ident, "fp") == 0)
+        return ET_FP;
+    return ET_NONE;
+}
+
+// Parse and validate a source reference from the data set definition.
+static int ReadSourceRef(TokenReaderT *tr, SourceRefT *src)
+{
+    char ident[MAX_IDENT_LEN+1];
+    uint line, col;
+    double fpVal;
+    int intVal;
+
+    TrIndication(tr, &line, &col);
+    if(!TrReadIdent(tr, MAX_IDENT_LEN, ident))
+        return 0;
+    src->mFormat = MatchSourceFormat(ident);
+    if(src->mFormat == SF_NONE)
+    {
+        TrErrorAt(tr, line, col, "Expected a source format.\n");
+        return 0;
+    }
+    if(!TrReadOperator(tr, "("))
+        return 0;
+    if(src->mFormat == SF_SOFA)
+    {
+        if(!TrReadFloat(tr, MIN_DISTANCE, MAX_DISTANCE, &fpVal))
+            return 0;
+        src->mRadius = fpVal;
+        if(!TrReadOperator(tr, ","))
+            return 0;
+        if(!TrReadFloat(tr, -90.0, 90.0, &fpVal))
+            return 0;
+        src->mElevation = fpVal;
+        if(!TrReadOperator(tr, ","))
+            return 0;
+        if(!TrReadFloat(tr, -360.0, 360.0, &fpVal))
+            return 0;
+        src->mAzimuth = fpVal;
+        if(!TrReadOperator(tr, ":"))
+            return 0;
+        if(!TrReadInt(tr, 0, MAX_WAVE_CHANNELS, &intVal))
+            return 0;
+        src->mType = ET_NONE;
+        src->mSize = 0;
+        src->mBits = 0;
+        src->mChannel = static_cast<uint>(intVal);
+        src->mSkip = 0;
+    }
+    else if(src->mFormat == SF_WAVE)
+    {
+        if(!TrReadInt(tr, 0, MAX_WAVE_CHANNELS, &intVal))
+            return 0;
+        src->mType = ET_NONE;
+        src->mSize = 0;
+        src->mBits = 0;
+        src->mChannel = static_cast<uint>(intVal);
+        src->mSkip = 0;
+    }
+    else
+    {
+        TrIndication(tr, &line, &col);
+        if(!TrReadIdent(tr, MAX_IDENT_LEN, ident))
+            return 0;
+        src->mType = MatchElementType(ident);
+        if(src->mType == ET_NONE)
+        {
+            TrErrorAt(tr, line, col, "Expected a source element type.\n");
+            return 0;
+        }
+        if(src->mFormat == SF_BIN_LE || src->mFormat == SF_BIN_BE)
+        {
+            if(!TrReadOperator(tr, ","))
+                return 0;
+            if(src->mType == ET_INT)
+            {
+                if(!TrReadInt(tr, MIN_BIN_SIZE, MAX_BIN_SIZE, &intVal))
+                    return 0;
+                src->mSize = static_cast<uint>(intVal);
+                if(!TrIsOperator(tr, ","))
+                    src->mBits = static_cast<int>(8*src->mSize);
+                else
+                {
+                    TrReadOperator(tr, ",");
+                    TrIndication(tr, &line, &col);
+                    if(!TrReadInt(tr, -2147483647-1, 2147483647, &intVal))
+                        return 0;
+                    if(std::abs(intVal) < MIN_BIN_BITS || static_cast<uint>(std::abs(intVal)) > (8*src->mSize))
+                    {
+                        TrErrorAt(tr, line, col, "Expected a value of (+/-) %d to %d.\n", MIN_BIN_BITS, 8*src->mSize);
+                        return 0;
+                    }
+                    src->mBits = intVal;
+                }
+            }
+            else
+            {
+                TrIndication(tr, &line, &col);
+                if(!TrReadInt(tr, -2147483647-1, 2147483647, &intVal))
+                    return 0;
+                if(intVal != 4 && intVal != 8)
+                {
+                    TrErrorAt(tr, line, col, "Expected a value of 4 or 8.\n");
+                    return 0;
+                }
+                src->mSize = static_cast<uint>(intVal);
+                src->mBits = 0;
+            }
+        }
+        else if(src->mFormat == SF_ASCII && src->mType == ET_INT)
+        {
+            if(!TrReadOperator(tr, ","))
+                return 0;
+            if(!TrReadInt(tr, MIN_ASCII_BITS, MAX_ASCII_BITS, &intVal))
+                return 0;
+            src->mSize = 0;
+            src->mBits = intVal;
+        }
+        else
+        {
+            src->mSize = 0;
+            src->mBits = 0;
+        }
+
+        if(!TrIsOperator(tr, ";"))
+            src->mSkip = 0;
+        else
+        {
+            TrReadOperator(tr, ";");
+            if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal))
+                return 0;
+            src->mSkip = static_cast<uint>(intVal);
+        }
+    }
+    if(!TrReadOperator(tr, ")"))
+        return 0;
+    if(TrIsOperator(tr, "@"))
+    {
+        TrReadOperator(tr, "@");
+        if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal))
+            return 0;
+        src->mOffset = static_cast<uint>(intVal);
+    }
+    else
+        src->mOffset = 0;
+    if(!TrReadOperator(tr, ":"))
+        return 0;
+    if(!TrReadString(tr, MAX_PATH_LEN, src->mPath))
+        return 0;
+    return 1;
+}
+
+// Parse and validate a SOFA source reference from the data set definition.
+static int ReadSofaRef(TokenReaderT *tr, SourceRefT *src)
+{
+    char ident[MAX_IDENT_LEN+1];
+    uint line, col;
+    int intVal;
+
+    TrIndication(tr, &line, &col);
+    if(!TrReadIdent(tr, MAX_IDENT_LEN, ident))
+        return 0;
+    src->mFormat = MatchSourceFormat(ident);
+    if(src->mFormat != SF_SOFA)
+    {
+        TrErrorAt(tr, line, col, "Expected the SOFA source format.\n");
+        return 0;
+    }
+
+    src->mType = ET_NONE;
+    src->mSize = 0;
+    src->mBits = 0;
+    src->mChannel = 0;
+    src->mSkip = 0;
+
+    if(TrIsOperator(tr, "@"))
+    {
+        TrReadOperator(tr, "@");
+        if(!TrReadInt(tr, 0, 0x7FFFFFFF, &intVal))
+            return 0;
+        src->mOffset = static_cast<uint>(intVal);
+    }
+    else
+        src->mOffset = 0;
+    if(!TrReadOperator(tr, ":"))
+        return 0;
+    if(!TrReadString(tr, MAX_PATH_LEN, src->mPath))
+        return 0;
+    return 1;
+}
+
+// Match the target ear (index) from a given identifier.
+static int MatchTargetEar(const char *ident)
+{
+    if(al::strcasecmp(ident, "left") == 0)
+        return 0;
+    if(al::strcasecmp(ident, "right") == 0)
+        return 1;
+    return -1;
+}
+
+// Calculate the onset time of an HRIR and average it with any existing
+// timing for its field, elevation, azimuth, and ear.
+static constexpr int OnsetRateMultiple{10};
+static double AverageHrirOnset(PPhaseResampler &rs, al::span<double> upsampled, const uint rate,
+    const uint n, const double *hrir, const double f, const double onset)
+{
+    rs.process(n, hrir, static_cast<uint>(upsampled.size()), upsampled.data());
+
+    auto abs_lt = [](const double &lhs, const double &rhs) -> bool
+    { return std::abs(lhs) < std::abs(rhs); };
+    auto iter = std::max_element(upsampled.cbegin(), upsampled.cend(), abs_lt);
+    return Lerp(onset, static_cast<double>(std::distance(upsampled.cbegin(), iter))/(10*rate), f);
+}
+
+// Calculate the magnitude response of an HRIR and average it with any
+// existing responses for its field, elevation, azimuth, and ear.
+static void AverageHrirMagnitude(const uint points, const uint n, const double *hrir, const double f, double *mag)
+{
+    uint m = 1 + (n / 2), i;
+    std::vector<complex_d> h(n);
+    std::vector<double> r(n);
+
+    for(i = 0;i < points;i++)
+        h[i] = hrir[i];
+    for(;i < n;i++)
+        h[i] = 0.0;
+    FftForward(n, h.data());
+    MagnitudeResponse(n, h.data(), r.data());
+    for(i = 0;i < m;i++)
+        mag[i] = Lerp(mag[i], r[i], f);
+}
+
+// Process the list of sources in the data set definition.
+static int ProcessSources(TokenReaderT *tr, HrirDataT *hData, const uint outRate)
+{
+    const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+    hData->mHrirsBase.resize(channels * hData->mIrCount * hData->mIrSize);
+    double *hrirs = hData->mHrirsBase.data();
+    auto hrir = std::make_unique<double[]>(hData->mIrSize);
+    uint line, col, fi, ei, ai;
+
+    std::vector<double> onsetSamples(OnsetRateMultiple * hData->mIrPoints);
+    PPhaseResampler onsetResampler;
+    onsetResampler.init(hData->mIrRate, OnsetRateMultiple*hData->mIrRate);
+
+    al::optional<PPhaseResampler> resampler;
+    if(outRate && outRate != hData->mIrRate)
+        resampler.emplace().init(hData->mIrRate, outRate);
+    const double rateScale{outRate ? static_cast<double>(outRate) / hData->mIrRate : 1.0};
+    const uint irPoints{outRate
+        ? std::min(static_cast<uint>(std::ceil(hData->mIrPoints*rateScale)), hData->mIrPoints)
+        : hData->mIrPoints};
+
+    printf("Loading sources...");
+    fflush(stdout);
+    int count{0};
+    while(TrIsOperator(tr, "["))
+    {
+        double factor[2]{ 1.0, 1.0 };
+
+        TrIndication(tr, &line, &col);
+        TrReadOperator(tr, "[");
+
+        if(TrIsOperator(tr, "*"))
+        {
+            SourceRefT src;
+            struct MYSOFA_EASY *sofa;
+            uint si;
+
+            TrReadOperator(tr, "*");
+            if(!TrReadOperator(tr, "]") || !TrReadOperator(tr, "="))
+                return 0;
+
+            TrIndication(tr, &line, &col);
+            if(!ReadSofaRef(tr, &src))
+                return 0;
+
+            if(hData->mChannelType == CT_STEREO)
+            {
+                char type[MAX_IDENT_LEN+1];
+                ChannelTypeT channelType;
+
+                if(!TrReadIdent(tr, MAX_IDENT_LEN, type))
+                    return 0;
+
+                channelType = MatchChannelType(type);
+
+                switch(channelType)
+                {
+                    case CT_NONE:
+                        TrErrorAt(tr, line, col, "Expected a channel type.\n");
+                        return 0;
+                    case CT_MONO:
+                        src.mChannel = 0;
+                        break;
+                    case CT_STEREO:
+                        src.mChannel = 1;
+                        break;
+                }
+            }
+            else
+            {
+                char type[MAX_IDENT_LEN+1];
+                ChannelTypeT channelType;
+
+                if(!TrReadIdent(tr, MAX_IDENT_LEN, type))
+                    return 0;
+
+                channelType = MatchChannelType(type);
+                if(channelType != CT_MONO)
+                {
+                    TrErrorAt(tr, line, col, "Expected a mono channel type.\n");
+                    return 0;
+                }
+                src.mChannel = 0;
+            }
+
+            sofa = LoadSofaFile(&src, hData->mIrRate, hData->mIrPoints);
+            if(!sofa) return 0;
+
+            for(si = 0;si < sofa->hrtf->M;si++)
+            {
+                printf("\rLoading sources... %d of %d", si+1, sofa->hrtf->M);
+                fflush(stdout);
+
+                float aer[3] = {
+                    sofa->hrtf->SourcePosition.values[3*si],
+                    sofa->hrtf->SourcePosition.values[3*si + 1],
+                    sofa->hrtf->SourcePosition.values[3*si + 2]
+                };
+                mysofa_c2s(aer);
+
+                if(std::fabs(aer[1]) >= 89.999f)
+                    aer[0] = 0.0f;
+                else
+                    aer[0] = std::fmod(360.0f - aer[0], 360.0f);
+
+                auto field = std::find_if(hData->mFds.cbegin(), hData->mFds.cend(),
+                    [&aer](const HrirFdT &fld) -> bool
+                    { return (std::abs(aer[2] - fld.mDistance) < 0.001); });
+                if(field == hData->mFds.cend())
+                    continue;
+                fi = static_cast<uint>(std::distance(hData->mFds.cbegin(), field));
+
+                const double evscale{180.0 / static_cast<double>(field->mEvs.size()-1)};
+                double ef{(90.0 + aer[1]) / evscale};
+                ei = static_cast<uint>(std::round(ef));
+                ef = (ef - ei) * evscale;
+                if(std::abs(ef) >= 0.1)
+                    continue;
+
+                const double azscale{360.0 / static_cast<double>(field->mEvs[ei].mAzs.size())};
+                double af{aer[0] / azscale};
+                ai = static_cast<uint>(std::round(af));
+                af = (af - ai) * azscale;
+                ai %= static_cast<uint>(field->mEvs[ei].mAzs.size());
+                if(std::abs(af) >= 0.1)
+                    continue;
+
+                HrirAzT *azd = &field->mEvs[ei].mAzs[ai];
+                if(azd->mIrs[0] != nullptr)
+                {
+                    TrErrorAt(tr, line, col, "Redefinition of source [ %d, %d, %d ].\n", fi, ei, ai);
+                    return 0;
+                }
+
+                ExtractSofaHrir(sofa, si, 0, src.mOffset, hData->mIrPoints, hrir.get());
+                azd->mIrs[0] = &hrirs[hData->mIrSize * azd->mIndex];
+                azd->mDelays[0] = AverageHrirOnset(onsetResampler, onsetSamples, hData->mIrRate,
+                    hData->mIrPoints, hrir.get(), 1.0, azd->mDelays[0]);
+                if(resampler)
+                    resampler->process(hData->mIrPoints, hrir.get(), hData->mIrSize, hrir.get());
+                AverageHrirMagnitude(irPoints, hData->mFftSize, hrir.get(), 1.0, azd->mIrs[0]);
+
+                if(src.mChannel == 1)
+                {
+                    ExtractSofaHrir(sofa, si, 1, src.mOffset, hData->mIrPoints, hrir.get());
+                    azd->mIrs[1] = &hrirs[hData->mIrSize * (hData->mIrCount + azd->mIndex)];
+                    azd->mDelays[1] = AverageHrirOnset(onsetResampler, onsetSamples,
+                        hData->mIrRate, hData->mIrPoints, hrir.get(), 1.0, azd->mDelays[1]);
+                    if(resampler)
+                        resampler->process(hData->mIrPoints, hrir.get(), hData->mIrSize,
+                            hrir.get());
+                    AverageHrirMagnitude(irPoints, hData->mFftSize, hrir.get(), 1.0, azd->mIrs[1]);
+                }
+
+                // TODO: Since some SOFA files contain minimum phase HRIRs,
+                // it would be beneficial to check for per-measurement delays
+                // (when available) to reconstruct the HRTDs.
+            }
+
+            continue;
+        }
+
+        if(!ReadIndexTriplet(tr, hData, &fi, &ei, &ai))
+            return 0;
+        if(!TrReadOperator(tr, "]"))
+            return 0;
+        HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
+
+        if(azd->mIrs[0] != nullptr)
+        {
+            TrErrorAt(tr, line, col, "Redefinition of source.\n");
+            return 0;
+        }
+        if(!TrReadOperator(tr, "="))
+            return 0;
+
+        for(;;)
+        {
+            SourceRefT src;
+
+            if(!ReadSourceRef(tr, &src))
+                return 0;
+
+            // TODO: Would be nice to display 'x of y files', but that would
+            // require preparing the source refs first to get a total count
+            // before loading them.
+            ++count;
+            printf("\rLoading sources... %d file%s", count, (count==1)?"":"s");
+            fflush(stdout);
+
+            if(!LoadSource(&src, hData->mIrRate, hData->mIrPoints, hrir.get()))
+                return 0;
+
+            uint ti{0};
+            if(hData->mChannelType == CT_STEREO)
+            {
+                char ident[MAX_IDENT_LEN+1];
+
+                if(!TrReadIdent(tr, MAX_IDENT_LEN, ident))
+                    return 0;
+                ti = static_cast<uint>(MatchTargetEar(ident));
+                if(static_cast<int>(ti) < 0)
+                {
+                    TrErrorAt(tr, line, col, "Expected a target ear.\n");
+                    return 0;
+                }
+            }
+            azd->mIrs[ti] = &hrirs[hData->mIrSize * (ti * hData->mIrCount + azd->mIndex)];
+            azd->mDelays[ti] = AverageHrirOnset(onsetResampler, onsetSamples, hData->mIrRate,
+                hData->mIrPoints, hrir.get(), 1.0 / factor[ti], azd->mDelays[ti]);
+            if(resampler)
+                resampler->process(hData->mIrPoints, hrir.get(), hData->mIrSize, hrir.get());
+            AverageHrirMagnitude(irPoints, hData->mFftSize, hrir.get(), 1.0 / factor[ti],
+                azd->mIrs[ti]);
+            factor[ti] += 1.0;
+            if(!TrIsOperator(tr, "+"))
+                break;
+            TrReadOperator(tr, "+");
+        }
+        if(hData->mChannelType == CT_STEREO)
+        {
+            if(azd->mIrs[0] == nullptr)
+            {
+                TrErrorAt(tr, line, col, "Missing left ear source reference(s).\n");
+                return 0;
+            }
+            else if(azd->mIrs[1] == nullptr)
+            {
+                TrErrorAt(tr, line, col, "Missing right ear source reference(s).\n");
+                return 0;
+            }
+        }
+    }
+    printf("\n");
+    hrir = nullptr;
+    if(resampler)
+    {
+        hData->mIrRate = outRate;
+        hData->mIrPoints = irPoints;
+        resampler.reset();
+    }
+    for(fi = 0;fi < hData->mFds.size();fi++)
+    {
+        for(ei = 0;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+            {
+                HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
+                if(azd->mIrs[0] != nullptr)
+                    break;
+            }
+            if(ai < hData->mFds[fi].mEvs[ei].mAzs.size())
+                break;
+        }
+        if(ei >= hData->mFds[fi].mEvs.size())
+        {
+            TrError(tr, "Missing source references [ %d, *, * ].\n", fi);
+            return 0;
+        }
+        hData->mFds[fi].mEvStart = ei;
+        for(;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+            {
+                HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
+
+                if(azd->mIrs[0] == nullptr)
+                {
+                    TrError(tr, "Missing source reference [ %d, %d, %d ].\n", fi, ei, ai);
+                    return 0;
+                }
+            }
+        }
+    }
+    for(uint ti{0};ti < channels;ti++)
+    {
+        for(fi = 0;fi < hData->mFds.size();fi++)
+        {
+            for(ei = 0;ei < hData->mFds[fi].mEvs.size();ei++)
+            {
+                for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+                {
+                    HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
+
+                    azd->mIrs[ti] = &hrirs[hData->mIrSize * (ti * hData->mIrCount + azd->mIndex)];
+                }
+            }
+        }
+    }
+    if(!TrLoad(tr))
+    {
+        mysofa_cache_release_all();
+        return 1;
+    }
+
+    TrError(tr, "Errant data at end of source list.\n");
+    mysofa_cache_release_all();
+    return 0;
+}
+
+
+bool LoadDefInput(std::istream &istream, const char *startbytes, std::streamsize startbytecount,
+    const char *filename, const uint fftSize, const uint truncSize, const uint outRate,
+    const ChannelModeT chanMode, HrirDataT *hData)
+{
+    TokenReaderT tr{istream};
+
+    TrSetup(startbytes, startbytecount, filename, &tr);
+    if(!ProcessMetrics(&tr, fftSize, truncSize, chanMode, hData)
+        || !ProcessSources(&tr, hData, outRate))
+        return false;
+
+    return true;
+}
diff --git a/utils/makemhr/loaddef.h b/utils/makemhr/loaddef.h
new file mode 100644 (file)
index 0000000..63600dc
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef LOADDEF_H
+#define LOADDEF_H
+
+#include <istream>
+
+#include "makemhr.h"
+
+
+bool LoadDefInput(std::istream &istream, const char *startbytes, std::streamsize startbytecount,
+    const char *filename, const uint fftSize, const uint truncSize, const uint outRate,
+    const ChannelModeT chanMode, HrirDataT *hData);
+
+#endif /* LOADDEF_H */
diff --git a/utils/makemhr/loadsofa.cpp b/utils/makemhr/loadsofa.cpp
new file mode 100644 (file)
index 0000000..dcb0a35
--- /dev/null
@@ -0,0 +1,603 @@
+/*
+ * HRTF utility for producing and demonstrating the process of creating an
+ * OpenAL Soft compatible HRIR data set.
+ *
+ * Copyright (C) 2018-2019  Christopher Fitzgerald
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Or visit:  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ */
+
+#include "loadsofa.h"
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <cmath>
+#include <cstdio>
+#include <functional>
+#include <future>
+#include <iterator>
+#include <memory>
+#include <numeric>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "aloptional.h"
+#include "alspan.h"
+#include "makemhr.h"
+#include "polyphase_resampler.h"
+#include "sofa-support.h"
+
+#include "mysofa.h"
+
+
+using uint = unsigned int;
+
+/* Attempts to produce a compatible layout.  Most data sets tend to be
+ * uniform and have the same major axis as used by OpenAL Soft's HRTF model.
+ * This will remove outliers and produce a maximally dense layout when
+ * possible.  Those sets that contain purely random measurements or use
+ * different major axes will fail.
+ */
+static bool PrepareLayout(const uint m, const float *xyzs, HrirDataT *hData)
+{
+    fprintf(stdout, "Detecting compatible layout...\n");
+
+    auto fds = GetCompatibleLayout(m, xyzs);
+    if(fds.size() > MAX_FD_COUNT)
+    {
+        fprintf(stdout, "Incompatible layout (inumerable radii).\n");
+        return false;
+    }
+
+    double distances[MAX_FD_COUNT]{};
+    uint evCounts[MAX_FD_COUNT]{};
+    auto azCounts = std::vector<std::array<uint,MAX_EV_COUNT>>(MAX_FD_COUNT);
+    for(auto &azs : azCounts) azs.fill(0u);
+
+    uint fi{0u}, ir_total{0u};
+    for(const auto &field : fds)
+    {
+        distances[fi] = field.mDistance;
+        evCounts[fi] = field.mEvCount;
+
+        for(uint ei{0u};ei < field.mEvStart;ei++)
+            azCounts[fi][ei] = field.mAzCounts[field.mEvCount-ei-1];
+        for(uint ei{field.mEvStart};ei < field.mEvCount;ei++)
+        {
+            azCounts[fi][ei] = field.mAzCounts[ei];
+            ir_total += field.mAzCounts[ei];
+        }
+
+        ++fi;
+    }
+    fprintf(stdout, "Using %u of %u IRs.\n", ir_total, m);
+    const auto azs = al::as_span(azCounts).first<MAX_FD_COUNT>();
+    return PrepareHrirData({distances, fi}, evCounts, azs, hData);
+}
+
+
+float GetSampleRate(MYSOFA_HRTF *sofaHrtf)
+{
+    const char *srate_dim{nullptr};
+    const char *srate_units{nullptr};
+    MYSOFA_ARRAY *srate_array{&sofaHrtf->DataSamplingRate};
+    MYSOFA_ATTRIBUTE *srate_attrs{srate_array->attributes};
+    while(srate_attrs)
+    {
+        if(std::string{"DIMENSION_LIST"} == srate_attrs->name)
+        {
+            if(srate_dim)
+            {
+                fprintf(stderr, "Duplicate SampleRate.DIMENSION_LIST\n");
+                return 0.0f;
+            }
+            srate_dim = srate_attrs->value;
+        }
+        else if(std::string{"Units"} == srate_attrs->name)
+        {
+            if(srate_units)
+            {
+                fprintf(stderr, "Duplicate SampleRate.Units\n");
+                return 0.0f;
+            }
+            srate_units = srate_attrs->value;
+        }
+        else
+            fprintf(stderr, "Unexpected sample rate attribute: %s = %s\n", srate_attrs->name,
+                srate_attrs->value);
+        srate_attrs = srate_attrs->next;
+    }
+    if(!srate_dim)
+    {
+        fprintf(stderr, "Missing sample rate dimensions\n");
+        return 0.0f;
+    }
+    if(srate_dim != std::string{"I"})
+    {
+        fprintf(stderr, "Unsupported sample rate dimensions: %s\n", srate_dim);
+        return 0.0f;
+    }
+    if(!srate_units)
+    {
+        fprintf(stderr, "Missing sample rate unit type\n");
+        return 0.0f;
+    }
+    if(srate_units != std::string{"hertz"})
+    {
+        fprintf(stderr, "Unsupported sample rate unit type: %s\n", srate_units);
+        return 0.0f;
+    }
+    /* I dimensions guarantees 1 element, so just extract it. */
+    if(srate_array->values[0] < MIN_RATE || srate_array->values[0] > MAX_RATE)
+    {
+        fprintf(stderr, "Sample rate out of range: %f (expected %u to %u)", srate_array->values[0],
+            MIN_RATE, MAX_RATE);
+        return 0.0f;
+    }
+    return srate_array->values[0];
+}
+
+enum class DelayType : uint8_t {
+    None,
+    I_R, /* [1][Channels] */
+    M_R, /* [HRIRs][Channels] */
+    Invalid,
+};
+DelayType PrepareDelay(MYSOFA_HRTF *sofaHrtf)
+{
+    const char *delay_dim{nullptr};
+    MYSOFA_ARRAY *delay_array{&sofaHrtf->DataDelay};
+    MYSOFA_ATTRIBUTE *delay_attrs{delay_array->attributes};
+    while(delay_attrs)
+    {
+        if(std::string{"DIMENSION_LIST"} == delay_attrs->name)
+        {
+            if(delay_dim)
+            {
+                fprintf(stderr, "Duplicate Delay.DIMENSION_LIST\n");
+                return DelayType::Invalid;
+            }
+            delay_dim = delay_attrs->value;
+        }
+        else
+            fprintf(stderr, "Unexpected delay attribute: %s = %s\n", delay_attrs->name,
+                delay_attrs->value ? delay_attrs->value : "<null>");
+        delay_attrs = delay_attrs->next;
+    }
+    if(!delay_dim)
+    {
+        fprintf(stderr, "Missing delay dimensions\n");
+        return DelayType::None;
+    }
+    if(delay_dim == std::string{"I,R"})
+        return DelayType::I_R;
+    else if(delay_dim == std::string{"M,R"})
+        return DelayType::M_R;
+
+    fprintf(stderr, "Unsupported delay dimensions: %s\n", delay_dim);
+    return DelayType::Invalid;
+}
+
+bool CheckIrData(MYSOFA_HRTF *sofaHrtf)
+{
+    const char *ir_dim{nullptr};
+    MYSOFA_ARRAY *ir_array{&sofaHrtf->DataIR};
+    MYSOFA_ATTRIBUTE *ir_attrs{ir_array->attributes};
+    while(ir_attrs)
+    {
+        if(std::string{"DIMENSION_LIST"} == ir_attrs->name)
+        {
+            if(ir_dim)
+            {
+                fprintf(stderr, "Duplicate IR.DIMENSION_LIST\n");
+                return false;
+            }
+            ir_dim = ir_attrs->value;
+        }
+        else
+            fprintf(stderr, "Unexpected IR attribute: %s = %s\n", ir_attrs->name,
+                ir_attrs->value ? ir_attrs->value : "<null>");
+        ir_attrs = ir_attrs->next;
+    }
+    if(!ir_dim)
+    {
+        fprintf(stderr, "Missing IR dimensions\n");
+        return false;
+    }
+    if(ir_dim != std::string{"M,R,N"})
+    {
+        fprintf(stderr, "Unsupported IR dimensions: %s\n", ir_dim);
+        return false;
+    }
+    return true;
+}
+
+
+/* Calculate the onset time of a HRIR. */
+static constexpr int OnsetRateMultiple{10};
+static double CalcHrirOnset(PPhaseResampler &rs, const uint rate, const uint n,
+    al::span<double> upsampled, const double *hrir)
+{
+    rs.process(n, hrir, static_cast<uint>(upsampled.size()), upsampled.data());
+
+    auto abs_lt = [](const double &lhs, const double &rhs) -> bool
+    { return std::abs(lhs) < std::abs(rhs); };
+    auto iter = std::max_element(upsampled.cbegin(), upsampled.cend(), abs_lt);
+    return static_cast<double>(std::distance(upsampled.cbegin(), iter)) /
+        (double{OnsetRateMultiple}*rate);
+}
+
+/* Calculate the magnitude response of a HRIR. */
+static void CalcHrirMagnitude(const uint points, const uint n, al::span<complex_d> h, double *hrir)
+{
+    auto iter = std::copy_n(hrir, points, h.begin());
+    std::fill(iter, h.end(), complex_d{0.0, 0.0});
+
+    FftForward(n, h.data());
+    MagnitudeResponse(n, h.data(), hrir);
+}
+
+static bool LoadResponses(MYSOFA_HRTF *sofaHrtf, HrirDataT *hData, const DelayType delayType,
+    const uint outRate)
+{
+    std::atomic<uint> loaded_count{0u};
+
+    auto load_proc = [sofaHrtf,hData,delayType,outRate,&loaded_count]() -> bool
+    {
+        const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+        hData->mHrirsBase.resize(channels * hData->mIrCount * hData->mIrSize, 0.0);
+        double *hrirs = hData->mHrirsBase.data();
+
+        std::unique_ptr<double[]> restmp;
+        al::optional<PPhaseResampler> resampler;
+        if(outRate && outRate != hData->mIrRate)
+        {
+            resampler.emplace().init(hData->mIrRate, outRate);
+            restmp = std::make_unique<double[]>(sofaHrtf->N);
+        }
+
+        for(uint si{0u};si < sofaHrtf->M;++si)
+        {
+            loaded_count.fetch_add(1u);
+
+            float aer[3]{
+                sofaHrtf->SourcePosition.values[3*si],
+                sofaHrtf->SourcePosition.values[3*si + 1],
+                sofaHrtf->SourcePosition.values[3*si + 2]
+            };
+            mysofa_c2s(aer);
+
+            if(std::abs(aer[1]) >= 89.999f)
+                aer[0] = 0.0f;
+            else
+                aer[0] = std::fmod(360.0f - aer[0], 360.0f);
+
+            auto field = std::find_if(hData->mFds.cbegin(), hData->mFds.cend(),
+                [&aer](const HrirFdT &fld) -> bool
+                { return (std::abs(aer[2] - fld.mDistance) < 0.001); });
+            if(field == hData->mFds.cend())
+                continue;
+
+            const double evscale{180.0 / static_cast<double>(field->mEvs.size()-1)};
+            double ef{(90.0 + aer[1]) / evscale};
+            auto ei = static_cast<uint>(std::round(ef));
+            ef = (ef - ei) * evscale;
+            if(std::abs(ef) >= 0.1) continue;
+
+            const double azscale{360.0 / static_cast<double>(field->mEvs[ei].mAzs.size())};
+            double af{aer[0] / azscale};
+            auto ai = static_cast<uint>(std::round(af));
+            af = (af-ai) * azscale;
+            ai %= static_cast<uint>(field->mEvs[ei].mAzs.size());
+            if(std::abs(af) >= 0.1) continue;
+
+            HrirAzT *azd = &field->mEvs[ei].mAzs[ai];
+            if(azd->mIrs[0] != nullptr)
+            {
+                fprintf(stderr, "\nMultiple measurements near [ a=%f, e=%f, r=%f ].\n",
+                    aer[0], aer[1], aer[2]);
+                return false;
+            }
+
+            for(uint ti{0u};ti < channels;++ti)
+            {
+                azd->mIrs[ti] = &hrirs[hData->mIrSize * (hData->mIrCount*ti + azd->mIndex)];
+                if(!resampler)
+                    std::copy_n(&sofaHrtf->DataIR.values[(si*sofaHrtf->R + ti)*sofaHrtf->N],
+                        sofaHrtf->N, azd->mIrs[ti]);
+                else
+                {
+                    std::copy_n(&sofaHrtf->DataIR.values[(si*sofaHrtf->R + ti)*sofaHrtf->N],
+                        sofaHrtf->N, restmp.get());
+                    resampler->process(sofaHrtf->N, restmp.get(), hData->mIrSize, azd->mIrs[ti]);
+                }
+            }
+
+            /* Include any per-channel or per-HRIR delays. */
+            if(delayType == DelayType::I_R)
+            {
+                const float *delayValues{sofaHrtf->DataDelay.values};
+                for(uint ti{0u};ti < channels;++ti)
+                    azd->mDelays[ti] = delayValues[ti] / static_cast<float>(hData->mIrRate);
+            }
+            else if(delayType == DelayType::M_R)
+            {
+                const float *delayValues{sofaHrtf->DataDelay.values};
+                for(uint ti{0u};ti < channels;++ti)
+                    azd->mDelays[ti] = delayValues[si*sofaHrtf->R + ti] /
+                        static_cast<float>(hData->mIrRate);
+            }
+        }
+
+        if(outRate && outRate != hData->mIrRate)
+        {
+            const double scale{static_cast<double>(outRate) / hData->mIrRate};
+            hData->mIrRate = outRate;
+            hData->mIrPoints = std::min(static_cast<uint>(std::ceil(hData->mIrPoints*scale)),
+                hData->mIrSize);
+        }
+        return true;
+    };
+
+    std::future_status load_status{};
+    auto load_future = std::async(std::launch::async, load_proc);
+    do {
+        load_status = load_future.wait_for(std::chrono::milliseconds{50});
+        printf("\rLoading HRIRs... %u of %u", loaded_count.load(), sofaHrtf->M);
+        fflush(stdout);
+    } while(load_status != std::future_status::ready);
+    fputc('\n', stdout);
+    return load_future.get();
+}
+
+
+/* Calculates the frequency magnitudes of the HRIR set. Work is delegated to
+ * this struct, which runs asynchronously on one or more threads (sharing the
+ * same calculator object).
+ */
+struct MagCalculator {
+    const uint mFftSize{};
+    const uint mIrPoints{};
+    std::vector<double*> mIrs{};
+    std::atomic<size_t> mCurrent{};
+    std::atomic<size_t> mDone{};
+
+    void Worker()
+    {
+        auto htemp = std::vector<complex_d>(mFftSize);
+
+        while(1)
+        {
+            /* Load the current index to process. */
+            size_t idx{mCurrent.load()};
+            do {
+                /* If the index is at the end, we're done. */
+                if(idx >= mIrs.size())
+                    return;
+                /* Otherwise, increment the current index atomically so other
+                 * threads know to go to the next one. If this call fails, the
+                 * current index was just changed by another thread and the new
+                 * value is loaded into idx, which we'll recheck.
+                 */
+            } while(!mCurrent.compare_exchange_weak(idx, idx+1, std::memory_order_relaxed));
+
+            CalcHrirMagnitude(mIrPoints, mFftSize, htemp, mIrs[idx]);
+
+            /* Increment the number of IRs done. */
+            mDone.fetch_add(1);
+        }
+    }
+};
+
+bool LoadSofaFile(const char *filename, const uint numThreads, const uint fftSize,
+    const uint truncSize, const uint outRate, const ChannelModeT chanMode, HrirDataT *hData)
+{
+    int err;
+    MySofaHrtfPtr sofaHrtf{mysofa_load(filename, &err)};
+    if(!sofaHrtf)
+    {
+        fprintf(stdout, "Error: Could not load %s: %s\n", filename, SofaErrorStr(err));
+        return false;
+    }
+
+    /* NOTE: Some valid SOFA files are failing this check. */
+    err = mysofa_check(sofaHrtf.get());
+    if(err != MYSOFA_OK)
+        fprintf(stderr, "Warning: Supposedly malformed source file '%s' (%s).\n", filename,
+            SofaErrorStr(err));
+
+    mysofa_tocartesian(sofaHrtf.get());
+
+    /* Make sure emitter and receiver counts are sane. */
+    if(sofaHrtf->E != 1)
+    {
+        fprintf(stderr, "%u emitters not supported\n", sofaHrtf->E);
+        return false;
+    }
+    if(sofaHrtf->R > 2 || sofaHrtf->R < 1)
+    {
+        fprintf(stderr, "%u receivers not supported\n", sofaHrtf->R);
+        return false;
+    }
+    /* Assume R=2 is a stereo measurement, and R=1 is mono left-ear-only. */
+    if(sofaHrtf->R == 2 && chanMode == CM_AllowStereo)
+        hData->mChannelType = CT_STEREO;
+    else
+        hData->mChannelType = CT_MONO;
+
+    /* Check and set the FFT and IR size. */
+    if(sofaHrtf->N > fftSize)
+    {
+        fprintf(stderr, "Sample points exceeds the FFT size.\n");
+        return false;
+    }
+    if(sofaHrtf->N < truncSize)
+    {
+        fprintf(stderr, "Sample points is below the truncation size.\n");
+        return false;
+    }
+    hData->mIrPoints = sofaHrtf->N;
+    hData->mFftSize = fftSize;
+    hData->mIrSize = std::max(1u + (fftSize/2u), sofaHrtf->N);
+
+    /* Assume a default head radius of 9cm. */
+    hData->mRadius = 0.09;
+
+    hData->mIrRate = static_cast<uint>(GetSampleRate(sofaHrtf.get()) + 0.5f);
+    if(!hData->mIrRate)
+        return false;
+
+    DelayType delayType = PrepareDelay(sofaHrtf.get());
+    if(delayType == DelayType::Invalid)
+        return false;
+
+    if(!CheckIrData(sofaHrtf.get()))
+        return false;
+    if(!PrepareLayout(sofaHrtf->M, sofaHrtf->SourcePosition.values, hData))
+        return false;
+    if(!LoadResponses(sofaHrtf.get(), hData, delayType, outRate))
+        return false;
+    sofaHrtf = nullptr;
+
+    for(uint fi{0u};fi < hData->mFds.size();fi++)
+    {
+        uint ei{0u};
+        for(;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            uint ai{0u};
+            for(;ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+            {
+                HrirAzT &azd = hData->mFds[fi].mEvs[ei].mAzs[ai];
+                if(azd.mIrs[0] != nullptr) break;
+            }
+            if(ai < hData->mFds[fi].mEvs[ei].mAzs.size())
+                break;
+        }
+        if(ei >= hData->mFds[fi].mEvs.size())
+        {
+            fprintf(stderr, "Missing source references [ %d, *, * ].\n", fi);
+            return false;
+        }
+        hData->mFds[fi].mEvStart = ei;
+        for(;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(uint ai{0u};ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+            {
+                HrirAzT &azd = hData->mFds[fi].mEvs[ei].mAzs[ai];
+                if(azd.mIrs[0] == nullptr)
+                {
+                    fprintf(stderr, "Missing source reference [ %d, %d, %d ].\n", fi, ei, ai);
+                    return false;
+                }
+            }
+        }
+    }
+
+
+    size_t hrir_total{0};
+    const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+    double *hrirs = hData->mHrirsBase.data();
+    for(uint fi{0u};fi < hData->mFds.size();fi++)
+    {
+        for(uint ei{0u};ei < hData->mFds[fi].mEvStart;ei++)
+        {
+            for(uint ai{0u};ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+            {
+                HrirAzT &azd = hData->mFds[fi].mEvs[ei].mAzs[ai];
+                for(uint ti{0u};ti < channels;ti++)
+                    azd.mIrs[ti] = &hrirs[hData->mIrSize * (hData->mIrCount*ti + azd.mIndex)];
+            }
+        }
+
+        for(uint ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();ei++)
+            hrir_total += hData->mFds[fi].mEvs[ei].mAzs.size() * channels;
+    }
+
+    std::atomic<size_t> hrir_done{0};
+    auto onset_proc = [hData,channels,&hrir_done]() -> bool
+    {
+        /* Temporary buffer used to calculate the IR's onset. */
+        auto upsampled = std::vector<double>(OnsetRateMultiple * hData->mIrPoints);
+        /* This resampler is used to help detect the response onset. */
+        PPhaseResampler rs;
+        rs.init(hData->mIrRate, OnsetRateMultiple*hData->mIrRate);
+
+        for(auto &field : hData->mFds)
+        {
+            for(auto &elev : field.mEvs.subspan(field.mEvStart))
+            {
+                for(auto &azd : elev.mAzs)
+                {
+                    for(uint ti{0};ti < channels;ti++)
+                    {
+                        hrir_done.fetch_add(1u, std::memory_order_acq_rel);
+                        azd.mDelays[ti] += CalcHrirOnset(rs, hData->mIrRate, hData->mIrPoints,
+                            upsampled, azd.mIrs[ti]);
+                    }
+                }
+            }
+        }
+        return true;
+    };
+
+    std::future_status load_status{};
+    auto load_future = std::async(std::launch::async, onset_proc);
+    do {
+        load_status = load_future.wait_for(std::chrono::milliseconds{50});
+        printf("\rCalculating HRIR onsets... %zu of %zu", hrir_done.load(), hrir_total);
+        fflush(stdout);
+    } while(load_status != std::future_status::ready);
+    fputc('\n', stdout);
+    if(!load_future.get())
+        return false;
+
+    MagCalculator calculator{hData->mFftSize, hData->mIrPoints};
+    for(auto &field : hData->mFds)
+    {
+        for(auto &elev : field.mEvs.subspan(field.mEvStart))
+        {
+            for(auto &azd : elev.mAzs)
+            {
+                for(uint ti{0};ti < channels;ti++)
+                    calculator.mIrs.push_back(azd.mIrs[ti]);
+            }
+        }
+    }
+
+    std::vector<std::thread> thrds;
+    thrds.reserve(numThreads);
+    for(size_t i{0};i < numThreads;++i)
+        thrds.emplace_back(std::mem_fn(&MagCalculator::Worker), &calculator);
+    size_t count;
+    do {
+        std::this_thread::sleep_for(std::chrono::milliseconds{50});
+        count = calculator.mDone.load();
+
+        printf("\rCalculating HRIR magnitudes... %zu of %zu", count, calculator.mIrs.size());
+        fflush(stdout);
+    } while(count != calculator.mIrs.size());
+    fputc('\n', stdout);
+
+    for(auto &thrd : thrds)
+    {
+        if(thrd.joinable())
+            thrd.join();
+    }
+    return true;
+}
diff --git a/utils/makemhr/loadsofa.h b/utils/makemhr/loadsofa.h
new file mode 100644 (file)
index 0000000..82dce85
--- /dev/null
@@ -0,0 +1,10 @@
+#ifndef LOADSOFA_H
+#define LOADSOFA_H
+
+#include "makemhr.h"
+
+
+bool LoadSofaFile(const char *filename, const uint numThreads, const uint fftSize,
+    const uint truncSize, const uint outRate, const ChannelModeT chanMode, HrirDataT *hData);
+
+#endif /* LOADSOFA_H */
diff --git a/utils/makemhr/makemhr.cpp b/utils/makemhr/makemhr.cpp
new file mode 100644 (file)
index 0000000..ae301dc
--- /dev/null
@@ -0,0 +1,1473 @@
+/*
+ * HRTF utility for producing and demonstrating the process of creating an
+ * OpenAL Soft compatible HRIR data set.
+ *
+ * Copyright (C) 2011-2019  Christopher Fitzgerald
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Or visit:  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ *
+ * --------------------------------------------------------------------------
+ *
+ * A big thanks goes out to all those whose work done in the field of
+ * binaural sound synthesis using measured HRTFs makes this utility and the
+ * OpenAL Soft implementation possible.
+ *
+ * The algorithm for diffuse-field equalization was adapted from the work
+ * done by Rio Emmanuel and Larcher Veronique of IRCAM and Bill Gardner of
+ * MIT Media Laboratory.  It operates as follows:
+ *
+ *  1.  Take the FFT of each HRIR and only keep the magnitude responses.
+ *  2.  Calculate the diffuse-field power-average of all HRIRs weighted by
+ *      their contribution to the total surface area covered by their
+ *      measurement. This has since been modified to use coverage volume for
+ *      multi-field HRIR data sets.
+ *  3.  Take the diffuse-field average and limit its magnitude range.
+ *  4.  Equalize the responses by using the inverse of the diffuse-field
+ *      average.
+ *  5.  Reconstruct the minimum-phase responses.
+ *  5.  Zero the DC component.
+ *  6.  IFFT the result and truncate to the desired-length minimum-phase FIR.
+ *
+ * The spherical head algorithm for calculating propagation delay was adapted
+ * from the paper:
+ *
+ *  Modeling Interaural Time Difference Assuming a Spherical Head
+ *  Joel David Miller
+ *  Music 150, Musical Acoustics, Stanford University
+ *  December 2, 2001
+ *
+ * The formulae for calculating the Kaiser window metrics are from the
+ * the textbook:
+ *
+ *  Discrete-Time Signal Processing
+ *  Alan V. Oppenheim and Ronald W. Schafer
+ *  Prentice-Hall Signal Processing Series
+ *  1999
+ */
+
+#define _UNICODE
+#include "config.h"
+
+#include "makemhr.h"
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <cmath>
+#include <complex>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <functional>
+#include <iostream>
+#include <limits>
+#include <memory>
+#include <numeric>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#ifdef HAVE_GETOPT
+#include <unistd.h>
+#else
+#include "../getopt.h"
+#endif
+
+#include "alcomplex.h"
+#include "alfstream.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "loaddef.h"
+#include "loadsofa.h"
+
+#include "win_main_utf8.h"
+
+
+namespace {
+
+using namespace std::placeholders;
+
+} // namespace
+
+#ifndef M_PI
+#define M_PI                         (3.14159265358979323846)
+#endif
+
+
+HrirDataT::~HrirDataT() = default;
+
+// Head model used for calculating the impulse delays.
+enum HeadModelT {
+    HM_NONE,
+    HM_DATASET, // Measure the onset from the dataset.
+    HM_SPHERE   // Calculate the onset using a spherical head model.
+};
+
+
+// The epsilon used to maintain signal stability.
+#define EPSILON                      (1e-9)
+
+// The limits to the FFT window size override on the command line.
+#define MIN_FFTSIZE                  (65536)
+#define MAX_FFTSIZE                  (131072)
+
+// The limits to the equalization range limit on the command line.
+#define MIN_LIMIT                    (2.0)
+#define MAX_LIMIT                    (120.0)
+
+// The limits to the truncation window size on the command line.
+#define MIN_TRUNCSIZE                (16)
+#define MAX_TRUNCSIZE                (128)
+
+// The limits to the custom head radius on the command line.
+#define MIN_CUSTOM_RADIUS            (0.05)
+#define MAX_CUSTOM_RADIUS            (0.15)
+
+// The defaults for the command line options.
+#define DEFAULT_FFTSIZE              (65536)
+#define DEFAULT_EQUALIZE             (1)
+#define DEFAULT_SURFACE              (1)
+#define DEFAULT_LIMIT                (24.0)
+#define DEFAULT_TRUNCSIZE            (64)
+#define DEFAULT_HEAD_MODEL           (HM_DATASET)
+#define DEFAULT_CUSTOM_RADIUS        (0.0)
+
+// The maximum propagation delay value supported by OpenAL Soft.
+#define MAX_HRTD                     (63.0)
+
+// The OpenAL Soft HRTF format marker.  It stands for minimum-phase head
+// response protocol 03.
+#define MHR_FORMAT                   ("MinPHR03")
+
+/* Channel index enums. Mono uses LeftChannel only. */
+enum ChannelIndex : uint {
+    LeftChannel = 0u,
+    RightChannel = 1u
+};
+
+
+/* Performs a string substitution.  Any case-insensitive occurrences of the
+ * pattern string are replaced with the replacement string.  The result is
+ * truncated if necessary.
+ */
+static std::string StrSubst(al::span<const char> in, const al::span<const char> pat,
+    const al::span<const char> rep)
+{
+    std::string ret;
+    ret.reserve(in.size() + pat.size());
+
+    while(in.size() >= pat.size())
+    {
+        if(al::strncasecmp(in.data(), pat.data(), pat.size()) == 0)
+        {
+            in = in.subspan(pat.size());
+            ret.append(rep.data(), rep.size());
+        }
+        else
+        {
+            size_t endpos{1};
+            while(endpos < in.size() && in[endpos] != pat.front())
+                ++endpos;
+            ret.append(in.data(), endpos);
+            in = in.subspan(endpos);
+        }
+    }
+    ret.append(in.data(), in.size());
+
+    return ret;
+}
+
+
+/*********************
+ *** Math routines ***
+ *********************/
+
+// Simple clamp routine.
+static double Clamp(const double val, const double lower, const double upper)
+{
+    return std::min(std::max(val, lower), upper);
+}
+
+static inline uint dither_rng(uint *seed)
+{
+    *seed = *seed * 96314165 + 907633515;
+    return *seed;
+}
+
+// Performs a triangular probability density function dither. The input samples
+// should be normalized (-1 to +1).
+static void TpdfDither(double *RESTRICT out, const double *RESTRICT in, const double scale,
+                       const uint count, const uint step, uint *seed)
+{
+    static constexpr double PRNG_SCALE = 1.0 / std::numeric_limits<uint>::max();
+
+    for(uint i{0};i < count;i++)
+    {
+        uint prn0{dither_rng(seed)};
+        uint prn1{dither_rng(seed)};
+        *out = std::round(*(in++)*scale + (prn0*PRNG_SCALE - prn1*PRNG_SCALE));
+        out += step;
+    }
+}
+
+
+/* Calculate the complex helical sequence (or discrete-time analytical signal)
+ * of the given input using the Hilbert transform. Given the natural logarithm
+ * of a signal's magnitude response, the imaginary components can be used as
+ * the angles for minimum-phase reconstruction.
+ */
+inline static void Hilbert(const uint n, complex_d *inout)
+{ complex_hilbert({inout, n}); }
+
+/* Calculate the magnitude response of the given input.  This is used in
+ * place of phase decomposition, since the phase residuals are discarded for
+ * minimum phase reconstruction.  The mirrored half of the response is also
+ * discarded.
+ */
+void MagnitudeResponse(const uint n, const complex_d *in, double *out)
+{
+    const uint m = 1 + (n / 2);
+    uint i;
+    for(i = 0;i < m;i++)
+        out[i] = std::max(std::abs(in[i]), EPSILON);
+}
+
+/* Apply a range limit (in dB) to the given magnitude response.  This is used
+ * to adjust the effects of the diffuse-field average on the equalization
+ * process.
+ */
+static void LimitMagnitudeResponse(const uint n, const uint m, const double limit, const double *in, double *out)
+{
+    double halfLim;
+    uint i, lower, upper;
+    double ave;
+
+    halfLim = limit / 2.0;
+    // Convert the response to dB.
+    for(i = 0;i < m;i++)
+        out[i] = 20.0 * std::log10(in[i]);
+    // Use six octaves to calculate the average magnitude of the signal.
+    lower = (static_cast<uint>(std::ceil(n / std::pow(2.0, 8.0)))) - 1;
+    upper = (static_cast<uint>(std::floor(n / std::pow(2.0, 2.0)))) - 1;
+    ave = 0.0;
+    for(i = lower;i <= upper;i++)
+        ave += out[i];
+    ave /= upper - lower + 1;
+    // Keep the response within range of the average magnitude.
+    for(i = 0;i < m;i++)
+        out[i] = Clamp(out[i], ave - halfLim, ave + halfLim);
+    // Convert the response back to linear magnitude.
+    for(i = 0;i < m;i++)
+        out[i] = std::pow(10.0, out[i] / 20.0);
+}
+
+/* Reconstructs the minimum-phase component for the given magnitude response
+ * of a signal.  This is equivalent to phase recomposition, sans the missing
+ * residuals (which were discarded).  The mirrored half of the response is
+ * reconstructed.
+ */
+static void MinimumPhase(const uint n, double *mags, complex_d *out)
+{
+    const uint m{(n/2) + 1};
+
+    uint i;
+    for(i = 0;i < m;i++)
+        out[i] = std::log(mags[i]);
+    for(;i < n;i++)
+    {
+        mags[i] = mags[n - i];
+        out[i] = out[n - i];
+    }
+    Hilbert(n, out);
+    // Remove any DC offset the filter has.
+    mags[0] = EPSILON;
+    for(i = 0;i < n;i++)
+    {
+        auto a = std::exp(complex_d{0.0, out[i].imag()});
+        out[i] = a * mags[i];
+    }
+}
+
+
+/***************************
+ *** File storage output ***
+ ***************************/
+
+// Write an ASCII string to a file.
+static int WriteAscii(const char *out, FILE *fp, const char *filename)
+{
+    size_t len;
+
+    len = strlen(out);
+    if(fwrite(out, 1, len, fp) != len)
+    {
+        fclose(fp);
+        fprintf(stderr, "\nError: Bad write to file '%s'.\n", filename);
+        return 0;
+    }
+    return 1;
+}
+
+// Write a binary value of the given byte order and byte size to a file,
+// loading it from a 32-bit unsigned integer.
+static int WriteBin4(const uint bytes, const uint32_t in, FILE *fp, const char *filename)
+{
+    uint8_t out[4];
+    uint i;
+
+    for(i = 0;i < bytes;i++)
+        out[i] = (in>>(i*8)) & 0x000000FF;
+
+    if(fwrite(out, 1, bytes, fp) != bytes)
+    {
+        fprintf(stderr, "\nError: Bad write to file '%s'.\n", filename);
+        return 0;
+    }
+    return 1;
+}
+
+// Store the OpenAL Soft HRTF data set.
+static int StoreMhr(const HrirDataT *hData, const char *filename)
+{
+    const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+    const uint n{hData->mIrPoints};
+    uint dither_seed{22222};
+    uint fi, ei, ai, i;
+    FILE *fp;
+
+    if((fp=fopen(filename, "wb")) == nullptr)
+    {
+        fprintf(stderr, "\nError: Could not open MHR file '%s'.\n", filename);
+        return 0;
+    }
+    if(!WriteAscii(MHR_FORMAT, fp, filename))
+        return 0;
+    if(!WriteBin4(4, hData->mIrRate, fp, filename))
+        return 0;
+    if(!WriteBin4(1, static_cast<uint32_t>(hData->mChannelType), fp, filename))
+        return 0;
+    if(!WriteBin4(1, hData->mIrPoints, fp, filename))
+        return 0;
+    if(!WriteBin4(1, static_cast<uint>(hData->mFds.size()), fp, filename))
+        return 0;
+    for(fi = static_cast<uint>(hData->mFds.size()-1);fi < hData->mFds.size();fi--)
+    {
+        auto fdist = static_cast<uint32_t>(std::round(1000.0 * hData->mFds[fi].mDistance));
+        if(!WriteBin4(2, fdist, fp, filename))
+            return 0;
+        if(!WriteBin4(1, static_cast<uint32_t>(hData->mFds[fi].mEvs.size()), fp, filename))
+            return 0;
+        for(ei = 0;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            const auto &elev = hData->mFds[fi].mEvs[ei];
+            if(!WriteBin4(1, static_cast<uint32_t>(elev.mAzs.size()), fp, filename))
+                return 0;
+        }
+    }
+
+    for(fi = static_cast<uint>(hData->mFds.size()-1);fi < hData->mFds.size();fi--)
+    {
+        constexpr double scale{8388607.0};
+        constexpr uint bps{3u};
+
+        for(ei = 0;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+            {
+                HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
+                double out[2 * MAX_TRUNCSIZE];
+
+                TpdfDither(out, azd->mIrs[0], scale, n, channels, &dither_seed);
+                if(hData->mChannelType == CT_STEREO)
+                    TpdfDither(out+1, azd->mIrs[1], scale, n, channels, &dither_seed);
+                for(i = 0;i < (channels * n);i++)
+                {
+                    const auto v = static_cast<int>(Clamp(out[i], -scale-1.0, scale));
+                    if(!WriteBin4(bps, static_cast<uint32_t>(v), fp, filename))
+                        return 0;
+                }
+            }
+        }
+    }
+    for(fi = static_cast<uint>(hData->mFds.size()-1);fi < hData->mFds.size();fi--)
+    {
+        /* Delay storage has 2 bits of extra precision. */
+        constexpr double DelayPrecScale{4.0};
+        for(ei = 0;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(const auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
+            {
+                auto v = static_cast<uint>(std::round(azd.mDelays[0]*DelayPrecScale));
+                if(!WriteBin4(1, v, fp, filename)) return 0;
+                if(hData->mChannelType == CT_STEREO)
+                {
+                    v = static_cast<uint>(std::round(azd.mDelays[1]*DelayPrecScale));
+                    if(!WriteBin4(1, v, fp, filename)) return 0;
+                }
+            }
+        }
+    }
+    fclose(fp);
+    return 1;
+}
+
+
+/***********************
+ *** HRTF processing ***
+ ***********************/
+
+/* Balances the maximum HRIR magnitudes of multi-field data sets by
+ * independently normalizing each field in relation to the overall maximum.
+ * This is done to ignore distance attenuation.
+ */
+static void BalanceFieldMagnitudes(const HrirDataT *hData, const uint channels, const uint m)
+{
+    double maxMags[MAX_FD_COUNT];
+    uint fi, ei, ti, i;
+
+    double maxMag{0.0};
+    for(fi = 0;fi < hData->mFds.size();fi++)
+    {
+        maxMags[fi] = 0.0;
+
+        for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(const auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
+            {
+                for(ti = 0;ti < channels;ti++)
+                {
+                    for(i = 0;i < m;i++)
+                        maxMags[fi] = std::max(azd.mIrs[ti][i], maxMags[fi]);
+                }
+            }
+        }
+
+        maxMag = std::max(maxMags[fi], maxMag);
+    }
+
+    for(fi = 0;fi < hData->mFds.size();fi++)
+    {
+        const double magFactor{maxMag / maxMags[fi]};
+
+        for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(const auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
+            {
+                for(ti = 0;ti < channels;ti++)
+                {
+                    for(i = 0;i < m;i++)
+                        azd.mIrs[ti][i] *= magFactor;
+                }
+            }
+        }
+    }
+}
+
+/* Calculate the contribution of each HRIR to the diffuse-field average based
+ * on its coverage volume.  All volumes are centered at the spherical HRIR
+ * coordinates and measured by extruded solid angle.
+ */
+static void CalculateDfWeights(const HrirDataT *hData, double *weights)
+{
+    double sum, innerRa, outerRa, evs, ev, upperEv, lowerEv;
+    double solidAngle, solidVolume;
+    uint fi, ei;
+
+    sum = 0.0;
+    // The head radius acts as the limit for the inner radius.
+    innerRa = hData->mRadius;
+    for(fi = 0;fi < hData->mFds.size();fi++)
+    {
+        // Each volume ends half way between progressive field measurements.
+        if((fi + 1) < hData->mFds.size())
+            outerRa = 0.5f * (hData->mFds[fi].mDistance + hData->mFds[fi + 1].mDistance);
+        // The final volume has its limit extended to some practical value.
+        // This is done to emphasize the far-field responses in the average.
+        else
+            outerRa = 10.0f;
+
+        const double raPowDiff{std::pow(outerRa, 3.0) - std::pow(innerRa, 3.0)};
+        evs = M_PI / 2.0 / static_cast<double>(hData->mFds[fi].mEvs.size() - 1);
+        for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            const auto &elev = hData->mFds[fi].mEvs[ei];
+            // For each elevation, calculate the upper and lower limits of
+            // the patch band.
+            ev = elev.mElevation;
+            lowerEv = std::max(-M_PI / 2.0, ev - evs);
+            upperEv = std::min(M_PI / 2.0, ev + evs);
+            // Calculate the surface area of the patch band.
+            solidAngle = 2.0 * M_PI * (std::sin(upperEv) - std::sin(lowerEv));
+            // Then the volume of the extruded patch band.
+            solidVolume = solidAngle * raPowDiff / 3.0;
+            // Each weight is the volume of one extruded patch.
+            weights[(fi*MAX_EV_COUNT) + ei] = solidVolume / static_cast<double>(elev.mAzs.size());
+            // Sum the total coverage volume of the HRIRs for all fields.
+            sum += solidAngle;
+        }
+
+        innerRa = outerRa;
+    }
+
+    for(fi = 0;fi < hData->mFds.size();fi++)
+    {
+        // Normalize the weights given the total surface coverage for all
+        // fields.
+        for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
+            weights[(fi * MAX_EV_COUNT) + ei] /= sum;
+    }
+}
+
+/* Calculate the diffuse-field average from the given magnitude responses of
+ * the HRIR set.  Weighting can be applied to compensate for the varying
+ * coverage of each HRIR.  The final average can then be limited by the
+ * specified magnitude range (in positive dB; 0.0 to skip).
+ */
+static void CalculateDiffuseFieldAverage(const HrirDataT *hData, const uint channels, const uint m,
+    const int weighted, const double limit, double *dfa)
+{
+    std::vector<double> weights(hData->mFds.size() * MAX_EV_COUNT);
+    uint count, ti, fi, ei, i, ai;
+
+    if(weighted)
+    {
+        // Use coverage weighting to calculate the average.
+        CalculateDfWeights(hData, weights.data());
+    }
+    else
+    {
+        double weight;
+
+        // If coverage weighting is not used, the weights still need to be
+        // averaged by the number of existing HRIRs.
+        count = hData->mIrCount;
+        for(fi = 0;fi < hData->mFds.size();fi++)
+        {
+            for(ei = 0;ei < hData->mFds[fi].mEvStart;ei++)
+                count -= static_cast<uint>(hData->mFds[fi].mEvs[ei].mAzs.size());
+        }
+        weight = 1.0 / count;
+
+        for(fi = 0;fi < hData->mFds.size();fi++)
+        {
+            for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
+                weights[(fi * MAX_EV_COUNT) + ei] = weight;
+        }
+    }
+    for(ti = 0;ti < channels;ti++)
+    {
+        for(i = 0;i < m;i++)
+            dfa[(ti * m) + i] = 0.0;
+        for(fi = 0;fi < hData->mFds.size();fi++)
+        {
+            for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
+            {
+                for(ai = 0;ai < hData->mFds[fi].mEvs[ei].mAzs.size();ai++)
+                {
+                    HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
+                    // Get the weight for this HRIR's contribution.
+                    double weight = weights[(fi * MAX_EV_COUNT) + ei];
+
+                    // Add this HRIR's weighted power average to the total.
+                    for(i = 0;i < m;i++)
+                        dfa[(ti * m) + i] += weight * azd->mIrs[ti][i] * azd->mIrs[ti][i];
+                }
+            }
+        }
+        // Finish the average calculation and keep it from being too small.
+        for(i = 0;i < m;i++)
+            dfa[(ti * m) + i] = std::max(sqrt(dfa[(ti * m) + i]), EPSILON);
+        // Apply a limit to the magnitude range of the diffuse-field average
+        // if desired.
+        if(limit > 0.0)
+            LimitMagnitudeResponse(hData->mFftSize, m, limit, &dfa[ti * m], &dfa[ti * m]);
+    }
+}
+
+// Perform diffuse-field equalization on the magnitude responses of the HRIR
+// set using the given average response.
+static void DiffuseFieldEqualize(const uint channels, const uint m, const double *dfa, const HrirDataT *hData)
+{
+    uint ti, fi, ei, i;
+
+    for(fi = 0;fi < hData->mFds.size();fi++)
+    {
+        for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
+        {
+            for(auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
+            {
+                for(ti = 0;ti < channels;ti++)
+                {
+                    for(i = 0;i < m;i++)
+                        azd.mIrs[ti][i] /= dfa[(ti * m) + i];
+                }
+            }
+        }
+    }
+}
+
+/* Given field and elevation indices and an azimuth, calculate the indices of
+ * the two HRIRs that bound the coordinate along with a factor for
+ * calculating the continuous HRIR using interpolation.
+ */
+static void CalcAzIndices(const HrirFdT &field, const uint ei, const double az, uint *a0, uint *a1, double *af)
+{
+    double f{(2.0*M_PI + az) * static_cast<double>(field.mEvs[ei].mAzs.size()) / (2.0*M_PI)};
+    const uint i{static_cast<uint>(f) % static_cast<uint>(field.mEvs[ei].mAzs.size())};
+
+    f -= std::floor(f);
+    *a0 = i;
+    *a1 = (i + 1) % static_cast<uint>(field.mEvs[ei].mAzs.size());
+    *af = f;
+}
+
+/* Synthesize any missing onset timings at the bottom elevations of each field.
+ * This just mirrors some top elevations for the bottom, and blends the
+ * remaining elevations (not an accurate model).
+ */
+static void SynthesizeOnsets(HrirDataT *hData)
+{
+    const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+
+    auto proc_field = [channels](HrirFdT &field) -> void
+    {
+        /* Get the starting elevation from the measurements, and use it as the
+         * upper elevation limit for what needs to be calculated.
+         */
+        const uint upperElevReal{field.mEvStart};
+        if(upperElevReal <= 0) return;
+
+        /* Get the lowest half of the missing elevations' delays by mirroring
+         * the top elevation delays. The responses are on a spherical grid
+         * centered between the ears, so these should align.
+         */
+        uint ei{};
+        if(channels > 1)
+        {
+            /* Take the polar opposite position of the desired measurement and
+             * swap the ears.
+             */
+            field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[1];
+            field.mEvs[0].mAzs[0].mDelays[1] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[0];
+            for(ei = 1u;ei < (upperElevReal+1)/2;++ei)
+            {
+                const uint topElev{static_cast<uint>(field.mEvs.size()-ei-1)};
+
+                for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
+                {
+                    uint a0, a1;
+                    double af;
+
+                    /* Rotate this current azimuth by a half-circle, and lookup
+                     * the mirrored elevation to find the indices for the polar
+                     * opposite position (may need blending).
+                     */
+                    const double az{field.mEvs[ei].mAzs[ai].mAzimuth + M_PI};
+                    CalcAzIndices(field, topElev, az, &a0, &a1, &af);
+
+                    /* Blend the delays, and again, swap the ears. */
+                    field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp(
+                        field.mEvs[topElev].mAzs[a0].mDelays[1],
+                        field.mEvs[topElev].mAzs[a1].mDelays[1], af);
+                    field.mEvs[ei].mAzs[ai].mDelays[1] = Lerp(
+                        field.mEvs[topElev].mAzs[a0].mDelays[0],
+                        field.mEvs[topElev].mAzs[a1].mDelays[0], af);
+                }
+            }
+        }
+        else
+        {
+            field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[0];
+            for(ei = 1u;ei < (upperElevReal+1)/2;++ei)
+            {
+                const uint topElev{static_cast<uint>(field.mEvs.size()-ei-1)};
+
+                for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
+                {
+                    uint a0, a1;
+                    double af;
+
+                    /* For mono data sets, mirror the azimuth front<->back
+                     * since the other ear is a mirror of what we have (e.g.
+                     * the left ear's back-left is simulated with the right
+                     * ear's front-right, which uses the left ear's front-left
+                     * measurement).
+                     */
+                    double az{field.mEvs[ei].mAzs[ai].mAzimuth};
+                    if(az <= M_PI) az = M_PI - az;
+                    else az = (M_PI*2.0)-az + M_PI;
+                    CalcAzIndices(field, topElev, az, &a0, &a1, &af);
+
+                    field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp(
+                        field.mEvs[topElev].mAzs[a0].mDelays[0],
+                        field.mEvs[topElev].mAzs[a1].mDelays[0], af);
+                }
+            }
+        }
+        /* Record the lowest elevation filled in with the mirrored top. */
+        const uint lowerElevFake{ei-1u};
+
+        /* Fill in the remaining delays using bilinear interpolation. This
+         * helps smooth the transition back to the real delays.
+         */
+        for(;ei < upperElevReal;++ei)
+        {
+            const double ef{(field.mEvs[upperElevReal].mElevation - field.mEvs[ei].mElevation) /
+                (field.mEvs[upperElevReal].mElevation - field.mEvs[lowerElevFake].mElevation)};
+
+            for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
+            {
+                uint a0, a1, a2, a3;
+                double af0, af1;
+
+                double az{field.mEvs[ei].mAzs[ai].mAzimuth};
+                CalcAzIndices(field, upperElevReal, az, &a0, &a1, &af0);
+                CalcAzIndices(field, lowerElevFake, az, &a2, &a3, &af1);
+                double blend[4]{
+                    (1.0-ef) * (1.0-af0),
+                    (1.0-ef) * (    af0),
+                    (    ef) * (1.0-af1),
+                    (    ef) * (    af1)
+                };
+
+                for(uint ti{0u};ti < channels;ti++)
+                {
+                    field.mEvs[ei].mAzs[ai].mDelays[ti] =
+                        field.mEvs[upperElevReal].mAzs[a0].mDelays[ti]*blend[0] +
+                        field.mEvs[upperElevReal].mAzs[a1].mDelays[ti]*blend[1] +
+                        field.mEvs[lowerElevFake].mAzs[a2].mDelays[ti]*blend[2] +
+                        field.mEvs[lowerElevFake].mAzs[a3].mDelays[ti]*blend[3];
+                }
+            }
+        }
+    };
+    std::for_each(hData->mFds.begin(), hData->mFds.end(), proc_field);
+}
+
+/* Attempt to synthesize any missing HRIRs at the bottom elevations of each
+ * field.  Right now this just blends the lowest elevation HRIRs together and
+ * applies a low-pass filter to simulate body occlusion.  It is a simple, if
+ * inaccurate model.
+ */
+static void SynthesizeHrirs(HrirDataT *hData)
+{
+    const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+    auto htemp = std::vector<complex_d>(hData->mFftSize);
+    const uint m{hData->mFftSize/2u + 1u};
+    auto filter = std::vector<double>(m);
+    const double beta{3.5e-6 * hData->mIrRate};
+
+    auto proc_field = [channels,m,beta,&htemp,&filter](HrirFdT &field) -> void
+    {
+        const uint oi{field.mEvStart};
+        if(oi <= 0) return;
+
+        for(uint ti{0u};ti < channels;ti++)
+        {
+            uint a0, a1;
+            double af;
+
+            /* Use the lowest immediate-left response for the left ear and
+             * lowest immediate-right response for the right ear. Given no comb
+             * effects as a result of the left response reaching the right ear
+             * and vice-versa, this produces a decent phantom-center response
+             * underneath the head.
+             */
+            CalcAzIndices(field, oi, ((ti==0) ? -M_PI : M_PI) / 2.0, &a0, &a1, &af);
+            for(uint i{0u};i < m;i++)
+            {
+                field.mEvs[0].mAzs[0].mIrs[ti][i] = Lerp(field.mEvs[oi].mAzs[a0].mIrs[ti][i],
+                    field.mEvs[oi].mAzs[a1].mIrs[ti][i], af);
+            }
+        }
+
+        for(uint ei{1u};ei < field.mEvStart;ei++)
+        {
+            const double of{static_cast<double>(ei) / field.mEvStart};
+            const double b{(1.0 - of) * beta};
+            double lp[4]{};
+
+            /* Calculate a low-pass filter to simulate body occlusion. */
+            lp[0] = Lerp(1.0, lp[0], b);
+            lp[1] = Lerp(lp[0], lp[1], b);
+            lp[2] = Lerp(lp[1], lp[2], b);
+            lp[3] = Lerp(lp[2], lp[3], b);
+            htemp[0] = lp[3];
+            for(size_t i{1u};i < htemp.size();i++)
+            {
+                lp[0] = Lerp(0.0, lp[0], b);
+                lp[1] = Lerp(lp[0], lp[1], b);
+                lp[2] = Lerp(lp[1], lp[2], b);
+                lp[3] = Lerp(lp[2], lp[3], b);
+                htemp[i] = lp[3];
+            }
+            /* Get the filter's frequency-domain response and extract the
+             * frequency magnitudes (phase will be reconstructed later)).
+             */
+            FftForward(static_cast<uint>(htemp.size()), htemp.data());
+            std::transform(htemp.cbegin(), htemp.cbegin()+m, filter.begin(),
+                [](const complex_d &c) -> double { return std::abs(c); });
+
+            for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
+            {
+                uint a0, a1;
+                double af;
+
+                CalcAzIndices(field, oi, field.mEvs[ei].mAzs[ai].mAzimuth, &a0, &a1, &af);
+                for(uint ti{0u};ti < channels;ti++)
+                {
+                    for(uint i{0u};i < m;i++)
+                    {
+                        /* Blend the two defined HRIRs closest to this azimuth,
+                         * then blend that with the synthesized -90 elevation.
+                         */
+                        const double s1{Lerp(field.mEvs[oi].mAzs[a0].mIrs[ti][i],
+                            field.mEvs[oi].mAzs[a1].mIrs[ti][i], af)};
+                        const double s{Lerp(field.mEvs[0].mAzs[0].mIrs[ti][i], s1, of)};
+                        field.mEvs[ei].mAzs[ai].mIrs[ti][i] = s * filter[i];
+                    }
+                }
+            }
+        }
+        const double b{beta};
+        double lp[4]{};
+        lp[0] = Lerp(1.0, lp[0], b);
+        lp[1] = Lerp(lp[0], lp[1], b);
+        lp[2] = Lerp(lp[1], lp[2], b);
+        lp[3] = Lerp(lp[2], lp[3], b);
+        htemp[0] = lp[3];
+        for(size_t i{1u};i < htemp.size();i++)
+        {
+            lp[0] = Lerp(0.0, lp[0], b);
+            lp[1] = Lerp(lp[0], lp[1], b);
+            lp[2] = Lerp(lp[1], lp[2], b);
+            lp[3] = Lerp(lp[2], lp[3], b);
+            htemp[i] = lp[3];
+        }
+        FftForward(static_cast<uint>(htemp.size()), htemp.data());
+        std::transform(htemp.cbegin(), htemp.cbegin()+m, filter.begin(),
+            [](const complex_d &c) -> double { return std::abs(c); });
+
+        for(uint ti{0u};ti < channels;ti++)
+        {
+            for(uint i{0u};i < m;i++)
+                field.mEvs[0].mAzs[0].mIrs[ti][i] *= filter[i];
+        }
+    };
+    std::for_each(hData->mFds.begin(), hData->mFds.end(), proc_field);
+}
+
+// The following routines assume a full set of HRIRs for all elevations.
+
+/* Perform minimum-phase reconstruction using the magnitude responses of the
+ * HRIR set. Work is delegated to this struct, which runs asynchronously on one
+ * or more threads (sharing the same reconstructor object).
+ */
+struct HrirReconstructor {
+    std::vector<double*> mIrs;
+    std::atomic<size_t> mCurrent;
+    std::atomic<size_t> mDone;
+    uint mFftSize;
+    uint mIrPoints;
+
+    void Worker()
+    {
+        auto h = std::vector<complex_d>(mFftSize);
+        auto mags = std::vector<double>(mFftSize);
+        size_t m{(mFftSize/2) + 1};
+
+        while(1)
+        {
+            /* Load the current index to process. */
+            size_t idx{mCurrent.load()};
+            do {
+                /* If the index is at the end, we're done. */
+                if(idx >= mIrs.size())
+                    return;
+                /* Otherwise, increment the current index atomically so other
+                 * threads know to go to the next one. If this call fails, the
+                 * current index was just changed by another thread and the new
+                 * value is loaded into idx, which we'll recheck.
+                 */
+            } while(!mCurrent.compare_exchange_weak(idx, idx+1, std::memory_order_relaxed));
+
+            /* Now do the reconstruction, and apply the inverse FFT to get the
+             * time-domain response.
+             */
+            for(size_t i{0};i < m;++i)
+                mags[i] = std::max(mIrs[idx][i], EPSILON);
+            MinimumPhase(mFftSize, mags.data(), h.data());
+            FftInverse(mFftSize, h.data());
+            for(uint i{0u};i < mIrPoints;++i)
+                mIrs[idx][i] = h[i].real();
+
+            /* Increment the number of IRs done. */
+            mDone.fetch_add(1);
+        }
+    }
+};
+
+static void ReconstructHrirs(const HrirDataT *hData, const uint numThreads)
+{
+    const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+
+    /* Set up the reconstructor with the needed size info and pointers to the
+     * IRs to process.
+     */
+    HrirReconstructor reconstructor;
+    reconstructor.mCurrent.store(0, std::memory_order_relaxed);
+    reconstructor.mDone.store(0, std::memory_order_relaxed);
+    reconstructor.mFftSize = hData->mFftSize;
+    reconstructor.mIrPoints = hData->mIrPoints;
+    for(const auto &field : hData->mFds)
+    {
+        for(auto &elev : field.mEvs)
+        {
+            for(const auto &azd : elev.mAzs)
+            {
+                for(uint ti{0u};ti < channels;ti++)
+                    reconstructor.mIrs.push_back(azd.mIrs[ti]);
+            }
+        }
+    }
+
+    /* Launch threads to work on reconstruction. */
+    std::vector<std::thread> thrds;
+    thrds.reserve(numThreads);
+    for(size_t i{0};i < numThreads;++i)
+        thrds.emplace_back(std::mem_fn(&HrirReconstructor::Worker), &reconstructor);
+
+    /* Keep track of the number of IRs done, periodically reporting it. */
+    size_t count;
+    do {
+        std::this_thread::sleep_for(std::chrono::milliseconds{50});
+
+        count = reconstructor.mDone.load();
+        size_t pcdone{count * 100 / reconstructor.mIrs.size()};
+
+        printf("\r%3zu%% done (%zu of %zu)", pcdone, count, reconstructor.mIrs.size());
+        fflush(stdout);
+    } while(count < reconstructor.mIrs.size());
+    fputc('\n', stdout);
+
+    for(auto &thrd : thrds)
+    {
+        if(thrd.joinable())
+            thrd.join();
+    }
+}
+
+// Normalize the HRIR set and slightly attenuate the result.
+static void NormalizeHrirs(HrirDataT *hData)
+{
+    const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
+    const uint irSize{hData->mIrPoints};
+
+    /* Find the maximum amplitude and RMS out of all the IRs. */
+    struct LevelPair { double amp, rms; };
+    auto mesasure_channel = [irSize](const LevelPair levels, const double *ir)
+    {
+        /* Calculate the peak amplitude and RMS of this IR. */
+        auto current = std::accumulate(ir, ir+irSize, LevelPair{0.0, 0.0},
+            [](const LevelPair cur, const double impulse)
+            {
+                return LevelPair{std::max(std::abs(impulse), cur.amp), cur.rms + impulse*impulse};
+            });
+        current.rms = std::sqrt(current.rms / irSize);
+
+        /* Accumulate levels by taking the maximum amplitude and RMS. */
+        return LevelPair{std::max(current.amp, levels.amp), std::max(current.rms, levels.rms)};
+    };
+    auto measure_azi = [channels,mesasure_channel](const LevelPair levels, const HrirAzT &azi)
+    { return std::accumulate(azi.mIrs, azi.mIrs+channels, levels, mesasure_channel); };
+    auto measure_elev = [measure_azi](const LevelPair levels, const HrirEvT &elev)
+    { return std::accumulate(elev.mAzs.cbegin(), elev.mAzs.cend(), levels, measure_azi); };
+    auto measure_field = [measure_elev](const LevelPair levels, const HrirFdT &field)
+    { return std::accumulate(field.mEvs.cbegin(), field.mEvs.cend(), levels, measure_elev); };
+
+    const auto maxlev = std::accumulate(hData->mFds.begin(), hData->mFds.end(),
+        LevelPair{0.0, 0.0}, measure_field);
+
+    /* Normalize using the maximum RMS of the HRIRs. The RMS measure for the
+     * non-filtered signal is of an impulse with equal length (to the filter):
+     *
+     * rms_impulse = sqrt(sum([ 1^2, 0^2, 0^2, ... ]) / n)
+     *             = sqrt(1 / n)
+     *
+     * This helps keep a more consistent volume between the non-filtered signal
+     * and various data sets.
+     */
+    double factor{std::sqrt(1.0 / irSize) / maxlev.rms};
+
+    /* Also ensure the samples themselves won't clip. */
+    factor = std::min(factor, 0.99/maxlev.amp);
+
+    /* Now scale all IRs by the given factor. */
+    auto proc_channel = [irSize,factor](double *ir)
+    { std::transform(ir, ir+irSize, ir, [factor](double s){ return s * factor; }); };
+    auto proc_azi = [channels,proc_channel](HrirAzT &azi)
+    { std::for_each(azi.mIrs, azi.mIrs+channels, proc_channel); };
+    auto proc_elev = [proc_azi](HrirEvT &elev)
+    { std::for_each(elev.mAzs.begin(), elev.mAzs.end(), proc_azi); };
+    auto proc1_field = [proc_elev](HrirFdT &field)
+    { std::for_each(field.mEvs.begin(), field.mEvs.end(), proc_elev); };
+
+    std::for_each(hData->mFds.begin(), hData->mFds.end(), proc1_field);
+}
+
+// Calculate the left-ear time delay using a spherical head model.
+static double CalcLTD(const double ev, const double az, const double rad, const double dist)
+{
+    double azp, dlp, l, al;
+
+    azp = std::asin(std::cos(ev) * std::sin(az));
+    dlp = std::sqrt((dist*dist) + (rad*rad) + (2.0*dist*rad*sin(azp)));
+    l = std::sqrt((dist*dist) - (rad*rad));
+    al = (0.5 * M_PI) + azp;
+    if(dlp > l)
+        dlp = l + (rad * (al - std::acos(rad / dist)));
+    return dlp / 343.3;
+}
+
+// Calculate the effective head-related time delays for each minimum-phase
+// HRIR. This is done per-field since distance delay is ignored.
+static void CalculateHrtds(const HeadModelT model, const double radius, HrirDataT *hData)
+{
+    uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1;
+    double customRatio{radius / hData->mRadius};
+    uint ti;
+
+    if(model == HM_SPHERE)
+    {
+        for(auto &field : hData->mFds)
+        {
+            for(auto &elev : field.mEvs)
+            {
+                for(auto &azd : elev.mAzs)
+                {
+                    for(ti = 0;ti < channels;ti++)
+                        azd.mDelays[ti] = CalcLTD(elev.mElevation, azd.mAzimuth, radius, field.mDistance);
+                }
+            }
+        }
+    }
+    else if(customRatio != 1.0)
+    {
+        for(auto &field : hData->mFds)
+        {
+            for(auto &elev : field.mEvs)
+            {
+                for(auto &azd : elev.mAzs)
+                {
+                    for(ti = 0;ti < channels;ti++)
+                        azd.mDelays[ti] *= customRatio;
+                }
+            }
+        }
+    }
+
+    double maxHrtd{0.0};
+    for(auto &field : hData->mFds)
+    {
+        double minHrtd{std::numeric_limits<double>::infinity()};
+        for(auto &elev : field.mEvs)
+        {
+            for(auto &azd : elev.mAzs)
+            {
+                for(ti = 0;ti < channels;ti++)
+                    minHrtd = std::min(azd.mDelays[ti], minHrtd);
+            }
+        }
+
+        for(auto &elev : field.mEvs)
+        {
+            for(auto &azd : elev.mAzs)
+            {
+                for(ti = 0;ti < channels;ti++)
+                {
+                    azd.mDelays[ti] = (azd.mDelays[ti]-minHrtd) * hData->mIrRate;
+                    maxHrtd = std::max(maxHrtd, azd.mDelays[ti]);
+                }
+            }
+        }
+    }
+    if(maxHrtd > MAX_HRTD)
+    {
+        fprintf(stdout, "  Scaling for max delay of %f samples to %f\n...\n", maxHrtd, MAX_HRTD);
+        const double scale{MAX_HRTD / maxHrtd};
+        for(auto &field : hData->mFds)
+        {
+            for(auto &elev : field.mEvs)
+            {
+                for(auto &azd : elev.mAzs)
+                {
+                    for(ti = 0;ti < channels;ti++)
+                        azd.mDelays[ti] *= scale;
+                }
+            }
+        }
+    }
+}
+
+// Allocate and configure dynamic HRIR structures.
+bool PrepareHrirData(const al::span<const double> distances,
+    const al::span<const uint,MAX_FD_COUNT> evCounts,
+    const al::span<const std::array<uint,MAX_EV_COUNT>,MAX_FD_COUNT> azCounts, HrirDataT *hData)
+{
+    uint evTotal{0}, azTotal{0};
+
+    for(size_t fi{0};fi < distances.size();++fi)
+    {
+        evTotal += evCounts[fi];
+        for(size_t ei{0};ei < evCounts[fi];++ei)
+            azTotal += azCounts[fi][ei];
+    }
+    if(!evTotal || !azTotal)
+        return false;
+
+    hData->mEvsBase.resize(evTotal);
+    hData->mAzsBase.resize(azTotal);
+    hData->mFds.resize(distances.size());
+    hData->mIrCount = azTotal;
+    evTotal = 0;
+    azTotal = 0;
+    for(size_t fi{0};fi < distances.size();++fi)
+    {
+        hData->mFds[fi].mDistance = distances[fi];
+        hData->mFds[fi].mEvStart = 0;
+        hData->mFds[fi].mEvs = {&hData->mEvsBase[evTotal], evCounts[fi]};
+        evTotal += evCounts[fi];
+        for(uint ei{0};ei < evCounts[fi];++ei)
+        {
+            uint azCount = azCounts[fi][ei];
+
+            hData->mFds[fi].mEvs[ei].mElevation = -M_PI / 2.0 + M_PI * ei / (evCounts[fi] - 1);
+            hData->mFds[fi].mEvs[ei].mAzs = {&hData->mAzsBase[azTotal], azCount};
+            for(uint ai{0};ai < azCount;ai++)
+            {
+                hData->mFds[fi].mEvs[ei].mAzs[ai].mAzimuth = 2.0 * M_PI * ai / azCount;
+                hData->mFds[fi].mEvs[ei].mAzs[ai].mIndex = azTotal + ai;
+                hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[0] = 0.0;
+                hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[1] = 0.0;
+                hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[0] = nullptr;
+                hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[1] = nullptr;
+            }
+            azTotal += azCount;
+        }
+    }
+    return true;
+}
+
+
+/* Parse the data set definition and process the source data, storing the
+ * resulting data set as desired.  If the input name is NULL it will read
+ * from standard input.
+ */
+static int ProcessDefinition(const char *inName, const uint outRate, const ChannelModeT chanMode,
+    const bool farfield, const uint numThreads, const uint fftSize, const int equalize,
+    const int surface, const double limit, const uint truncSize, const HeadModelT model,
+    const double radius, const char *outName)
+{
+    HrirDataT hData;
+
+    fprintf(stdout, "Using %u thread%s.\n", numThreads, (numThreads==1)?"":"s");
+    if(!inName)
+    {
+        inName = "stdin";
+        fprintf(stdout, "Reading HRIR definition from %s...\n", inName);
+        if(!LoadDefInput(std::cin, nullptr, 0, inName, fftSize, truncSize, outRate, chanMode, &hData))
+            return 0;
+    }
+    else
+    {
+        std::unique_ptr<al::ifstream> input{new al::ifstream{inName}};
+        if(!input->is_open())
+        {
+            fprintf(stderr, "Error: Could not open input file '%s'\n", inName);
+            return 0;
+        }
+
+        char startbytes[4]{};
+        input->read(startbytes, sizeof(startbytes));
+        std::streamsize startbytecount{input->gcount()};
+        if(startbytecount != sizeof(startbytes) || !input->good())
+        {
+            fprintf(stderr, "Error: Could not read input file '%s'\n", inName);
+            return 0;
+        }
+
+        if(startbytes[0] == '\x89' && startbytes[1] == 'H' && startbytes[2] == 'D'
+            && startbytes[3] == 'F')
+        {
+            input = nullptr;
+            fprintf(stdout, "Reading HRTF data from %s...\n", inName);
+            if(!LoadSofaFile(inName, numThreads, fftSize, truncSize, outRate, chanMode, &hData))
+                return 0;
+        }
+        else
+        {
+            fprintf(stdout, "Reading HRIR definition from %s...\n", inName);
+            if(!LoadDefInput(*input, startbytes, startbytecount, inName, fftSize, truncSize, outRate, chanMode, &hData))
+                return 0;
+        }
+    }
+
+    if(equalize)
+    {
+        uint c{(hData.mChannelType == CT_STEREO) ? 2u : 1u};
+        uint m{hData.mFftSize/2u + 1u};
+        auto dfa = std::vector<double>(c * m);
+
+        if(hData.mFds.size() > 1)
+        {
+            fprintf(stdout, "Balancing field magnitudes...\n");
+            BalanceFieldMagnitudes(&hData, c, m);
+        }
+        fprintf(stdout, "Calculating diffuse-field average...\n");
+        CalculateDiffuseFieldAverage(&hData, c, m, surface, limit, dfa.data());
+        fprintf(stdout, "Performing diffuse-field equalization...\n");
+        DiffuseFieldEqualize(c, m, dfa.data(), &hData);
+    }
+    if(hData.mFds.size() > 1)
+    {
+        fprintf(stdout, "Sorting %zu fields...\n", hData.mFds.size());
+        std::sort(hData.mFds.begin(), hData.mFds.end(),
+            [](const HrirFdT &lhs, const HrirFdT &rhs) noexcept
+            { return lhs.mDistance < rhs.mDistance; });
+        if(farfield)
+        {
+            fprintf(stdout, "Clearing %zu near field%s...\n", hData.mFds.size()-1,
+                (hData.mFds.size()-1 != 1) ? "s" : "");
+            hData.mFds.erase(hData.mFds.cbegin(), hData.mFds.cend()-1);
+        }
+    }
+    fprintf(stdout, "Synthesizing missing elevations...\n");
+    if(model == HM_DATASET)
+        SynthesizeOnsets(&hData);
+    SynthesizeHrirs(&hData);
+    fprintf(stdout, "Performing minimum phase reconstruction...\n");
+    ReconstructHrirs(&hData, numThreads);
+    fprintf(stdout, "Truncating minimum-phase HRIRs...\n");
+    hData.mIrPoints = truncSize;
+    fprintf(stdout, "Normalizing final HRIRs...\n");
+    NormalizeHrirs(&hData);
+    fprintf(stdout, "Calculating impulse delays...\n");
+    CalculateHrtds(model, (radius > DEFAULT_CUSTOM_RADIUS) ? radius : hData.mRadius, &hData);
+
+    const auto rateStr = std::to_string(hData.mIrRate);
+    const auto expName = StrSubst({outName, strlen(outName)}, {"%r", 2},
+        {rateStr.data(), rateStr.size()});
+    fprintf(stdout, "Creating MHR data set %s...\n", expName.c_str());
+    return StoreMhr(&hData, expName.c_str());
+}
+
+static void PrintHelp(const char *argv0, FILE *ofile)
+{
+    fprintf(ofile, "Usage:  %s [<option>...]\n\n", argv0);
+    fprintf(ofile, "Options:\n");
+    fprintf(ofile, " -r <rate>       Change the data set sample rate to the specified value and\n");
+    fprintf(ofile, "                 resample the HRIRs accordingly.\n");
+    fprintf(ofile, " -m              Change the data set to mono, mirroring the left ear for the\n");
+    fprintf(ofile, "                 right ear.\n");
+    fprintf(ofile, " -a              Change the data set to single field, using the farthest field.\n");
+    fprintf(ofile, " -j <threads>    Number of threads used to process HRIRs (default: 2).\n");
+    fprintf(ofile, " -f <points>     Override the FFT window size (default: %u).\n", DEFAULT_FFTSIZE);
+    fprintf(ofile, " -e {on|off}     Toggle diffuse-field equalization (default: %s).\n", (DEFAULT_EQUALIZE ? "on" : "off"));
+    fprintf(ofile, " -s {on|off}     Toggle surface-weighted diffuse-field average (default: %s).\n", (DEFAULT_SURFACE ? "on" : "off"));
+    fprintf(ofile, " -l {<dB>|none}  Specify a limit to the magnitude range of the diffuse-field\n");
+    fprintf(ofile, "                 average (default: %.2f).\n", DEFAULT_LIMIT);
+    fprintf(ofile, " -w <points>     Specify the size of the truncation window that's applied\n");
+    fprintf(ofile, "                 after minimum-phase reconstruction (default: %u).\n", DEFAULT_TRUNCSIZE);
+    fprintf(ofile, " -d {dataset|    Specify the model used for calculating the head-delay timing\n");
+    fprintf(ofile, "     sphere}     values (default: %s).\n", ((DEFAULT_HEAD_MODEL == HM_DATASET) ? "dataset" : "sphere"));
+    fprintf(ofile, " -c <radius>     Use a customized head radius measured to-ear in meters.\n");
+    fprintf(ofile, " -i <filename>   Specify an HRIR definition file to use (defaults to stdin).\n");
+    fprintf(ofile, " -o <filename>   Specify an output file. Use of '%%r' will be substituted with\n");
+    fprintf(ofile, "                 the data set sample rate.\n");
+}
+
+// Standard command line dispatch.
+int main(int argc, char *argv[])
+{
+    const char *inName = nullptr, *outName = nullptr;
+    uint outRate, fftSize;
+    int equalize, surface;
+    char *end = nullptr;
+    ChannelModeT chanMode;
+    HeadModelT model;
+    uint numThreads;
+    uint truncSize;
+    double radius;
+    bool farfield;
+    double limit;
+    int opt;
+
+    if(argc < 2)
+    {
+        fprintf(stdout, "HRTF Processing and Composition Utility\n\n");
+        PrintHelp(argv[0], stdout);
+        exit(EXIT_SUCCESS);
+    }
+
+    outName = "./oalsoft_hrtf_%r.mhr";
+    outRate = 0;
+    chanMode = CM_AllowStereo;
+    fftSize = DEFAULT_FFTSIZE;
+    equalize = DEFAULT_EQUALIZE;
+    surface = DEFAULT_SURFACE;
+    limit = DEFAULT_LIMIT;
+    numThreads = 2;
+    truncSize = DEFAULT_TRUNCSIZE;
+    model = DEFAULT_HEAD_MODEL;
+    radius = DEFAULT_CUSTOM_RADIUS;
+    farfield = false;
+
+    while((opt=getopt(argc, argv, "r:maj:f:e:s:l:w:d:c:e:i:o:h")) != -1)
+    {
+        switch(opt)
+        {
+        case 'r':
+            outRate = static_cast<uint>(strtoul(optarg, &end, 10));
+            if(end[0] != '\0' || outRate < MIN_RATE || outRate > MAX_RATE)
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %u to %u.\n", optarg, opt, MIN_RATE, MAX_RATE);
+                exit(EXIT_FAILURE);
+            }
+            break;
+
+        case 'm':
+            chanMode = CM_ForceMono;
+            break;
+
+        case 'a':
+            farfield = true;
+            break;
+
+        case 'j':
+            numThreads = static_cast<uint>(strtoul(optarg, &end, 10));
+            if(end[0] != '\0' || numThreads > 64)
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %u to %u.\n", optarg, opt, 0, 64);
+                exit(EXIT_FAILURE);
+            }
+            if(numThreads == 0)
+                numThreads = std::thread::hardware_concurrency();
+            break;
+
+        case 'f':
+            fftSize = static_cast<uint>(strtoul(optarg, &end, 10));
+            if(end[0] != '\0' || (fftSize&(fftSize-1)) || fftSize < MIN_FFTSIZE || fftSize > MAX_FFTSIZE)
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected a power-of-two between %u to %u.\n", optarg, opt, MIN_FFTSIZE, MAX_FFTSIZE);
+                exit(EXIT_FAILURE);
+            }
+            break;
+
+        case 'e':
+            if(strcmp(optarg, "on") == 0)
+                equalize = 1;
+            else if(strcmp(optarg, "off") == 0)
+                equalize = 0;
+            else
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected on or off.\n", optarg, opt);
+                exit(EXIT_FAILURE);
+            }
+            break;
+
+        case 's':
+            if(strcmp(optarg, "on") == 0)
+                surface = 1;
+            else if(strcmp(optarg, "off") == 0)
+                surface = 0;
+            else
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected on or off.\n", optarg, opt);
+                exit(EXIT_FAILURE);
+            }
+            break;
+
+        case 'l':
+            if(strcmp(optarg, "none") == 0)
+                limit = 0.0;
+            else
+            {
+                limit = strtod(optarg, &end);
+                if(end[0] != '\0' || limit < MIN_LIMIT || limit > MAX_LIMIT)
+                {
+                    fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %.0f to %.0f.\n", optarg, opt, MIN_LIMIT, MAX_LIMIT);
+                    exit(EXIT_FAILURE);
+                }
+            }
+            break;
+
+        case 'w':
+            truncSize = static_cast<uint>(strtoul(optarg, &end, 10));
+            if(end[0] != '\0' || truncSize < MIN_TRUNCSIZE || truncSize > MAX_TRUNCSIZE)
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %u to %u.\n", optarg, opt, MIN_TRUNCSIZE, MAX_TRUNCSIZE);
+                exit(EXIT_FAILURE);
+            }
+            break;
+
+        case 'd':
+            if(strcmp(optarg, "dataset") == 0)
+                model = HM_DATASET;
+            else if(strcmp(optarg, "sphere") == 0)
+                model = HM_SPHERE;
+            else
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected dataset or sphere.\n", optarg, opt);
+                exit(EXIT_FAILURE);
+            }
+            break;
+
+        case 'c':
+            radius = strtod(optarg, &end);
+            if(end[0] != '\0' || radius < MIN_CUSTOM_RADIUS || radius > MAX_CUSTOM_RADIUS)
+            {
+                fprintf(stderr, "\nError: Got unexpected value \"%s\" for option -%c, expected between %.2f to %.2f.\n", optarg, opt, MIN_CUSTOM_RADIUS, MAX_CUSTOM_RADIUS);
+                exit(EXIT_FAILURE);
+            }
+            break;
+
+        case 'i':
+            inName = optarg;
+            break;
+
+        case 'o':
+            outName = optarg;
+            break;
+
+        case 'h':
+            PrintHelp(argv[0], stdout);
+            exit(EXIT_SUCCESS);
+
+        default: /* '?' */
+            PrintHelp(argv[0], stderr);
+            exit(EXIT_FAILURE);
+        }
+    }
+
+    int ret = ProcessDefinition(inName, outRate, chanMode, farfield, numThreads, fftSize, equalize,
+        surface, limit, truncSize, model, radius, outName);
+    if(!ret) return -1;
+    fprintf(stdout, "Operation completed.\n");
+
+    return EXIT_SUCCESS;
+}
diff --git a/utils/makemhr/makemhr.h b/utils/makemhr/makemhr.h
new file mode 100644 (file)
index 0000000..13b5b2d
--- /dev/null
@@ -0,0 +1,131 @@
+#ifndef MAKEMHR_H
+#define MAKEMHR_H
+
+#include <vector>
+#include <complex>
+
+#include "alcomplex.h"
+#include "polyphase_resampler.h"
+
+
+// The maximum path length used when processing filenames.
+#define MAX_PATH_LEN                 (256)
+
+// The limit to the number of 'distances' listed in the data set definition.
+// Must be less than 256
+#define MAX_FD_COUNT                 (16)
+
+// The limits to the number of 'elevations' listed in the data set definition.
+// Must be less than 256.
+#define MIN_EV_COUNT                 (5)
+#define MAX_EV_COUNT                 (181)
+
+// The limits for each of the 'azimuths' listed in the data set definition.
+// Must be less than 256.
+#define MIN_AZ_COUNT                 (1)
+#define MAX_AZ_COUNT                 (255)
+
+// The limits for the 'distance' from source to listener for each field in
+// the definition file.
+#define MIN_DISTANCE                 (0.05)
+#define MAX_DISTANCE                 (2.50)
+
+// The limits for the sample 'rate' metric in the data set definition and for
+// resampling.
+#define MIN_RATE                     (32000)
+#define MAX_RATE                     (96000)
+
+// The limits for the HRIR 'points' metric in the data set definition.
+#define MIN_POINTS                   (16)
+#define MAX_POINTS                   (8192)
+
+
+using uint = unsigned int;
+
+/* Complex double type. */
+using complex_d = std::complex<double>;
+
+
+enum ChannelModeT : bool {
+    CM_AllowStereo = false,
+    CM_ForceMono = true
+};
+
+// Sample and channel type enum values.
+enum SampleTypeT {
+    ST_S16 = 0,
+    ST_S24 = 1
+};
+
+// Certain iterations rely on these integer enum values.
+enum ChannelTypeT {
+    CT_NONE   = -1,
+    CT_MONO   = 0,
+    CT_STEREO = 1
+};
+
+// Structured HRIR storage for stereo azimuth pairs, elevations, and fields.
+struct HrirAzT {
+    double mAzimuth{0.0};
+    uint mIndex{0u};
+    double mDelays[2]{0.0, 0.0};
+    double *mIrs[2]{nullptr, nullptr};
+};
+
+struct HrirEvT {
+    double mElevation{0.0};
+    al::span<HrirAzT> mAzs;
+};
+
+struct HrirFdT {
+    double mDistance{0.0};
+    uint mEvStart{0u};
+    al::span<HrirEvT> mEvs;
+};
+
+// The HRIR metrics and data set used when loading, processing, and storing
+// the resulting HRTF.
+struct HrirDataT {
+    uint mIrRate{0u};
+    SampleTypeT mSampleType{ST_S24};
+    ChannelTypeT mChannelType{CT_NONE};
+    uint mIrPoints{0u};
+    uint mFftSize{0u};
+    uint mIrSize{0u};
+    double mRadius{0.0};
+    uint mIrCount{0u};
+
+    std::vector<double> mHrirsBase;
+    std::vector<HrirEvT> mEvsBase;
+    std::vector<HrirAzT> mAzsBase;
+
+    std::vector<HrirFdT> mFds;
+
+    /* GCC warns when it tries to inline this. */
+    ~HrirDataT();
+};
+
+
+bool PrepareHrirData(const al::span<const double> distances,
+    const al::span<const uint,MAX_FD_COUNT> evCounts,
+    const al::span<const std::array<uint,MAX_EV_COUNT>,MAX_FD_COUNT> azCounts, HrirDataT *hData);
+void MagnitudeResponse(const uint n, const complex_d *in, double *out);
+
+// Performs a forward FFT.
+inline void FftForward(const uint n, complex_d *inout)
+{ forward_fft(al::as_span(inout, n)); }
+
+// Performs an inverse FFT.
+inline void FftInverse(const uint n, complex_d *inout)
+{
+    inverse_fft(al::as_span(inout, n));
+    double f{1.0 / n};
+    for(uint i{0};i < n;i++)
+        inout[i] *= f;
+}
+
+// Performs linear interpolation.
+inline double Lerp(const double a, const double b, const double f)
+{ return a + f * (b - a); }
+
+#endif /* MAKEMHR_H */
diff --git a/utils/openal-info.c b/utils/openal-info.c
new file mode 100644 (file)
index 0000000..b646693
--- /dev/null
@@ -0,0 +1,444 @@
+/*
+ * OpenAL Info Utility
+ *
+ * Copyright (c) 2010 by Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "AL/alc.h"
+#include "AL/al.h"
+#include "AL/alext.h"
+
+#include "win_main_utf8.h"
+
+/* C doesn't allow casting between function and non-function pointer types, so
+ * with C99 we need to use a union to reinterpret the pointer type. Pre-C99
+ * still needs to use a normal cast and live with the warning (C++ is fine with
+ * a regular reinterpret_cast).
+ */
+#if __STDC_VERSION__ >= 199901L
+#define FUNCTION_CAST(T, ptr) (union{void *p; T f;}){ptr}.f
+#else
+#define FUNCTION_CAST(T, ptr) (T)(ptr)
+#endif
+
+#define MAX_WIDTH  80
+
+static void printList(const char *list, char separator)
+{
+    size_t col = MAX_WIDTH, len;
+    const char *indent = "    ";
+    const char *next;
+
+    if(!list || *list == '\0')
+    {
+        fprintf(stdout, "\n%s!!! none !!!\n", indent);
+        return;
+    }
+
+    do {
+        next = strchr(list, separator);
+        if(next)
+        {
+            len = (size_t)(next-list);
+            do {
+                next++;
+            } while(*next == separator);
+        }
+        else
+            len = strlen(list);
+
+        if(len + col + 2 >= MAX_WIDTH)
+        {
+            fprintf(stdout, "\n%s", indent);
+            col = strlen(indent);
+        }
+        else
+        {
+            fputc(' ', stdout);
+            col++;
+        }
+
+        len = fwrite(list, 1, len, stdout);
+        col += len;
+
+        if(!next || *next == '\0')
+            break;
+        fputc(',', stdout);
+        col++;
+
+        list = next;
+    } while(1);
+    fputc('\n', stdout);
+}
+
+static void printDeviceList(const char *list)
+{
+    if(!list || *list == '\0')
+        printf("    !!! none !!!\n");
+    else do {
+        printf("    %s\n", list);
+        list += strlen(list) + 1;
+    } while(*list != '\0');
+}
+
+
+static ALenum checkALErrors(int linenum)
+{
+    ALenum err = alGetError();
+    if(err != AL_NO_ERROR)
+        printf("OpenAL Error: %s (0x%x), @ %d\n", alGetString(err), err, linenum);
+    return err;
+}
+#define checkALErrors() checkALErrors(__LINE__)
+
+static ALCenum checkALCErrors(ALCdevice *device, int linenum)
+{
+    ALCenum err = alcGetError(device);
+    if(err != ALC_NO_ERROR)
+        printf("ALC Error: %s (0x%x), @ %d\n", alcGetString(device, err), err, linenum);
+    return err;
+}
+#define checkALCErrors(x) checkALCErrors((x),__LINE__)
+
+
+static void printALCInfo(ALCdevice *device)
+{
+    ALCint major, minor;
+
+    if(device)
+    {
+        const ALCchar *devname = NULL;
+        printf("\n");
+        if(alcIsExtensionPresent(device, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE)
+            devname = alcGetString(device, ALC_ALL_DEVICES_SPECIFIER);
+        if(checkALCErrors(device) != ALC_NO_ERROR || !devname)
+            devname = alcGetString(device, ALC_DEVICE_SPECIFIER);
+        printf("** Info for device \"%s\" **\n", devname);
+    }
+    alcGetIntegerv(device, ALC_MAJOR_VERSION, 1, &major);
+    alcGetIntegerv(device, ALC_MINOR_VERSION, 1, &minor);
+    if(checkALCErrors(device) == ALC_NO_ERROR)
+        printf("ALC version: %d.%d\n", major, minor);
+    if(device)
+    {
+        printf("ALC extensions:");
+        printList(alcGetString(device, ALC_EXTENSIONS), ' ');
+        checkALCErrors(device);
+    }
+}
+
+static void printHRTFInfo(ALCdevice *device)
+{
+    LPALCGETSTRINGISOFT alcGetStringiSOFT;
+    ALCint num_hrtfs;
+
+    if(alcIsExtensionPresent(device, "ALC_SOFT_HRTF") == ALC_FALSE)
+    {
+        printf("HRTF extension not available\n");
+        return;
+    }
+
+    alcGetStringiSOFT = FUNCTION_CAST(LPALCGETSTRINGISOFT,
+        alcGetProcAddress(device, "alcGetStringiSOFT"));
+
+    alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtfs);
+    if(!num_hrtfs)
+        printf("No HRTFs found\n");
+    else
+    {
+        ALCint i;
+        printf("Available HRTFs:\n");
+        for(i = 0;i < num_hrtfs;++i)
+        {
+            const ALCchar *name = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i);
+            printf("    %s\n", name);
+        }
+    }
+    checkALCErrors(device);
+}
+
+static void printModeInfo(ALCdevice *device)
+{
+    ALCint srate = 0;
+
+    if(alcIsExtensionPresent(device, "ALC_SOFT_output_mode"))
+    {
+        const char *modename = "(error)";
+        ALCenum mode = 0;
+
+        alcGetIntegerv(device, ALC_OUTPUT_MODE_SOFT, 1, &mode);
+        checkALCErrors(device);
+        switch(mode)
+        {
+        case ALC_ANY_SOFT: modename = "Unknown / unspecified"; break;
+        case ALC_MONO_SOFT: modename = "Mono"; break;
+        case ALC_STEREO_SOFT: modename = "Stereo (unspecified encoding)"; break;
+        case ALC_STEREO_BASIC_SOFT: modename = "Stereo (basic)"; break;
+        case ALC_STEREO_UHJ_SOFT: modename = "Stereo (UHJ)"; break;
+        case ALC_STEREO_HRTF_SOFT: modename = "Stereo (HRTF)"; break;
+        case ALC_QUAD_SOFT: modename = "Quadraphonic"; break;
+        case ALC_SURROUND_5_1_SOFT: modename = "5.1 Surround"; break;
+        case ALC_SURROUND_6_1_SOFT: modename = "6.1 Surround"; break;
+        case ALC_SURROUND_7_1_SOFT: modename = "7.1 Surround"; break;
+        }
+        printf("Device output mode: %s\n", modename);
+    }
+    else
+        printf("Output mode extension not available\n");
+
+    alcGetIntegerv(device, ALC_FREQUENCY, 1, &srate);
+    if(checkALCErrors(device) == ALC_NO_ERROR)
+        printf("Device sample rate: %dhz\n", srate);
+}
+
+static void printALInfo(void)
+{
+    printf("OpenAL vendor string: %s\n", alGetString(AL_VENDOR));
+    printf("OpenAL renderer string: %s\n", alGetString(AL_RENDERER));
+    printf("OpenAL version string: %s\n", alGetString(AL_VERSION));
+    printf("OpenAL extensions:");
+    printList(alGetString(AL_EXTENSIONS), ' ');
+    checkALErrors();
+}
+
+static void printResamplerInfo(void)
+{
+    LPALGETSTRINGISOFT alGetStringiSOFT;
+    ALint num_resamplers;
+    ALint def_resampler;
+
+    if(!alIsExtensionPresent("AL_SOFT_source_resampler"))
+    {
+        printf("Resampler info not available\n");
+        return;
+    }
+
+    alGetStringiSOFT = FUNCTION_CAST(LPALGETSTRINGISOFT, alGetProcAddress("alGetStringiSOFT"));
+
+    num_resamplers = alGetInteger(AL_NUM_RESAMPLERS_SOFT);
+    def_resampler = alGetInteger(AL_DEFAULT_RESAMPLER_SOFT);
+
+    if(!num_resamplers)
+        printf("!!! No resamplers found !!!\n");
+    else
+    {
+        ALint i;
+        printf("Available resamplers:\n");
+        for(i = 0;i < num_resamplers;++i)
+        {
+            const ALchar *name = alGetStringiSOFT(AL_RESAMPLER_NAME_SOFT, i);
+            printf("    %s%s\n", name, (i==def_resampler)?" *":"");
+        }
+    }
+    checkALErrors();
+}
+
+static void printEFXInfo(ALCdevice *device)
+{
+    static LPALGENFILTERS palGenFilters;
+    static LPALDELETEFILTERS palDeleteFilters;
+    static LPALFILTERI palFilteri;
+    static LPALGENEFFECTS palGenEffects;
+    static LPALDELETEEFFECTS palDeleteEffects;
+    static LPALEFFECTI palEffecti;
+
+    static const ALint filters[] = {
+        AL_FILTER_LOWPASS, AL_FILTER_HIGHPASS, AL_FILTER_BANDPASS,
+        AL_FILTER_NULL
+    };
+    char filterNames[] = "Low-pass,High-pass,Band-pass,";
+    static const ALint effects[] = {
+        AL_EFFECT_EAXREVERB, AL_EFFECT_REVERB, AL_EFFECT_CHORUS,
+        AL_EFFECT_DISTORTION, AL_EFFECT_ECHO, AL_EFFECT_FLANGER,
+        AL_EFFECT_FREQUENCY_SHIFTER, AL_EFFECT_VOCAL_MORPHER,
+        AL_EFFECT_PITCH_SHIFTER, AL_EFFECT_RING_MODULATOR,
+        AL_EFFECT_AUTOWAH, AL_EFFECT_COMPRESSOR, AL_EFFECT_EQUALIZER,
+        AL_EFFECT_NULL
+    };
+    static const ALint dedeffects[] = {
+        AL_EFFECT_DEDICATED_DIALOGUE, AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT,
+        AL_EFFECT_NULL
+    };
+    char effectNames[] = "EAX Reverb,Reverb,Chorus,Distortion,Echo,Flanger,"
+        "Frequency Shifter,Vocal Morpher,Pitch Shifter,Ring Modulator,Autowah,"
+        "Compressor,Equalizer,Dedicated Dialog,Dedicated LFE,";
+    ALCint major, minor, sends;
+    ALuint object;
+    char *current;
+    int i;
+
+    if(alcIsExtensionPresent(device, "ALC_EXT_EFX") == AL_FALSE)
+    {
+        printf("EFX not available\n");
+        return;
+    }
+
+    palGenFilters = FUNCTION_CAST(LPALGENFILTERS, alGetProcAddress("alGenFilters"));
+    palDeleteFilters = FUNCTION_CAST(LPALDELETEFILTERS, alGetProcAddress("alDeleteFilters"));
+    palFilteri = FUNCTION_CAST(LPALFILTERI, alGetProcAddress("alFilteri"));
+    palGenEffects = FUNCTION_CAST(LPALGENEFFECTS, alGetProcAddress("alGenEffects"));
+    palDeleteEffects = FUNCTION_CAST(LPALDELETEEFFECTS, alGetProcAddress("alDeleteEffects"));
+    palEffecti = FUNCTION_CAST(LPALEFFECTI, alGetProcAddress("alEffecti"));
+
+    alcGetIntegerv(device, ALC_EFX_MAJOR_VERSION, 1, &major);
+    alcGetIntegerv(device, ALC_EFX_MINOR_VERSION, 1, &minor);
+    if(checkALCErrors(device) == ALC_NO_ERROR)
+        printf("EFX version: %d.%d\n", major, minor);
+    alcGetIntegerv(device, ALC_MAX_AUXILIARY_SENDS, 1, &sends);
+    if(checkALCErrors(device) == ALC_NO_ERROR)
+        printf("Max auxiliary sends: %d\n", sends);
+
+    palGenFilters(1, &object);
+    checkALErrors();
+
+    current = filterNames;
+    for(i = 0;filters[i] != AL_FILTER_NULL;i++)
+    {
+        char *next = strchr(current, ',');
+        assert(next != NULL);
+
+        palFilteri(object, AL_FILTER_TYPE, filters[i]);
+        if(alGetError() != AL_NO_ERROR)
+            memmove(current, next+1, strlen(next));
+        else
+            current = next+1;
+    }
+    printf("Supported filters:");
+    printList(filterNames, ',');
+
+    palDeleteFilters(1, &object);
+    palGenEffects(1, &object);
+    checkALErrors();
+
+    current = effectNames;
+    for(i = 0;effects[i] != AL_EFFECT_NULL;i++)
+    {
+        char *next = strchr(current, ',');
+        assert(next != NULL);
+
+        palEffecti(object, AL_EFFECT_TYPE, effects[i]);
+        if(alGetError() != AL_NO_ERROR)
+            memmove(current, next+1, strlen(next));
+        else
+            current = next+1;
+    }
+    if(alcIsExtensionPresent(device, "ALC_EXT_DEDICATED"))
+    {
+        for(i = 0;dedeffects[i] != AL_EFFECT_NULL;i++)
+        {
+            char *next = strchr(current, ',');
+            assert(next != NULL);
+
+            palEffecti(object, AL_EFFECT_TYPE, dedeffects[i]);
+            if(alGetError() != AL_NO_ERROR)
+                memmove(current, next+1, strlen(next));
+            else
+                current = next+1;
+        }
+    }
+    else
+    {
+        for(i = 0;dedeffects[i] != AL_EFFECT_NULL;i++)
+        {
+            char *next = strchr(current, ',');
+            assert(next != NULL);
+            memmove(current, next+1, strlen(next));
+        }
+    }
+    printf("Supported effects:");
+    printList(effectNames, ',');
+
+    palDeleteEffects(1, &object);
+    checkALErrors();
+}
+
+int main(int argc, char *argv[])
+{
+    ALCdevice *device;
+    ALCcontext *context;
+
+#ifdef _WIN32
+    /* OpenAL Soft gives UTF-8 strings, so set the console to expect that. */
+    SetConsoleOutputCP(CP_UTF8);
+#endif
+
+    if(argc > 1 && (strcmp(argv[1], "--help") == 0 ||
+                    strcmp(argv[1], "-h") == 0))
+    {
+        printf("Usage: %s [playback device]\n", argv[0]);
+        return 0;
+    }
+
+    printf("Available playback devices:\n");
+    if(alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE)
+        printDeviceList(alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER));
+    else
+        printDeviceList(alcGetString(NULL, ALC_DEVICE_SPECIFIER));
+    printf("Available capture devices:\n");
+    printDeviceList(alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER));
+
+    if(alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE)
+        printf("Default playback device: %s\n",
+               alcGetString(NULL, ALC_DEFAULT_ALL_DEVICES_SPECIFIER));
+    else
+        printf("Default playback device: %s\n",
+               alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER));
+    printf("Default capture device: %s\n",
+           alcGetString(NULL, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER));
+
+    printALCInfo(NULL);
+
+    device = alcOpenDevice((argc>1) ? argv[1] : NULL);
+    if(!device)
+    {
+        printf("\n!!! Failed to open %s !!!\n\n", ((argc>1) ? argv[1] : "default device"));
+        return 1;
+    }
+    printALCInfo(device);
+    printHRTFInfo(device);
+
+    context = alcCreateContext(device, NULL);
+    if(!context || alcMakeContextCurrent(context) == ALC_FALSE)
+    {
+        if(context)
+            alcDestroyContext(context);
+        alcCloseDevice(device);
+        printf("\n!!! Failed to set a context !!!\n\n");
+        return 1;
+    }
+
+    printModeInfo(device);
+    printALInfo();
+    printResamplerInfo();
+    printEFXInfo(device);
+
+    alcMakeContextCurrent(NULL);
+    alcDestroyContext(context);
+    alcCloseDevice(device);
+
+    return 0;
+}
diff --git a/utils/sofa-info.cpp b/utils/sofa-info.cpp
new file mode 100644 (file)
index 0000000..6dffef4
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * SOFA info utility for inspecting SOFA file metrics and determining HRTF
+ * utility compatible layouts.
+ *
+ * Copyright (C) 2018-2019  Christopher Fitzgerald
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Or visit:  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ */
+
+#include <stdio.h>
+
+#include <memory>
+#include <vector>
+
+#include "sofa-support.h"
+
+#include "mysofa.h"
+
+#include "win_main_utf8.h"
+
+using uint = unsigned int;
+
+static void PrintSofaAttributes(const char *prefix, struct MYSOFA_ATTRIBUTE *attribute)
+{
+    while(attribute)
+    {
+        fprintf(stdout, "%s.%s: %s\n", prefix, attribute->name, attribute->value);
+        attribute = attribute->next;
+    }
+}
+
+static void PrintSofaArray(const char *prefix, struct MYSOFA_ARRAY *array)
+{
+    PrintSofaAttributes(prefix, array->attributes);
+    for(uint i{0u};i < array->elements;i++)
+        fprintf(stdout, "%s[%u]: %.6f\n", prefix, i, array->values[i]);
+}
+
+/* Attempts to produce a compatible layout.  Most data sets tend to be
+ * uniform and have the same major axis as used by OpenAL Soft's HRTF model.
+ * This will remove outliers and produce a maximally dense layout when
+ * possible.  Those sets that contain purely random measurements or use
+ * different major axes will fail.
+ */
+static void PrintCompatibleLayout(const uint m, const float *xyzs)
+{
+    fputc('\n', stdout);
+
+    auto fds = GetCompatibleLayout(m, xyzs);
+    if(fds.empty())
+    {
+        fprintf(stdout, "No compatible field layouts in SOFA file.\n");
+        return;
+    }
+
+    uint used_elems{0};
+    for(size_t fi{0u};fi < fds.size();++fi)
+    {
+        for(uint ei{fds[fi].mEvStart};ei < fds[fi].mEvCount;++ei)
+            used_elems += fds[fi].mAzCounts[ei];
+    }
+
+    fprintf(stdout, "Compatible Layout (%u of %u measurements):\n\ndistance = %.3f", used_elems, m,
+        fds[0].mDistance);
+    for(size_t fi{1u};fi < fds.size();fi++)
+        fprintf(stdout, ", %.3f", fds[fi].mDistance);
+
+    fprintf(stdout, "\nazimuths = ");
+    for(size_t fi{0u};fi < fds.size();++fi)
+    {
+        for(uint ei{0u};ei < fds[fi].mEvStart;++ei)
+            fprintf(stdout, "%d%s", fds[fi].mAzCounts[fds[fi].mEvCount - 1 - ei], ", ");
+        for(uint ei{fds[fi].mEvStart};ei < fds[fi].mEvCount;++ei)
+            fprintf(stdout, "%d%s", fds[fi].mAzCounts[ei],
+                (ei < (fds[fi].mEvCount - 1)) ? ", " :
+                (fi < (fds.size() - 1)) ? ";\n           " : "\n");
+    }
+}
+
+// Load and inspect the given SOFA file.
+static void SofaInfo(const char *filename)
+{
+    int err;
+    MySofaHrtfPtr sofa{mysofa_load(filename, &err)};
+    if(!sofa)
+    {
+        fprintf(stdout, "Error: Could not load source file '%s' (%s).\n", filename,
+            SofaErrorStr(err));
+        return;
+    }
+
+    /* NOTE: Some valid SOFA files are failing this check. */
+    err = mysofa_check(sofa.get());
+    if(err != MYSOFA_OK)
+        fprintf(stdout, "Warning: Supposedly malformed source file '%s' (%s).\n", filename,
+            SofaErrorStr(err));
+
+    mysofa_tocartesian(sofa.get());
+
+    PrintSofaAttributes("Info", sofa->attributes);
+
+    fprintf(stdout, "Measurements: %u\n", sofa->M);
+    fprintf(stdout, "Receivers: %u\n", sofa->R);
+    fprintf(stdout, "Emitters: %u\n", sofa->E);
+    fprintf(stdout, "Samples: %u\n", sofa->N);
+
+    PrintSofaArray("SampleRate", &sofa->DataSamplingRate);
+    PrintSofaArray("DataDelay", &sofa->DataDelay);
+
+    PrintCompatibleLayout(sofa->M, sofa->SourcePosition.values);
+}
+
+int main(int argc, char *argv[])
+{
+    if(argc != 2)
+    {
+        fprintf(stdout, "Usage: %s <sofa-file>\n", argv[0]);
+        return 0;
+    }
+
+    SofaInfo(argv[1]);
+
+    return 0;
+}
+
diff --git a/utils/sofa-support.cpp b/utils/sofa-support.cpp
new file mode 100644 (file)
index 0000000..e37789d
--- /dev/null
@@ -0,0 +1,292 @@
+/*
+ * SOFA utility methods for inspecting SOFA file metrics and determining HRTF
+ * utility compatible layouts.
+ *
+ * Copyright (C) 2018-2019  Christopher Fitzgerald
+ * Copyright (C) 2019  Christopher Robinson
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Or visit:  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ */
+
+#include "sofa-support.h"
+
+#include <stdio.h>
+
+#include <algorithm>
+#include <array>
+#include <cmath>
+#include <utility>
+#include <vector>
+
+#include "mysofa.h"
+
+
+namespace {
+
+using uint = unsigned int;
+using double3 = std::array<double,3>;
+
+
+/* Produces a sorted array of unique elements from a particular axis of the
+ * triplets array.  The filters are used to focus on particular coordinates
+ * of other axes as necessary.  The epsilons are used to constrain the
+ * equality of unique elements.
+ */
+std::vector<double> GetUniquelySortedElems(const std::vector<double3> &aers, const uint axis,
+    const double *const (&filters)[3], const double (&epsilons)[3])
+{
+    std::vector<double> elems;
+    for(const double3 &aer : aers)
+    {
+        const double elem{aer[axis]};
+
+        uint j;
+        for(j = 0;j < 3;j++)
+        {
+            if(filters[j] && std::abs(aer[j] - *filters[j]) > epsilons[j])
+                break;
+        }
+        if(j < 3)
+            continue;
+
+        auto iter = elems.begin();
+        for(;iter != elems.end();++iter)
+        {
+            const double delta{elem - *iter};
+            if(delta > epsilons[axis]) continue;
+            if(delta >= -epsilons[axis]) break;
+
+            iter = elems.emplace(iter, elem);
+            break;
+        }
+        if(iter == elems.end())
+            elems.emplace_back(elem);
+    }
+    return elems;
+}
+
+/* Given a list of azimuths, this will produce the smallest step size that can
+ * uniformly cover the list. Ideally this will be over half, but in degenerate
+ * cases this can fall to a minimum of 5 (the lower limit).
+ */
+double GetUniformAzimStep(const double epsilon, const std::vector<double> &elems)
+{
+    if(elems.size() < 5) return 0.0;
+
+    /* Get the maximum count possible, given the first two elements. It would
+     * be impossible to have more than this since the first element must be
+     * included.
+     */
+    uint count{static_cast<uint>(std::ceil(360.0 / (elems[1]-elems[0])))};
+    count = std::min(count, 255u);
+
+    for(;count >= 5;--count)
+    {
+        /* Given the stepping value for this number of elements, check each
+         * multiple to ensure there's a matching element.
+         */
+        const double step{360.0 / count};
+        bool good{true};
+        size_t idx{1u};
+        for(uint mult{1u};mult < count && good;++mult)
+        {
+            const double target{step*mult + elems[0]};
+            while(idx < elems.size() && target-elems[idx] > epsilon)
+                ++idx;
+            good &= (idx < elems.size()) && !(std::abs(target-elems[idx++]) > epsilon);
+        }
+        if(good)
+            return step;
+    }
+    return 0.0;
+}
+
+/* Given a list of elevations, this will produce the smallest step size that
+ * can uniformly cover the list. Ideally this will be over half, but in
+ * degenerate cases this can fall to a minimum of 5 (the lower limit).
+ */
+double GetUniformElevStep(const double epsilon, std::vector<double> &elems)
+{
+    if(elems.size() < 5) return 0.0;
+
+    /* Reverse the elevations so it increments starting with -90 (flipped from
+     * +90). This makes it easier to work out a proper stepping value.
+     */
+    std::reverse(elems.begin(), elems.end());
+    for(auto &v : elems) v *= -1.0;
+
+    uint count{static_cast<uint>(std::ceil(180.0 / (elems[1]-elems[0])))};
+    count = std::min(count, 255u);
+
+    double ret{0.0};
+    for(;count >= 5;--count)
+    {
+        const double step{180.0 / count};
+        bool good{true};
+        size_t idx{1u};
+        /* Elevations don't need to match all multiples if there's not enough
+         * elements to check. Missing elevations can be synthesized.
+         */
+        for(uint mult{1u};mult <= count && idx < elems.size() && good;++mult)
+        {
+            const double target{step*mult + elems[0]};
+            while(idx < elems.size() && target-elems[idx] > epsilon)
+                ++idx;
+            good &= !(idx < elems.size()) || !(std::abs(target-elems[idx++]) > epsilon);
+        }
+        if(good)
+        {
+            ret = step;
+            break;
+        }
+    }
+    /* Re-reverse the elevations to restore the correct order. */
+    for(auto &v : elems) v *= -1.0;
+    std::reverse(elems.begin(), elems.end());
+
+    return ret;
+}
+
+} // namespace
+
+
+const char *SofaErrorStr(int err)
+{
+    switch(err)
+    {
+    case MYSOFA_OK: return "OK";
+    case MYSOFA_INVALID_FORMAT: return "Invalid format";
+    case MYSOFA_UNSUPPORTED_FORMAT: return "Unsupported format";
+    case MYSOFA_INTERNAL_ERROR: return "Internal error";
+    case MYSOFA_NO_MEMORY: return "Out of memory";
+    case MYSOFA_READ_ERROR: return "Read error";
+    }
+    return "Unknown";
+}
+
+std::vector<SofaField> GetCompatibleLayout(const size_t m, const float *xyzs)
+{
+    auto aers = std::vector<double3>(m, double3{});
+    for(size_t i{0u};i < m;++i)
+    {
+        float vals[3]{xyzs[i*3], xyzs[i*3 + 1], xyzs[i*3 + 2]};
+        mysofa_c2s(&vals[0]);
+        aers[i] = {vals[0], vals[1], vals[2]};
+    }
+
+    auto radii = GetUniquelySortedElems(aers, 2, {}, {0.1, 0.1, 0.001});
+    std::vector<SofaField> fds;
+    fds.reserve(radii.size());
+
+    for(const double dist : radii)
+    {
+        auto elevs = GetUniquelySortedElems(aers, 1, {nullptr, nullptr, &dist}, {0.1, 0.1, 0.001});
+
+        /* Remove elevations that don't have a valid set of azimuths. */
+        auto invalid_elev = [&dist,&aers](const double ev) -> bool
+        {
+            auto azims = GetUniquelySortedElems(aers, 0, {nullptr, &ev, &dist}, {0.1, 0.1, 0.001});
+
+            if(std::abs(ev) > 89.999)
+                return azims.size() != 1;
+            if(azims.empty() || !(std::abs(azims[0]) < 0.1))
+                return true;
+            return GetUniformAzimStep(0.1, azims) <= 0.0;
+        };
+        elevs.erase(std::remove_if(elevs.begin(), elevs.end(), invalid_elev), elevs.end());
+
+        double step{GetUniformElevStep(0.1, elevs)};
+        if(step <= 0.0)
+        {
+            if(elevs.empty())
+                fprintf(stdout, "No usable elevations on field distance %f.\n", dist);
+            else
+            {
+                fprintf(stdout, "Non-uniform elevations on field distance %.3f.\nGot: %+.2f", dist,
+                    elevs[0]);
+                for(size_t ei{1u};ei < elevs.size();++ei)
+                    fprintf(stdout, ", %+.2f", elevs[ei]);
+                fputc('\n', stdout);
+            }
+            continue;
+        }
+
+        uint evStart{0u};
+        for(uint ei{0u};ei < elevs.size();ei++)
+        {
+            if(!(elevs[ei] < 0.0))
+            {
+                fprintf(stdout, "Too many missing elevations on field distance %f.\n", dist);
+                return fds;
+            }
+
+            double eif{(90.0+elevs[ei]) / step};
+            const double ev_start{std::round(eif)};
+
+            if(std::abs(eif - ev_start) < (0.1/step))
+            {
+                evStart = static_cast<uint>(ev_start);
+                break;
+            }
+        }
+
+        const auto evCount = static_cast<uint>(std::round(180.0 / step)) + 1;
+        if(evCount < 5)
+        {
+            fprintf(stdout, "Too few uniform elevations on field distance %f.\n", dist);
+            continue;
+        }
+
+        SofaField field{};
+        field.mDistance = dist;
+        field.mEvCount = evCount;
+        field.mEvStart = evStart;
+        field.mAzCounts.resize(evCount, 0u);
+        auto &azCounts = field.mAzCounts;
+
+        for(uint ei{evStart};ei < evCount;ei++)
+        {
+            double ev{-90.0 + ei*180.0/(evCount - 1)};
+            auto azims = GetUniquelySortedElems(aers, 0, {nullptr, &ev, &dist}, {0.1, 0.1, 0.001});
+
+            if(ei == 0 || ei == (evCount-1))
+            {
+                if(azims.size() != 1)
+                {
+                    fprintf(stdout, "Non-singular poles on field distance %f.\n", dist);
+                    return fds;
+                }
+                azCounts[ei] = 1;
+            }
+            else
+            {
+                step = GetUniformAzimStep(0.1, azims);
+                if(step <= 0.0)
+                {
+                    fprintf(stdout, "Non-uniform azimuths on elevation %f, field distance %f.\n",
+                        ev, dist);
+                    return fds;
+                }
+                azCounts[ei] = static_cast<uint>(std::round(360.0f / step));
+            }
+        }
+
+        fds.emplace_back(std::move(field));
+    }
+
+    return fds;
+}
diff --git a/utils/sofa-support.h b/utils/sofa-support.h
new file mode 100644 (file)
index 0000000..1229f49
--- /dev/null
@@ -0,0 +1,30 @@
+#ifndef UTILS_SOFA_SUPPORT_H
+#define UTILS_SOFA_SUPPORT_H
+
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+#include "mysofa.h"
+
+
+struct MySofaDeleter {
+    void operator()(MYSOFA_HRTF *sofa) { mysofa_free(sofa); }
+};
+using MySofaHrtfPtr = std::unique_ptr<MYSOFA_HRTF,MySofaDeleter>;
+
+// Per-field measurement info.
+struct SofaField {
+    using uint = unsigned int;
+
+    double mDistance{0.0};
+    uint mEvCount{0u};
+    uint mEvStart{0u};
+    std::vector<uint> mAzCounts;
+};
+
+const char *SofaErrorStr(int err);
+
+std::vector<SofaField> GetCompatibleLayout(const size_t m, const float *xyzs);
+
+#endif /* UTILS_SOFA_SUPPORT_H */
diff --git a/utils/uhjdecoder.cpp b/utils/uhjdecoder.cpp
new file mode 100644 (file)
index 0000000..6d992e3
--- /dev/null
@@ -0,0 +1,538 @@
+/*
+ * 2-channel UHJ Decoder
+ *
+ * Copyright (c) Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <array>
+#include <complex>
+#include <cstring>
+#include <memory>
+#include <stddef.h>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "albit.h"
+#include "albyte.h"
+#include "alcomplex.h"
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alspan.h"
+#include "vector.h"
+#include "opthelpers.h"
+#include "phase_shifter.h"
+
+#include "sndfile.h"
+
+#include "win_main_utf8.h"
+
+
+struct FileDeleter {
+    void operator()(FILE *file) { fclose(file); }
+};
+using FilePtr = std::unique_ptr<FILE,FileDeleter>;
+
+struct SndFileDeleter {
+    void operator()(SNDFILE *sndfile) { sf_close(sndfile); }
+};
+using SndFilePtr = std::unique_ptr<SNDFILE,SndFileDeleter>;
+
+
+using ubyte = unsigned char;
+using ushort = unsigned short;
+using uint = unsigned int;
+using complex_d = std::complex<double>;
+
+using byte4 = std::array<al::byte,4>;
+
+
+constexpr ubyte SUBTYPE_BFORMAT_FLOAT[]{
+    0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
+    0xca, 0x00, 0x00, 0x00
+};
+
+void fwrite16le(ushort val, FILE *f)
+{
+    ubyte data[2]{ static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff) };
+    fwrite(data, 1, 2, f);
+}
+
+void fwrite32le(uint val, FILE *f)
+{
+    ubyte data[4]{ static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff),
+        static_cast<ubyte>((val>>16)&0xff), static_cast<ubyte>((val>>24)&0xff) };
+    fwrite(data, 1, 4, f);
+}
+
+template<al::endian = al::endian::native>
+byte4 f32AsLEBytes(const float &value) = delete;
+
+template<>
+byte4 f32AsLEBytes<al::endian::little>(const float &value)
+{
+    byte4 ret{};
+    std::memcpy(ret.data(), &value, 4);
+    return ret;
+}
+template<>
+byte4 f32AsLEBytes<al::endian::big>(const float &value)
+{
+    byte4 ret{};
+    std::memcpy(ret.data(), &value, 4);
+    std::swap(ret[0], ret[3]);
+    std::swap(ret[1], ret[2]);
+    return ret;
+}
+
+
+constexpr uint BufferLineSize{1024};
+
+using FloatBufferLine = std::array<float,BufferLineSize>;
+using FloatBufferSpan = al::span<float,BufferLineSize>;
+
+
+struct UhjDecoder {
+    constexpr static size_t sFilterDelay{1024};
+
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mS{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mD{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mT{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mQ{};
+
+    /* History for the FIR filter. */
+    alignas(16) std::array<float,sFilterDelay-1> mDTHistory{};
+    alignas(16) std::array<float,sFilterDelay-1> mSHistory{};
+
+    alignas(16) std::array<float,BufferLineSize + sFilterDelay*2> mTemp{};
+
+    void decode(const float *RESTRICT InSamples, const size_t InChannels,
+        const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo);
+    void decode2(const float *RESTRICT InSamples, const al::span<FloatBufferLine> OutSamples,
+        const size_t SamplesToDo);
+
+    DEF_NEWDEL(UhjDecoder)
+};
+
+const PhaseShifterT<UhjDecoder::sFilterDelay*2> PShift{};
+
+
+/* Decoding UHJ is done as:
+ *
+ * S = Left + Right
+ * D = Left - Right
+ *
+ * W = 0.981532*S + 0.197484*j(0.828331*D + 0.767820*T)
+ * X = 0.418496*S - j(0.828331*D + 0.767820*T)
+ * Y = 0.795968*D - 0.676392*T + j(0.186633*S)
+ * Z = 1.023332*Q
+ *
+ * where j is a +90 degree phase shift. 3-channel UHJ excludes Q, while 2-
+ * channel excludes Q and T. The B-Format signal reconstructed from 2-channel
+ * UHJ should not be run through a normal B-Format decoder, as it needs
+ * different shelf filters.
+ *
+ * NOTE: Some sources specify
+ *
+ * S = (Left + Right)/2
+ * D = (Left - Right)/2
+ *
+ * However, this is incorrect. It's halving Left and Right even though they
+ * were already halved during encoding, causing S and D to be half what they
+ * initially were at the encoding stage. This division is not present in
+ * Gerzon's original paper for deriving Sigma (S) or Delta (D) from the L and R
+ * signals. As proof, taking Y for example:
+ *
+ * Y = 0.795968*D - 0.676392*T + j(0.186633*S)
+ *
+ * * Plug in the encoding parameters, using ? as a placeholder for whether S
+ *   and D should receive an extra 0.5 factor
+ * Y = 0.795968*(j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y)*? -
+ *     0.676392*(j(-0.1432*W + 0.6512*X) - 0.7071068*Y) +
+ *     0.186633*j(0.9396926*W + 0.1855740*X)*?
+ *
+ * * Move common factors in
+ * Y = (j(-0.3420201*0.795968*?*W + 0.5098604*0.795968*?*X) + 0.6554516*0.795968*?*Y) -
+ *     (j(-0.1432*0.676392*W + 0.6512*0.676392*X) - 0.7071068*0.676392*Y) +
+ *     j(0.9396926*0.186633*?*W + 0.1855740*0.186633*?*X)
+ *
+ * * Clean up extraneous groupings
+ * Y = j(-0.3420201*0.795968*?*W + 0.5098604*0.795968*?*X) + 0.6554516*0.795968*?*Y -
+ *     j(-0.1432*0.676392*W + 0.6512*0.676392*X) + 0.7071068*0.676392*Y +
+ *     j*(0.9396926*0.186633*?*W + 0.1855740*0.186633*?*X)
+ *
+ * * Move phase shifts together and combine them
+ * Y = j(-0.3420201*0.795968*?*W + 0.5098604*0.795968*?*X - -0.1432*0.676392*W -
+ *        0.6512*0.676392*X + 0.9396926*0.186633*?*W + 0.1855740*0.186633*?*X) +
+ *     0.6554516*0.795968*?*Y + 0.7071068*0.676392*Y
+ *
+ * * Reorder terms
+ * Y = j(-0.3420201*0.795968*?*W +  0.1432*0.676392*W + 0.9396926*0.186633*?*W +
+ *        0.5098604*0.795968*?*X + -0.6512*0.676392*X + 0.1855740*0.186633*?*X) +
+ *     0.7071068*0.676392*Y + 0.6554516*0.795968*?*Y
+ *
+ * * Move common factors out
+ * Y = j((-0.3420201*0.795968*? +  0.1432*0.676392 + 0.9396926*0.186633*?)*W +
+ *       ( 0.5098604*0.795968*? + -0.6512*0.676392 + 0.1855740*0.186633*?)*X) +
+ *     (0.7071068*0.676392 + 0.6554516*0.795968*?)*Y
+ *
+ * * Result w/ 0.5 factor:
+ * -0.3420201*0.795968*0.5 +  0.1432*0.676392 + 0.9396926*0.186633*0.5 =  0.04843*W
+ *  0.5098604*0.795968*0.5 + -0.6512*0.676392 + 0.1855740*0.186633*0.5 = -0.22023*X
+ *  0.7071068*0.676392                        + 0.6554516*0.795968*0.5 =  0.73914*Y
+ * -> Y = j(0.04843*W + -0.22023*X) + 0.73914*Y
+ *
+ * * Result w/o 0.5 factor:
+ * -0.3420201*0.795968 +  0.1432*0.676392 + 0.9396926*0.186633 = 0.00000*W
+ *  0.5098604*0.795968 + -0.6512*0.676392 + 0.1855740*0.186633 = 0.00000*X
+ *  0.7071068*0.676392                    + 0.6554516*0.795968 = 1.00000*Y
+ * -> Y = j(0.00000*W + 0.00000*X) + 1.00000*Y
+ *
+ * Not halving produces a result matching the original input.
+ */
+void UhjDecoder::decode(const float *RESTRICT InSamples, const size_t InChannels,
+    const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    float *woutput{OutSamples[0].data()};
+    float *xoutput{OutSamples[1].data()};
+    float *youtput{OutSamples[2].data()};
+
+    /* Add a delay to the input channels, to align it with the all-passed
+     * signal.
+     */
+
+    /* S = Left + Right */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mS[sFilterDelay+i] = InSamples[i*InChannels + 0] + InSamples[i*InChannels + 1];
+
+    /* D = Left - Right */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mD[sFilterDelay+i] = InSamples[i*InChannels + 0] - InSamples[i*InChannels + 1];
+
+    if(InChannels > 2)
+    {
+        /* T */
+        for(size_t i{0};i < SamplesToDo;++i)
+            mT[sFilterDelay+i] = InSamples[i*InChannels + 2];
+    }
+    if(InChannels > 3)
+    {
+        /* Q */
+        for(size_t i{0};i < SamplesToDo;++i)
+            mQ[sFilterDelay+i] = InSamples[i*InChannels + 3];
+    }
+
+    /* Precompute j(0.828331*D + 0.767820*T) and store in xoutput. */
+    auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin());
+    std::transform(mD.cbegin(), mD.cbegin()+SamplesToDo+sFilterDelay, mT.cbegin(), tmpiter,
+        [](const float d, const float t) noexcept { return 0.828331f*d + 0.767820f*t; });
+    std::copy_n(mTemp.cbegin()+SamplesToDo, mDTHistory.size(), mDTHistory.begin());
+    PShift.process({xoutput, SamplesToDo}, mTemp.data());
+
+    for(size_t i{0};i < SamplesToDo;++i)
+    {
+        /* W = 0.981532*S + 0.197484*j(0.828331*D + 0.767820*T) */
+        woutput[i] = 0.981532f*mS[i] + 0.197484f*xoutput[i];
+        /* X = 0.418496*S - j(0.828331*D + 0.767820*T) */
+        xoutput[i] = 0.418496f*mS[i] - xoutput[i];
+    }
+
+    /* Precompute j*S and store in youtput. */
+    tmpiter = std::copy(mSHistory.cbegin(), mSHistory.cend(), mTemp.begin());
+    std::copy_n(mS.cbegin(), SamplesToDo+sFilterDelay, tmpiter);
+    std::copy_n(mTemp.cbegin()+SamplesToDo, mSHistory.size(), mSHistory.begin());
+    PShift.process({youtput, SamplesToDo}, mTemp.data());
+
+    for(size_t i{0};i < SamplesToDo;++i)
+    {
+        /* Y = 0.795968*D - 0.676392*T + j(0.186633*S) */
+        youtput[i] = 0.795968f*mD[i] - 0.676392f*mT[i] + 0.186633f*youtput[i];
+    }
+
+    if(OutSamples.size() > 3)
+    {
+        float *zoutput{OutSamples[3].data()};
+        /* Z = 1.023332*Q */
+        for(size_t i{0};i < SamplesToDo;++i)
+            zoutput[i] = 1.023332f*mQ[i];
+    }
+
+    std::copy(mS.begin()+SamplesToDo, mS.begin()+SamplesToDo+sFilterDelay, mS.begin());
+    std::copy(mD.begin()+SamplesToDo, mD.begin()+SamplesToDo+sFilterDelay, mD.begin());
+    std::copy(mT.begin()+SamplesToDo, mT.begin()+SamplesToDo+sFilterDelay, mT.begin());
+    std::copy(mQ.begin()+SamplesToDo, mQ.begin()+SamplesToDo+sFilterDelay, mQ.begin());
+}
+
+/* This is an alternative equation for decoding 2-channel UHJ. Not sure what
+ * the intended benefit is over the above equation as this slightly reduces the
+ * amount of the original left response and has more of the phase-shifted
+ * forward response on the left response.
+ *
+ * This decoding is done as:
+ *
+ * S = Left + Right
+ * D = Left - Right
+ *
+ * W = 0.981530*S + j*0.163585*D
+ * X = 0.418504*S - j*0.828347*D
+ * Y = 0.762956*D + j*0.384230*S
+ *
+ * where j is a +90 degree phase shift.
+ *
+ * NOTE: As above, S and D should not be halved. The only consequence of
+ * halving here is merely a -6dB reduction in output, but it's still incorrect.
+ */
+void UhjDecoder::decode2(const float *RESTRICT InSamples,
+    const al::span<FloatBufferLine> OutSamples, const size_t SamplesToDo)
+{
+    ASSUME(SamplesToDo > 0);
+
+    float *woutput{OutSamples[0].data()};
+    float *xoutput{OutSamples[1].data()};
+    float *youtput{OutSamples[2].data()};
+
+    /* S = Left + Right */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mS[sFilterDelay+i] = InSamples[i*2 + 0] + InSamples[i*2 + 1];
+
+    /* D = Left - Right */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mD[sFilterDelay+i] = InSamples[i*2 + 0] - InSamples[i*2 + 1];
+
+    /* Precompute j*D and store in xoutput. */
+    auto tmpiter = std::copy(mDTHistory.cbegin(), mDTHistory.cend(), mTemp.begin());
+    std::copy_n(mD.cbegin(), SamplesToDo+sFilterDelay, tmpiter);
+    std::copy_n(mTemp.cbegin()+SamplesToDo, mDTHistory.size(), mDTHistory.begin());
+    PShift.process({xoutput, SamplesToDo}, mTemp.data());
+
+    for(size_t i{0};i < SamplesToDo;++i)
+    {
+        /* W = 0.981530*S + j*0.163585*D */
+        woutput[i] = 0.981530f*mS[i] + 0.163585f*xoutput[i];
+        /* X = 0.418504*S - j*0.828347*D */
+        xoutput[i] = 0.418504f*mS[i] - 0.828347f*xoutput[i];
+    }
+
+    /* Precompute j*S and store in youtput. */
+    tmpiter = std::copy(mSHistory.cbegin(), mSHistory.cend(), mTemp.begin());
+    std::copy_n(mS.cbegin(), SamplesToDo+sFilterDelay, tmpiter);
+    std::copy_n(mTemp.cbegin()+SamplesToDo, mSHistory.size(), mSHistory.begin());
+    PShift.process({youtput, SamplesToDo}, mTemp.data());
+
+    for(size_t i{0};i < SamplesToDo;++i)
+    {
+        /* Y = 0.762956*D + j*0.384230*S */
+        youtput[i] = 0.762956f*mD[i] + 0.384230f*youtput[i];
+    }
+
+    std::copy(mS.begin()+SamplesToDo, mS.begin()+SamplesToDo+sFilterDelay, mS.begin());
+    std::copy(mD.begin()+SamplesToDo, mD.begin()+SamplesToDo+sFilterDelay, mD.begin());
+}
+
+
+int main(int argc, char **argv)
+{
+    if(argc < 2 || std::strcmp(argv[1], "-h") == 0 || std::strcmp(argv[1], "--help") == 0)
+    {
+        printf("Usage: %s <[options] filename.wav...>\n\n"
+            "  Options:\n"
+            "    --general      Use the general equations for 2-channel UHJ (default).\n"
+            "    --alternative  Use the alternative equations for 2-channel UHJ.\n"
+            "\n"
+            "Note: When decoding 2-channel UHJ to an .amb file, the result should not use\n"
+            "the normal B-Format shelf filters! Only 3- and 4-channel UHJ can accurately\n"
+            "reconstruct the original B-Format signal.",
+            argv[0]);
+        return 1;
+    }
+
+    size_t num_files{0}, num_decoded{0};
+    bool use_general{true};
+    for(int fidx{1};fidx < argc;++fidx)
+    {
+        if(std::strcmp(argv[fidx], "--general") == 0)
+        {
+            use_general = true;
+            continue;
+        }
+        if(std::strcmp(argv[fidx], "--alternative") == 0)
+        {
+            use_general = false;
+            continue;
+        }
+        ++num_files;
+        SF_INFO ininfo{};
+        SndFilePtr infile{sf_open(argv[fidx], SFM_READ, &ininfo)};
+        if(!infile)
+        {
+            fprintf(stderr, "Failed to open %s\n", argv[fidx]);
+            continue;
+        }
+        if(sf_command(infile.get(), SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT)
+        {
+            fprintf(stderr, "%s is already B-Format\n", argv[fidx]);
+            continue;
+        }
+        uint outchans{};
+        if(ininfo.channels == 2)
+            outchans = 3;
+        else if(ininfo.channels == 3 || ininfo.channels == 4)
+            outchans = static_cast<uint>(ininfo.channels);
+        else
+        {
+            fprintf(stderr, "%s is not a 2-, 3-, or 4-channel file\n", argv[fidx]);
+            continue;
+        }
+        printf("Converting %s from %d-channel UHJ%s...\n", argv[fidx], ininfo.channels,
+            (ininfo.channels == 2) ? use_general ? " (general)" : " (alternative)" : "");
+
+        std::string outname{argv[fidx]};
+        auto lastslash = outname.find_last_of('/');
+        if(lastslash != std::string::npos)
+            outname.erase(0, lastslash+1);
+        auto lastdot = outname.find_last_of('.');
+        if(lastdot != std::string::npos)
+            outname.resize(lastdot+1);
+        outname += "amb";
+
+        FilePtr outfile{fopen(outname.c_str(), "wb")};
+        if(!outfile)
+        {
+            fprintf(stderr, "Failed to create %s\n", outname.c_str());
+            continue;
+        }
+
+        fputs("RIFF", outfile.get());
+        fwrite32le(0xFFFFFFFF, outfile.get()); // 'RIFF' header len; filled in at close
+
+        fputs("WAVE", outfile.get());
+
+        fputs("fmt ", outfile.get());
+        fwrite32le(40, outfile.get()); // 'fmt ' header len; 40 bytes for EXTENSIBLE
+
+        // 16-bit val, format type id (extensible: 0xFFFE)
+        fwrite16le(0xFFFE, outfile.get());
+        // 16-bit val, channel count
+        fwrite16le(static_cast<ushort>(outchans), outfile.get());
+        // 32-bit val, frequency
+        fwrite32le(static_cast<uint>(ininfo.samplerate), outfile.get());
+        // 32-bit val, bytes per second
+        fwrite32le(static_cast<uint>(ininfo.samplerate)*sizeof(float)*outchans, outfile.get());
+        // 16-bit val, frame size
+        fwrite16le(static_cast<ushort>(sizeof(float)*outchans), outfile.get());
+        // 16-bit val, bits per sample
+        fwrite16le(static_cast<ushort>(sizeof(float)*8), outfile.get());
+        // 16-bit val, extra byte count
+        fwrite16le(22, outfile.get());
+        // 16-bit val, valid bits per sample
+        fwrite16le(static_cast<ushort>(sizeof(float)*8), outfile.get());
+        // 32-bit val, channel mask
+        fwrite32le(0, outfile.get());
+        // 16 byte GUID, sub-type format
+        fwrite(SUBTYPE_BFORMAT_FLOAT, 1, 16, outfile.get());
+
+        fputs("data", outfile.get());
+        fwrite32le(0xFFFFFFFF, outfile.get()); // 'data' header len; filled in at close
+        if(ferror(outfile.get()))
+        {
+            fprintf(stderr, "Error writing wave file header: %s (%d)\n", strerror(errno), errno);
+            continue;
+        }
+
+        auto DataStart = ftell(outfile.get());
+
+        auto decoder = std::make_unique<UhjDecoder>();
+        auto inmem = std::make_unique<float[]>(BufferLineSize*static_cast<uint>(ininfo.channels));
+        auto decmem = al::vector<std::array<float,BufferLineSize>, 16>(outchans);
+        auto outmem = std::make_unique<byte4[]>(BufferLineSize*outchans);
+
+        /* A number of initial samples need to be skipped to cut the lead-in
+         * from the all-pass filter delay. The same number of samples need to
+         * be fed through the decoder after reaching the end of the input file
+         * to ensure none of the original input is lost.
+         */
+        size_t LeadIn{UhjDecoder::sFilterDelay};
+        sf_count_t LeadOut{UhjDecoder::sFilterDelay};
+        while(LeadOut > 0)
+        {
+            sf_count_t sgot{sf_readf_float(infile.get(), inmem.get(), BufferLineSize)};
+            sgot = std::max<sf_count_t>(sgot, 0);
+            if(sgot < BufferLineSize)
+            {
+                const sf_count_t remaining{std::min(BufferLineSize - sgot, LeadOut)};
+                std::fill_n(inmem.get() + sgot*ininfo.channels, remaining*ininfo.channels, 0.0f);
+                sgot += remaining;
+                LeadOut -= remaining;
+            }
+
+            auto got = static_cast<size_t>(sgot);
+            if(ininfo.channels > 2 || use_general)
+                decoder->decode(inmem.get(), static_cast<uint>(ininfo.channels), decmem, got);
+            else
+                decoder->decode2(inmem.get(), decmem, got);
+            if(LeadIn >= got)
+            {
+                LeadIn -= got;
+                continue;
+            }
+
+            got -= LeadIn;
+            for(size_t i{0};i < got;++i)
+            {
+                /* Attenuate by -3dB for FuMa output levels. */
+                constexpr auto inv_sqrt2 = static_cast<float>(1.0/al::numbers::sqrt2);
+                for(size_t j{0};j < outchans;++j)
+                    outmem[i*outchans + j] = f32AsLEBytes(decmem[j][LeadIn+i] * inv_sqrt2);
+            }
+            LeadIn = 0;
+
+            size_t wrote{fwrite(outmem.get(), sizeof(byte4)*outchans, got, outfile.get())};
+            if(wrote < got)
+            {
+                fprintf(stderr, "Error writing wave data: %s (%d)\n", strerror(errno), errno);
+                break;
+            }
+        }
+
+        auto DataEnd = ftell(outfile.get());
+        if(DataEnd > DataStart)
+        {
+            long dataLen{DataEnd - DataStart};
+            if(fseek(outfile.get(), 4, SEEK_SET) == 0)
+                fwrite32le(static_cast<uint>(DataEnd-8), outfile.get()); // 'WAVE' header len
+            if(fseek(outfile.get(), DataStart-4, SEEK_SET) == 0)
+                fwrite32le(static_cast<uint>(dataLen), outfile.get()); // 'data' header len
+        }
+        fflush(outfile.get());
+        ++num_decoded;
+    }
+    if(num_decoded == 0)
+        fprintf(stderr, "Failed to decode any input files\n");
+    else if(num_decoded < num_files)
+        fprintf(stderr, "Decoded %zu of %zu files\n", num_decoded, num_files);
+    else
+        printf("Decoded %zu file%s\n", num_decoded, (num_decoded==1)?"":"s");
+    return 0;
+}
diff --git a/utils/uhjencoder.cpp b/utils/uhjencoder.cpp
new file mode 100644 (file)
index 0000000..3469899
--- /dev/null
@@ -0,0 +1,531 @@
+/*
+ * 2-channel UHJ Encoder
+ *
+ * Copyright (c) Chris Robinson <chris.kcat@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <array>
+#include <cstring>
+#include <inttypes.h>
+#include <memory>
+#include <stddef.h>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "almalloc.h"
+#include "alnumbers.h"
+#include "alspan.h"
+#include "opthelpers.h"
+#include "phase_shifter.h"
+#include "vector.h"
+
+#include "sndfile.h"
+
+#include "win_main_utf8.h"
+
+
+namespace {
+
+struct SndFileDeleter {
+    void operator()(SNDFILE *sndfile) { sf_close(sndfile); }
+};
+using SndFilePtr = std::unique_ptr<SNDFILE,SndFileDeleter>;
+
+
+using uint = unsigned int;
+
+constexpr uint BufferLineSize{1024};
+
+using FloatBufferLine = std::array<float,BufferLineSize>;
+using FloatBufferSpan = al::span<float,BufferLineSize>;
+
+
+struct UhjEncoder {
+    constexpr static size_t sFilterDelay{1024};
+
+    /* Delays and processing storage for the unfiltered signal. */
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mW{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mX{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mY{};
+    alignas(16) std::array<float,BufferLineSize+sFilterDelay> mZ{};
+
+    alignas(16) std::array<float,BufferLineSize> mS{};
+    alignas(16) std::array<float,BufferLineSize> mD{};
+    alignas(16) std::array<float,BufferLineSize> mT{};
+
+    /* History for the FIR filter. */
+    alignas(16) std::array<float,sFilterDelay*2 - 1> mWXHistory1{};
+    alignas(16) std::array<float,sFilterDelay*2 - 1> mWXHistory2{};
+
+    alignas(16) std::array<float,BufferLineSize + sFilterDelay*2> mTemp{};
+
+    void encode(const al::span<FloatBufferLine> OutSamples,
+        const al::span<FloatBufferLine,4> InSamples, const size_t SamplesToDo);
+
+    DEF_NEWDEL(UhjEncoder)
+};
+
+const PhaseShifterT<UhjEncoder::sFilterDelay*2> PShift{};
+
+
+/* Encoding UHJ from B-Format is done as:
+ *
+ * S = 0.9396926*W + 0.1855740*X
+ * D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y
+ *
+ * Left = (S + D)/2.0
+ * Right = (S - D)/2.0
+ * T = j(-0.1432*W + 0.6512*X) - 0.7071068*Y
+ * Q = 0.9772*Z
+ *
+ * where j is a wide-band +90 degree phase shift. T is excluded from 2-channel
+ * output, and Q is excluded from 2- and 3-channel output.
+ */
+void UhjEncoder::encode(const al::span<FloatBufferLine> OutSamples,
+    const al::span<FloatBufferLine,4> InSamples, const size_t SamplesToDo)
+{
+    const float *RESTRICT winput{al::assume_aligned<16>(InSamples[0].data())};
+    const float *RESTRICT xinput{al::assume_aligned<16>(InSamples[1].data())};
+    const float *RESTRICT yinput{al::assume_aligned<16>(InSamples[2].data())};
+    const float *RESTRICT zinput{al::assume_aligned<16>(InSamples[3].data())};
+
+    /* Combine the previously delayed input signal with the new input. */
+    std::copy_n(winput, SamplesToDo, mW.begin()+sFilterDelay);
+    std::copy_n(xinput, SamplesToDo, mX.begin()+sFilterDelay);
+    std::copy_n(yinput, SamplesToDo, mY.begin()+sFilterDelay);
+    std::copy_n(zinput, SamplesToDo, mZ.begin()+sFilterDelay);
+
+    /* S = 0.9396926*W + 0.1855740*X */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mS[i] = 0.9396926f*mW[i] + 0.1855740f*mX[i];
+
+    /* Precompute j(-0.3420201*W + 0.5098604*X) and store in mD. */
+    auto tmpiter = std::copy(mWXHistory1.cbegin(), mWXHistory1.cend(), mTemp.begin());
+    std::transform(winput, winput+SamplesToDo, xinput, tmpiter,
+        [](const float w, const float x) noexcept -> float
+        { return -0.3420201f*w + 0.5098604f*x; });
+    std::copy_n(mTemp.cbegin()+SamplesToDo, mWXHistory1.size(), mWXHistory1.begin());
+    PShift.process({mD.data(), SamplesToDo}, mTemp.data());
+
+    /* D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y */
+    for(size_t i{0};i < SamplesToDo;++i)
+        mD[i] = mD[i] + 0.6554516f*mY[i];
+
+    /* Left = (S + D)/2.0 */
+    float *RESTRICT left{al::assume_aligned<16>(OutSamples[0].data())};
+    for(size_t i{0};i < SamplesToDo;i++)
+        left[i] = (mS[i] + mD[i]) * 0.5f;
+    /* Right = (S - D)/2.0 */
+    float *RESTRICT right{al::assume_aligned<16>(OutSamples[1].data())};
+    for(size_t i{0};i < SamplesToDo;i++)
+        right[i] = (mS[i] - mD[i]) * 0.5f;
+
+    if(OutSamples.size() > 2)
+    {
+        /* Precompute j(-0.1432*W + 0.6512*X) and store in mT. */
+        tmpiter = std::copy(mWXHistory2.cbegin(), mWXHistory2.cend(), mTemp.begin());
+        std::transform(winput, winput+SamplesToDo, xinput, tmpiter,
+            [](const float w, const float x) noexcept -> float
+            { return -0.1432f*w + 0.6512f*x; });
+        std::copy_n(mTemp.cbegin()+SamplesToDo, mWXHistory2.size(), mWXHistory2.begin());
+        PShift.process({mT.data(), SamplesToDo}, mTemp.data());
+
+        /* T = j(-0.1432*W + 0.6512*X) - 0.7071068*Y */
+        float *RESTRICT t{al::assume_aligned<16>(OutSamples[2].data())};
+        for(size_t i{0};i < SamplesToDo;i++)
+            t[i] = mT[i] - 0.7071068f*mY[i];
+    }
+    if(OutSamples.size() > 3)
+    {
+        /* Q = 0.9772*Z */
+        float *RESTRICT q{al::assume_aligned<16>(OutSamples[3].data())};
+        for(size_t i{0};i < SamplesToDo;i++)
+            q[i] = 0.9772f*mZ[i];
+    }
+
+    /* Copy the future samples to the front for next time. */
+    std::copy(mW.cbegin()+SamplesToDo, mW.cbegin()+SamplesToDo+sFilterDelay, mW.begin());
+    std::copy(mX.cbegin()+SamplesToDo, mX.cbegin()+SamplesToDo+sFilterDelay, mX.begin());
+    std::copy(mY.cbegin()+SamplesToDo, mY.cbegin()+SamplesToDo+sFilterDelay, mY.begin());
+    std::copy(mZ.cbegin()+SamplesToDo, mZ.cbegin()+SamplesToDo+sFilterDelay, mZ.begin());
+}
+
+
+struct SpeakerPos {
+    int mChannelID;
+    float mAzimuth;
+    float mElevation;
+};
+
+/* Azimuth is counter-clockwise. */
+constexpr SpeakerPos StereoMap[2]{
+    { SF_CHANNEL_MAP_LEFT,   30.0f, 0.0f },
+    { SF_CHANNEL_MAP_RIGHT, -30.0f, 0.0f },
+}, QuadMap[4]{
+    { SF_CHANNEL_MAP_LEFT,         45.0f, 0.0f },
+    { SF_CHANNEL_MAP_RIGHT,       -45.0f, 0.0f },
+    { SF_CHANNEL_MAP_REAR_LEFT,   135.0f, 0.0f },
+    { SF_CHANNEL_MAP_REAR_RIGHT, -135.0f, 0.0f },
+}, X51Map[6]{
+    { SF_CHANNEL_MAP_LEFT,         30.0f, 0.0f },
+    { SF_CHANNEL_MAP_RIGHT,       -30.0f, 0.0f },
+    { SF_CHANNEL_MAP_CENTER,        0.0f, 0.0f },
+    { SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
+    { SF_CHANNEL_MAP_SIDE_LEFT,   110.0f, 0.0f },
+    { SF_CHANNEL_MAP_SIDE_RIGHT, -110.0f, 0.0f },
+}, X51RearMap[6]{
+    { SF_CHANNEL_MAP_LEFT,         30.0f, 0.0f },
+    { SF_CHANNEL_MAP_RIGHT,       -30.0f, 0.0f },
+    { SF_CHANNEL_MAP_CENTER,        0.0f, 0.0f },
+    { SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
+    { SF_CHANNEL_MAP_REAR_LEFT,   110.0f, 0.0f },
+    { SF_CHANNEL_MAP_REAR_RIGHT, -110.0f, 0.0f },
+}, X71Map[8]{
+    { SF_CHANNEL_MAP_LEFT,         30.0f, 0.0f },
+    { SF_CHANNEL_MAP_RIGHT,       -30.0f, 0.0f },
+    { SF_CHANNEL_MAP_CENTER,        0.0f, 0.0f },
+    { SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
+    { SF_CHANNEL_MAP_REAR_LEFT,   150.0f, 0.0f },
+    { SF_CHANNEL_MAP_REAR_RIGHT, -150.0f, 0.0f },
+    { SF_CHANNEL_MAP_SIDE_LEFT,    90.0f, 0.0f },
+    { SF_CHANNEL_MAP_SIDE_RIGHT,  -90.0f, 0.0f },
+}, X714Map[12]{
+    { SF_CHANNEL_MAP_LEFT,         30.0f,  0.0f },
+    { SF_CHANNEL_MAP_RIGHT,       -30.0f,  0.0f },
+    { SF_CHANNEL_MAP_CENTER,        0.0f,  0.0f },
+    { SF_CHANNEL_MAP_LFE, 0.0f, 0.0f },
+    { SF_CHANNEL_MAP_REAR_LEFT,   150.0f,  0.0f },
+    { SF_CHANNEL_MAP_REAR_RIGHT, -150.0f,  0.0f },
+    { SF_CHANNEL_MAP_SIDE_LEFT,    90.0f,  0.0f },
+    { SF_CHANNEL_MAP_SIDE_RIGHT,  -90.0f,  0.0f },
+    { SF_CHANNEL_MAP_TOP_FRONT_LEFT,    45.0f, 35.0f },
+    { SF_CHANNEL_MAP_TOP_FRONT_RIGHT,  -45.0f, 35.0f },
+    { SF_CHANNEL_MAP_TOP_REAR_LEFT,    135.0f, 35.0f },
+    { SF_CHANNEL_MAP_TOP_REAR_RIGHT,  -135.0f, 35.0f },
+};
+
+constexpr auto GenCoeffs(double x /*+front*/, double y /*+left*/, double z /*+up*/) noexcept
+{
+    /* Coefficients are +3dB of FuMa. */
+    return std::array<float,4>{{
+        1.0f,
+        static_cast<float>(al::numbers::sqrt2 * x),
+        static_cast<float>(al::numbers::sqrt2 * y),
+        static_cast<float>(al::numbers::sqrt2 * z)
+    }};
+}
+
+} // namespace
+
+
+int main(int argc, char **argv)
+{
+    if(argc < 2 || std::strcmp(argv[1], "-h") == 0 || std::strcmp(argv[1], "--help") == 0)
+    {
+        printf("Usage: %s <infile...>\n\n", argv[0]);
+        return 1;
+    }
+
+    uint uhjchans{2};
+    size_t num_files{0}, num_encoded{0};
+    for(int fidx{1};fidx < argc;++fidx)
+    {
+        if(strcmp(argv[fidx], "-bhj") == 0)
+        {
+            uhjchans = 2;
+            continue;
+        }
+        if(strcmp(argv[fidx], "-thj") == 0)
+        {
+            uhjchans = 3;
+            continue;
+        }
+        if(strcmp(argv[fidx], "-phj") == 0)
+        {
+            uhjchans = 4;
+            continue;
+        }
+        ++num_files;
+
+        std::string outname{argv[fidx]};
+        size_t lastslash{outname.find_last_of('/')};
+        if(lastslash != std::string::npos)
+            outname.erase(0, lastslash+1);
+        size_t extpos{outname.find_last_of('.')};
+        if(extpos != std::string::npos)
+            outname.resize(extpos);
+        outname += ".uhj.flac";
+
+        SF_INFO ininfo{};
+        SndFilePtr infile{sf_open(argv[fidx], SFM_READ, &ininfo)};
+        if(!infile)
+        {
+            fprintf(stderr, "Failed to open %s\n", argv[fidx]);
+            continue;
+        }
+        printf("Converting %s to %s...\n", argv[fidx], outname.c_str());
+
+        /* Work out the channel map, preferably using the actual channel map
+         * from the file/format, but falling back to assuming WFX order.
+         */
+        al::span<const SpeakerPos> spkrs;
+        auto chanmap = std::vector<int>(static_cast<uint>(ininfo.channels), SF_CHANNEL_MAP_INVALID);
+        if(sf_command(infile.get(), SFC_GET_CHANNEL_MAP_INFO, chanmap.data(),
+            ininfo.channels*int{sizeof(int)}) == SF_TRUE)
+        {
+            static const std::array<int,2> stereomap{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT}};
+            static const std::array<int,4> quadmap{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
+                SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT}};
+            static const std::array<int,6> x51map{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
+                SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
+                SF_CHANNEL_MAP_SIDE_LEFT, SF_CHANNEL_MAP_SIDE_RIGHT}};
+            static const std::array<int,6> x51rearmap{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
+                SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
+                SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT}};
+            static const std::array<int,8> x71map{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
+                SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
+                SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT,
+                SF_CHANNEL_MAP_SIDE_LEFT, SF_CHANNEL_MAP_SIDE_RIGHT}};
+            static const std::array<int,12> x714map{{SF_CHANNEL_MAP_LEFT, SF_CHANNEL_MAP_RIGHT,
+                SF_CHANNEL_MAP_CENTER, SF_CHANNEL_MAP_LFE,
+                SF_CHANNEL_MAP_REAR_LEFT, SF_CHANNEL_MAP_REAR_RIGHT,
+                SF_CHANNEL_MAP_SIDE_LEFT, SF_CHANNEL_MAP_SIDE_RIGHT,
+                SF_CHANNEL_MAP_TOP_FRONT_LEFT, SF_CHANNEL_MAP_TOP_FRONT_RIGHT,
+                SF_CHANNEL_MAP_TOP_REAR_LEFT, SF_CHANNEL_MAP_TOP_REAR_RIGHT}};
+            static const std::array<int,3> ambi2dmap{{SF_CHANNEL_MAP_AMBISONIC_B_W,
+                SF_CHANNEL_MAP_AMBISONIC_B_X, SF_CHANNEL_MAP_AMBISONIC_B_Y}};
+            static const std::array<int,4> ambi3dmap{{SF_CHANNEL_MAP_AMBISONIC_B_W,
+                SF_CHANNEL_MAP_AMBISONIC_B_X, SF_CHANNEL_MAP_AMBISONIC_B_Y,
+                SF_CHANNEL_MAP_AMBISONIC_B_Z}};
+
+            auto match_chanmap = [](const al::span<int> a, const al::span<const int> b) -> bool
+            {
+                if(a.size() != b.size())
+                    return false;
+                for(const int id : a)
+                {
+                    if(std::find(b.begin(), b.end(), id) != b.end())
+                        return false;
+                }
+                return true;
+            };
+            if(match_chanmap(chanmap, stereomap))
+                spkrs = StereoMap;
+            else if(match_chanmap(chanmap, quadmap))
+                spkrs = QuadMap;
+            else if(match_chanmap(chanmap, x51map))
+                spkrs = X51Map;
+            else if(match_chanmap(chanmap, x51rearmap))
+                spkrs = X51RearMap;
+            else if(match_chanmap(chanmap, x71map))
+                spkrs = X71Map;
+            else if(match_chanmap(chanmap, x714map))
+                spkrs = X714Map;
+            else if(match_chanmap(chanmap, ambi2dmap) || match_chanmap(chanmap, ambi3dmap))
+            {
+                /* Do nothing. */
+            }
+            else
+            {
+                std::string mapstr;
+                if(!chanmap.empty())
+                {
+                    mapstr = std::to_string(chanmap[0]);
+                    for(int idx : al::span<int>{chanmap}.subspan<1>())
+                    {
+                        mapstr += ',';
+                        mapstr += std::to_string(idx);
+                    }
+                }
+                fprintf(stderr, " ... %zu channels not supported (map: %s)\n", chanmap.size(),
+                    mapstr.c_str());
+                continue;
+            }
+        }
+        else if(ininfo.channels == 2)
+        {
+            fprintf(stderr, " ... assuming WFX order stereo\n");
+            spkrs = StereoMap;
+            chanmap[0] = SF_CHANNEL_MAP_FRONT_LEFT;
+            chanmap[1] = SF_CHANNEL_MAP_FRONT_RIGHT;
+        }
+        else if(ininfo.channels == 6)
+        {
+            fprintf(stderr, " ... assuming WFX order 5.1\n");
+            spkrs = X51Map;
+            chanmap[0] = SF_CHANNEL_MAP_FRONT_LEFT;
+            chanmap[1] = SF_CHANNEL_MAP_FRONT_RIGHT;
+            chanmap[2] = SF_CHANNEL_MAP_FRONT_CENTER;
+            chanmap[3] = SF_CHANNEL_MAP_LFE;
+            chanmap[4] = SF_CHANNEL_MAP_SIDE_LEFT;
+            chanmap[5] = SF_CHANNEL_MAP_SIDE_RIGHT;
+        }
+        else if(ininfo.channels == 8)
+        {
+            fprintf(stderr, " ... assuming WFX order 7.1\n");
+            spkrs = X71Map;
+            chanmap[0] = SF_CHANNEL_MAP_FRONT_LEFT;
+            chanmap[1] = SF_CHANNEL_MAP_FRONT_RIGHT;
+            chanmap[2] = SF_CHANNEL_MAP_FRONT_CENTER;
+            chanmap[3] = SF_CHANNEL_MAP_LFE;
+            chanmap[4] = SF_CHANNEL_MAP_REAR_LEFT;
+            chanmap[5] = SF_CHANNEL_MAP_REAR_RIGHT;
+            chanmap[6] = SF_CHANNEL_MAP_SIDE_LEFT;
+            chanmap[7] = SF_CHANNEL_MAP_SIDE_RIGHT;
+        }
+        else
+        {
+            fprintf(stderr, " ... unmapped %d-channel audio not supported\n", ininfo.channels);
+            continue;
+        }
+
+        SF_INFO outinfo{};
+        outinfo.frames = ininfo.frames;
+        outinfo.samplerate = ininfo.samplerate;
+        outinfo.channels = static_cast<int>(uhjchans);
+        outinfo.format = SF_FORMAT_PCM_24 | SF_FORMAT_FLAC;
+        SndFilePtr outfile{sf_open(outname.c_str(), SFM_WRITE, &outinfo)};
+        if(!outfile)
+        {
+            fprintf(stderr, " ... failed to create %s\n", outname.c_str());
+            continue;
+        }
+
+        auto encoder = std::make_unique<UhjEncoder>();
+        auto splbuf = al::vector<FloatBufferLine, 16>(static_cast<uint>(9+ininfo.channels)+uhjchans);
+        auto ambmem = al::span<FloatBufferLine,4>{splbuf.data(), 4};
+        auto encmem = al::span<FloatBufferLine,4>{&splbuf[4], 4};
+        auto srcmem = al::span<float,BufferLineSize>{splbuf[8].data(), BufferLineSize};
+        auto outmem = al::span<float>{splbuf[9].data(), BufferLineSize*uhjchans};
+
+        /* A number of initial samples need to be skipped to cut the lead-in
+         * from the all-pass filter delay. The same number of samples need to
+         * be fed through the encoder after reaching the end of the input file
+         * to ensure none of the original input is lost.
+         */
+        size_t total_wrote{0};
+        size_t LeadIn{UhjEncoder::sFilterDelay};
+        sf_count_t LeadOut{UhjEncoder::sFilterDelay};
+        while(LeadIn > 0 || LeadOut > 0)
+        {
+            auto inmem = outmem.data() + outmem.size();
+            auto sgot = sf_readf_float(infile.get(), inmem, BufferLineSize);
+
+            sgot = std::max<sf_count_t>(sgot, 0);
+            if(sgot < BufferLineSize)
+            {
+                const sf_count_t remaining{std::min(BufferLineSize - sgot, LeadOut)};
+                std::fill_n(inmem + sgot*ininfo.channels, remaining*ininfo.channels, 0.0f);
+                sgot += remaining;
+                LeadOut -= remaining;
+            }
+
+            for(auto&& buf : ambmem)
+                buf.fill(0.0f);
+
+            auto got = static_cast<size_t>(sgot);
+            if(spkrs.empty())
+            {
+                /* B-Format is already in the correct order. It just needs a
+                 * +3dB boost.
+                 */
+                static constexpr float scale{al::numbers::sqrt2_v<float>};
+                const size_t chans{std::min<size_t>(static_cast<uint>(ininfo.channels), 4u)};
+                for(size_t c{0};c < chans;++c)
+                {
+                    for(size_t i{0};i < got;++i)
+                        ambmem[c][i] = inmem[i*static_cast<uint>(ininfo.channels)] * scale;
+                    ++inmem;
+                }
+            }
+            else for(const int chanid : chanmap)
+            {
+                /* Skip LFE. Or mix directly into W? Or W+X? */
+                if(chanid == SF_CHANNEL_MAP_LFE)
+                {
+                    ++inmem;
+                    continue;
+                }
+
+                const auto spkr = std::find_if(spkrs.cbegin(), spkrs.cend(),
+                    [chanid](const SpeakerPos &pos){return pos.mChannelID == chanid;});
+                if(spkr == spkrs.cend())
+                {
+                    fprintf(stderr, " ... failed to find channel ID %d\n", chanid);
+                    continue;
+                }
+
+                for(size_t i{0};i < got;++i)
+                    srcmem[i] = inmem[i * static_cast<uint>(ininfo.channels)];
+                ++inmem;
+
+                static constexpr auto Deg2Rad = al::numbers::pi / 180.0;
+                const auto coeffs = GenCoeffs(
+                    std::cos(spkr->mAzimuth*Deg2Rad) * std::cos(spkr->mElevation*Deg2Rad),
+                    std::sin(spkr->mAzimuth*Deg2Rad) * std::cos(spkr->mElevation*Deg2Rad),
+                    std::sin(spkr->mElevation*Deg2Rad));
+                for(size_t c{0};c < 4;++c)
+                {
+                    for(size_t i{0};i < got;++i)
+                        ambmem[c][i] += srcmem[i] * coeffs[c];
+                }
+            }
+
+            encoder->encode(encmem.subspan(0, uhjchans), ambmem, got);
+            if(LeadIn >= got)
+            {
+                LeadIn -= got;
+                continue;
+            }
+
+            got -= LeadIn;
+            for(size_t c{0};c < uhjchans;++c)
+            {
+                constexpr float max_val{8388607.0f / 8388608.0f};
+                auto clamp = [](float v, float mn, float mx) noexcept
+                { return std::min(std::max(v, mn), mx); };
+                for(size_t i{0};i < got;++i)
+                    outmem[i*uhjchans + c] = clamp(encmem[c][LeadIn+i], -1.0f, max_val);
+            }
+            LeadIn = 0;
+
+            sf_count_t wrote{sf_writef_float(outfile.get(), outmem.data(),
+                static_cast<sf_count_t>(got))};
+            if(wrote < 0)
+                fprintf(stderr, " ... failed to write samples: %d\n", sf_error(outfile.get()));
+            else
+                total_wrote += static_cast<size_t>(wrote);
+        }
+        printf(" ... wrote %zu samples (%" PRId64 ").\n", total_wrote, int64_t{ininfo.frames});
+        ++num_encoded;
+    }
+    if(num_encoded == 0)
+        fprintf(stderr, "Failed to encode any input files\n");
+    else if(num_encoded < num_files)
+        fprintf(stderr, "Encoded %zu of %zu files\n", num_encoded, num_files);
+    else
+        printf("Encoded %s%zu file%s\n", (num_encoded > 1) ? "all " : "", num_encoded,
+            (num_encoded == 1) ? "" : "s");
+    return 0;
+}
diff --git a/version.cmake b/version.cmake
new file mode 100644 (file)
index 0000000..af7ff0a
--- /dev/null
@@ -0,0 +1,11 @@
+EXECUTE_PROCESS(
+    COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
+    OUTPUT_VARIABLE GIT_BRANCH
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+)
+EXECUTE_PROCESS(
+    COMMAND ${GIT_EXECUTABLE} log -1 --format=%h
+    OUTPUT_VARIABLE GIT_COMMIT_HASH
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+)
+CONFIGURE_FILE(${SRC} ${DST})
diff --git a/version.h.in b/version.h.in
new file mode 100644 (file)
index 0000000..9bb439d
--- /dev/null
@@ -0,0 +1,9 @@
+/* Define to the library version */
+#define ALSOFT_VERSION "${LIB_VERSION}"
+#define ALSOFT_VERSION_NUM ${LIB_VERSION_NUM}
+
+/* Define the branch being built */
+#define ALSOFT_GIT_BRANCH "${GIT_BRANCH}"
+
+/* Define the hash of the head commit */
+#define ALSOFT_GIT_COMMIT_HASH "${GIT_COMMIT_HASH}"