aboutsummaryrefslogtreecommitdiffstats
path: root/alc/backends
diff options
context:
space:
mode:
Diffstat (limited to 'alc/backends')
-rw-r--r--alc/backends/alsa.cpp1288
-rw-r--r--alc/backends/alsa.h19
-rw-r--r--alc/backends/base.cpp58
-rw-r--r--alc/backends/base.h78
-rw-r--r--alc/backends/coreaudio.cpp709
-rw-r--r--alc/backends/coreaudio.h19
-rw-r--r--alc/backends/dsound.cpp938
-rw-r--r--alc/backends/dsound.h19
-rw-r--r--alc/backends/jack.cpp562
-rw-r--r--alc/backends/jack.h19
-rw-r--r--alc/backends/loopback.cpp80
-rw-r--r--alc/backends/loopback.h19
-rw-r--r--alc/backends/null.cpp184
-rw-r--r--alc/backends/null.h19
-rw-r--r--alc/backends/opensl.cpp936
-rw-r--r--alc/backends/opensl.h19
-rw-r--r--alc/backends/oss.cpp751
-rw-r--r--alc/backends/oss.h19
-rw-r--r--alc/backends/portaudio.cpp463
-rw-r--r--alc/backends/portaudio.h19
-rw-r--r--alc/backends/pulseaudio.cpp1532
-rw-r--r--alc/backends/pulseaudio.h19
-rw-r--r--alc/backends/qsa.cpp953
-rw-r--r--alc/backends/qsa.h19
-rw-r--r--alc/backends/sdl2.cpp227
-rw-r--r--alc/backends/sdl2.h19
-rw-r--r--alc/backends/sndio.cpp495
-rw-r--r--alc/backends/sndio.h19
-rw-r--r--alc/backends/solaris.cpp302
-rw-r--r--alc/backends/solaris.h19
-rw-r--r--alc/backends/wasapi.cpp1763
-rw-r--r--alc/backends/wasapi.h19
-rw-r--r--alc/backends/wave.cpp402
-rw-r--r--alc/backends/wave.h19
-rw-r--r--alc/backends/winmm.cpp640
-rw-r--r--alc/backends/winmm.h19
36 files changed, 12684 insertions, 0 deletions
diff --git a/alc/backends/alsa.cpp b/alc/backends/alsa.cpp
new file mode 100644
index 00000000..c133df68
--- /dev/null
+++ b/alc/backends/alsa.cpp
@@ -0,0 +1,1288 @@
+/**
+ * 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 "backends/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 "AL/al.h"
+
+#include "albyte.h"
+#include "alcmain.h"
+#include "alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alu.h"
+#include "compat.h"
+#include "logging.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "vector.h"
+
+#include <alsa/asoundlib.h>
+
+
+namespace {
+
+constexpr ALCchar 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_set_access); \
+ MAGIC(snd_pcm_hw_params_set_format); \
+ MAGIC(snd_pcm_hw_params_set_channels); \
+ MAGIC(snd_pcm_hw_params_set_periods_near); \
+ MAGIC(snd_pcm_hw_params_set_rate_near); \
+ MAGIC(snd_pcm_hw_params_set_rate); \
+ MAGIC(snd_pcm_hw_params_set_rate_resample); \
+ MAGIC(snd_pcm_hw_params_set_buffer_time_near); \
+ MAGIC(snd_pcm_hw_params_set_period_time_near); \
+ MAGIC(snd_pcm_hw_params_set_buffer_size_near); \
+ MAGIC(snd_pcm_hw_params_set_period_size_near); \
+ MAGIC(snd_pcm_hw_params_set_buffer_size_min); \
+ MAGIC(snd_pcm_hw_params_get_buffer_time_min); \
+ MAGIC(snd_pcm_hw_params_get_buffer_time_max); \
+ MAGIC(snd_pcm_hw_params_get_period_time_min); \
+ MAGIC(snd_pcm_hw_params_get_period_time_max); \
+ MAGIC(snd_pcm_hw_params_get_buffer_size); \
+ MAGIC(snd_pcm_hw_params_get_period_size); \
+ MAGIC(snd_pcm_hw_params_get_access); \
+ MAGIC(snd_pcm_hw_params_get_periods); \
+ MAGIC(snd_pcm_hw_params_test_format); \
+ MAGIC(snd_pcm_hw_params_test_channels); \
+ MAGIC(snd_pcm_hw_params); \
+ MAGIC(snd_pcm_sw_params_malloc); \
+ MAGIC(snd_pcm_sw_params_current); \
+ MAGIC(snd_pcm_sw_params_set_avail_min); \
+ MAGIC(snd_pcm_sw_params_set_stop_threshold); \
+ MAGIC(snd_pcm_sw_params); \
+ MAGIC(snd_pcm_sw_params_free); \
+ MAGIC(snd_pcm_prepare); \
+ MAGIC(snd_pcm_start); \
+ MAGIC(snd_pcm_resume); \
+ MAGIC(snd_pcm_reset); \
+ MAGIC(snd_pcm_wait); \
+ MAGIC(snd_pcm_delay); \
+ MAGIC(snd_pcm_state); \
+ MAGIC(snd_pcm_avail_update); \
+ MAGIC(snd_pcm_areas_silence); \
+ MAGIC(snd_pcm_mmap_begin); \
+ MAGIC(snd_pcm_mmap_commit); \
+ MAGIC(snd_pcm_readi); \
+ 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)
+
+static 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_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_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_areas_silence psnd_pcm_areas_silence
+#define snd_pcm_mmap_begin psnd_pcm_mmap_begin
+#define snd_pcm_mmap_commit psnd_pcm_mmap_commit
+#define snd_pcm_readi psnd_pcm_readi
+#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 DevMap {
+ std::string name;
+ std::string device_name;
+};
+
+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);
+
+ devlist.emplace_back(DevMap{alsaDevice,
+ GetConfigValue(nullptr, "alsa", (stream==SND_PCM_STREAM_PLAYBACK) ? "device" : "capture",
+ "default")});
+
+ if(stream == SND_PCM_STREAM_PLAYBACK)
+ {
+ const char *customdevs;
+ const char *next{GetConfigValue(nullptr, "alsa", "custom-devices", "")};
+ while((customdevs=next) != nullptr && customdevs[0])
+ {
+ next = strchr(customdevs, ';');
+ const char *sep{strchr(customdevs, '=')};
+ if(!sep)
+ {
+ std::string spec{next ? std::string(customdevs, next++) : std::string(customdevs)};
+ ERR("Invalid ALSA device specification \"%s\"\n", spec.c_str());
+ continue;
+ }
+
+ const char *oldsep{sep++};
+ devlist.emplace_back(DevMap{std::string(customdevs, oldsep),
+ next ? std::string(sep, next++) : std::string(sep)});
+ const auto &entry = devlist.back();
+ TRACE("Got device \"%s\", \"%s\"\n", entry.name.c_str(), entry.device_name.c_str());
+ }
+ }
+
+ 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(1)
+ {
+ 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, 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(DevMap{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(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~AlsaPlayback() override;
+
+ int mixerProc();
+ int mixerNoMMapProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ ClockLatency getClockLatency() override;
+
+ snd_pcm_t *mPcmHandle{nullptr};
+
+ al::vector<char> 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 num_updates{mDevice->BufferSize / update_size};
+ while(!mKillNow.load(std::memory_order_acquire))
+ {
+ int state{verify_state(mPcmHandle)};
+ if(state < 0)
+ {
+ ERR("Invalid state detected: %s\n", snd_strerror(state));
+ aluHandleDisconnect(mDevice, "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(avail));
+ continue;
+ }
+
+ if(static_cast<snd_pcm_uframes_t>(avail) > update_size*(num_updates+1))
+ {
+ WARN("available samples exceeds the buffer size\n");
+ snd_pcm_reset(mPcmHandle);
+ continue;
+ }
+
+ // make sure there's frames to process
+ 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;
+ }
+ avail -= avail%update_size;
+
+ // it is possible that contiguous areas are smaller, thus we use a loop
+ lock();
+ while(avail > 0)
+ {
+ snd_pcm_uframes_t frames{static_cast<snd_pcm_uframes_t>(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)};
+ aluMixData(mDevice, WritePtr, frames);
+
+ snd_pcm_sframes_t commitres{snd_pcm_mmap_commit(mPcmHandle, offset, frames)};
+ if(commitres < 0 || (commitres-frames) != 0)
+ {
+ ERR("mmap commit error: %s\n",
+ snd_strerror(commitres >= 0 ? -EPIPE : commitres));
+ break;
+ }
+
+ avail -= frames;
+ }
+ unlock();
+ }
+
+ 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));
+ aluHandleDisconnect(mDevice, "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(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;
+ }
+
+ lock();
+ char *WritePtr{mBuffer.data()};
+ avail = snd_pcm_bytes_to_frames(mPcmHandle, mBuffer.size());
+ aluMixData(mDevice, WritePtr, avail);
+ while(avail > 0)
+ {
+ snd_pcm_sframes_t ret{snd_pcm_writei(mPcmHandle, WritePtr, avail)};
+ switch(ret)
+ {
+ case -EAGAIN:
+ continue;
+#if ESTRPIPE != EPIPE
+ case -ESTRPIPE:
+#endif
+ case -EPIPE:
+ case -EINTR:
+ ret = snd_pcm_recover(mPcmHandle, 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;
+ }
+ }
+ unlock();
+ }
+
+ return 0;
+}
+
+
+ALCenum AlsaPlayback::open(const ALCchar *name)
+{
+ const char *driver{};
+ 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())
+ return ALC_INVALID_VALUE;
+ driver = iter->device_name.c_str();
+ }
+ else
+ {
+ name = alsaDevice;
+ driver = GetConfigValue(nullptr, "alsa", "device", "default");
+ }
+
+ TRACE("Opening device \"%s\"\n", driver);
+ int err{snd_pcm_open(&mPcmHandle, driver, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)};
+ if(err < 0)
+ {
+ ERR("Could not open playback device '%s': %s\n", driver, snd_strerror(err));
+ return ALC_OUT_OF_MEMORY;
+ }
+
+ /* Free alsa's global config tree. Otherwise valgrind reports a ton of leaks. */
+ snd_config_update_free_global();
+
+ mDevice->DeviceName = name;
+
+ return ALC_NO_ERROR;
+}
+
+ALCboolean 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", 1)};
+ ALuint periodLen{static_cast<ALuint>(mDevice->UpdateSize * 1000000_u64 / mDevice->Frequency)};
+ ALuint bufferLen{static_cast<ALuint>(mDevice->BufferSize * 1000000_u64 / mDevice->Frequency)};
+ ALuint rate{mDevice->Frequency};
+
+ snd_pcm_uframes_t periodSizeInFrames{};
+ snd_pcm_uframes_t bufferSizeInFrames{};
+ snd_pcm_sw_params_t *sp{};
+ snd_pcm_hw_params_t *hp{};
+ snd_pcm_access_t access{};
+ const char *funcerr{};
+ int err{};
+
+ snd_pcm_hw_params_malloc(&hp);
+#define CHECK(x) if((funcerr=#x),(err=(x)) < 0) goto error
+ CHECK(snd_pcm_hw_params_any(mPcmHandle, hp));
+ /* set interleaved access */
+ if(!allowmmap || snd_pcm_hw_params_set_access(mPcmHandle, hp, SND_PCM_ACCESS_MMAP_INTERLEAVED) < 0)
+ {
+ /* No mmap */
+ CHECK(snd_pcm_hw_params_set_access(mPcmHandle, hp, SND_PCM_ACCESS_RW_INTERLEAVED));
+ }
+ /* test and set format (implicitly sets sample bits) */
+ if(snd_pcm_hw_params_test_format(mPcmHandle, hp, 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, format) >= 0)
+ {
+ mDevice->FmtType = fmt.fmttype;
+ break;
+ }
+ }
+ }
+ CHECK(snd_pcm_hw_params_set_format(mPcmHandle, hp, format));
+ /* test and set channels (implicitly sets frame bits) */
+ if(snd_pcm_hw_params_test_channels(mPcmHandle, hp, mDevice->channelsFromFmt()) < 0)
+ {
+ static const DevFmtChannels channellist[] = {
+ DevFmtStereo,
+ DevFmtQuad,
+ DevFmtX51,
+ DevFmtX71,
+ DevFmtMono,
+ };
+
+ for(const auto &chan : channellist)
+ {
+ if(snd_pcm_hw_params_test_channels(mPcmHandle, hp, ChannelsFromDevFmt(chan, 0)) >= 0)
+ {
+ mDevice->FmtChans = chan;
+ mDevice->mAmbiOrder = 0;
+ break;
+ }
+ }
+ }
+ CHECK(snd_pcm_hw_params_set_channels(mPcmHandle, hp, mDevice->channelsFromFmt()));
+ /* set rate (implicitly constrains period/buffer parameters) */
+ if(!GetConfigValueBool(mDevice->DeviceName.c_str(), "alsa", "allow-resampler", 0) ||
+ !mDevice->Flags.get<FrequencyRequest>())
+ {
+ if(snd_pcm_hw_params_set_rate_resample(mPcmHandle, hp, 0) < 0)
+ ERR("Failed to disable ALSA resampler\n");
+ }
+ else if(snd_pcm_hw_params_set_rate_resample(mPcmHandle, hp, 1) < 0)
+ ERR("Failed to enable ALSA resampler\n");
+ CHECK(snd_pcm_hw_params_set_rate_near(mPcmHandle, hp, &rate, nullptr));
+ /* set period time (implicitly constrains period/buffer parameters) */
+ if((err=snd_pcm_hw_params_set_period_time_near(mPcmHandle, hp, &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, &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));
+
+ /* retrieve configuration info */
+ CHECK(snd_pcm_hw_params_get_access(hp, &access));
+ CHECK(snd_pcm_hw_params_get_period_size(hp, &periodSizeInFrames, nullptr));
+ CHECK(snd_pcm_hw_params_get_buffer_size(hp, &bufferSizeInFrames));
+ snd_pcm_hw_params_free(hp);
+ hp = nullptr;
+
+ snd_pcm_sw_params_malloc(&sp);
+ CHECK(snd_pcm_sw_params_current(mPcmHandle, sp));
+ CHECK(snd_pcm_sw_params_set_avail_min(mPcmHandle, sp, periodSizeInFrames));
+ CHECK(snd_pcm_sw_params_set_stop_threshold(mPcmHandle, sp, bufferSizeInFrames));
+ CHECK(snd_pcm_sw_params(mPcmHandle, sp));
+#undef CHECK
+ snd_pcm_sw_params_free(sp);
+ sp = nullptr;
+
+ mDevice->BufferSize = bufferSizeInFrames;
+ mDevice->UpdateSize = periodSizeInFrames;
+ mDevice->Frequency = rate;
+
+ SetDefaultChannelOrder(mDevice);
+
+ return ALC_TRUE;
+
+error:
+ ERR("%s failed: %s\n", funcerr, snd_strerror(err));
+ if(hp) snd_pcm_hw_params_free(hp);
+ if(sp) snd_pcm_sw_params_free(sp);
+ return ALC_FALSE;
+}
+
+ALCboolean AlsaPlayback::start()
+{
+ snd_pcm_hw_params_t *hp{};
+ snd_pcm_access_t access;
+ const char *funcerr;
+ int err;
+
+ snd_pcm_hw_params_malloc(&hp);
+#define CHECK(x) if((funcerr=#x),(err=(x)) < 0) goto error
+ CHECK(snd_pcm_hw_params_current(mPcmHandle, hp));
+ /* retrieve configuration info */
+ CHECK(snd_pcm_hw_params_get_access(hp, &access));
+#undef CHECK
+ if(0)
+ {
+ error:
+ ERR("%s failed: %s\n", funcerr, snd_strerror(err));
+ if(hp) snd_pcm_hw_params_free(hp);
+ return ALC_FALSE;
+ }
+ snd_pcm_hw_params_free(hp);
+ hp = nullptr;
+
+ int (AlsaPlayback::*thread_func)(){};
+ if(access == SND_PCM_ACCESS_RW_INTERLEAVED)
+ {
+ mBuffer.resize(snd_pcm_frames_to_bytes(mPcmHandle, mDevice->UpdateSize));
+ thread_func = &AlsaPlayback::mixerNoMMapProc;
+ }
+ else
+ {
+ err = snd_pcm_prepare(mPcmHandle);
+ if(err < 0)
+ {
+ ERR("snd_pcm_prepare(data->mPcmHandle) failed: %s\n", snd_strerror(err));
+ return ALC_FALSE;
+ }
+ thread_func = &AlsaPlayback::mixerProc;
+ }
+
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(thread_func), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ mBuffer.clear();
+ return ALC_FALSE;
+}
+
+void AlsaPlayback::stop()
+{
+ if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+ return;
+ mThread.join();
+
+ mBuffer.clear();
+}
+
+ClockLatency AlsaPlayback::getClockLatency()
+{
+ ClockLatency ret;
+
+ lock();
+ 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;
+ unlock();
+
+ return ret;
+}
+
+
+struct AlsaCapture final : public BackendBase {
+ AlsaCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~AlsaCapture() override;
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(ALCvoid *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+ ClockLatency getClockLatency() override;
+
+ snd_pcm_t *mPcmHandle{nullptr};
+
+ al::vector<char> 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;
+}
+
+
+ALCenum AlsaCapture::open(const ALCchar *name)
+{
+ const char *driver{};
+ 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())
+ return ALC_INVALID_VALUE;
+ driver = iter->device_name.c_str();
+ }
+ else
+ {
+ name = alsaDevice;
+ driver = GetConfigValue(nullptr, "alsa", "capture", "default");
+ }
+
+ TRACE("Opening device \"%s\"\n", driver);
+ int err{snd_pcm_open(&mPcmHandle, driver, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)};
+ if(err < 0)
+ {
+ ERR("Could not open capture device '%s': %s\n", driver, snd_strerror(err));
+ return ALC_INVALID_VALUE;
+ }
+
+ /* 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(bufferSizeInFrames, 25*mDevice->Frequency/1000)};
+
+ bool needring{false};
+ const char *funcerr{};
+ snd_pcm_hw_params_t *hp{};
+ snd_pcm_hw_params_malloc(&hp);
+#define CHECK(x) if((funcerr=#x),(err=(x)) < 0) goto error
+ CHECK(snd_pcm_hw_params_any(mPcmHandle, hp));
+ /* set interleaved access */
+ CHECK(snd_pcm_hw_params_set_access(mPcmHandle, hp, SND_PCM_ACCESS_RW_INTERLEAVED));
+ /* set format (implicitly sets sample bits) */
+ CHECK(snd_pcm_hw_params_set_format(mPcmHandle, hp, format));
+ /* set channels (implicitly sets frame bits) */
+ CHECK(snd_pcm_hw_params_set_channels(mPcmHandle, hp, mDevice->channelsFromFmt()));
+ /* set rate (implicitly constrains period/buffer parameters) */
+ CHECK(snd_pcm_hw_params_set_rate(mPcmHandle, hp, 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, &bufferSizeInFrames) < 0)
+ {
+ TRACE("Buffer too large, using intermediate ring buffer\n");
+ needring = true;
+ CHECK(snd_pcm_hw_params_set_buffer_size_near(mPcmHandle, hp, &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, &periodSizeInFrames, nullptr));
+ /* install and prepare hardware configuration */
+ CHECK(snd_pcm_hw_params(mPcmHandle, hp));
+ /* retrieve configuration info */
+ CHECK(snd_pcm_hw_params_get_period_size(hp, &periodSizeInFrames, nullptr));
+#undef CHECK
+ snd_pcm_hw_params_free(hp);
+ hp = nullptr;
+
+ if(needring)
+ {
+ mRing = CreateRingBuffer(mDevice->BufferSize, mDevice->frameSizeFromFmt(), false);
+ if(!mRing)
+ {
+ ERR("ring buffer create failed\n");
+ goto error2;
+ }
+ }
+
+ mDevice->DeviceName = name;
+
+ return ALC_NO_ERROR;
+
+error:
+ ERR("%s failed: %s\n", funcerr, snd_strerror(err));
+ if(hp) snd_pcm_hw_params_free(hp);
+
+error2:
+ mRing = nullptr;
+ snd_pcm_close(mPcmHandle);
+ mPcmHandle = nullptr;
+
+ return ALC_INVALID_VALUE;
+}
+
+
+ALCboolean AlsaCapture::start()
+{
+ int err{snd_pcm_prepare(mPcmHandle)};
+ if(err < 0)
+ ERR("prepare failed: %s\n", snd_strerror(err));
+ else
+ {
+ err = snd_pcm_start(mPcmHandle);
+ if(err < 0)
+ ERR("start failed: %s\n", snd_strerror(err));
+ }
+ if(err < 0)
+ {
+ aluHandleDisconnect(mDevice, "Capture state failure: %s", snd_strerror(err));
+ return ALC_FALSE;
+ }
+
+ mDoCapture = true;
+ return ALC_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.
+ */
+ ALCuint avail{availableSamples()};
+ if(!mRing && avail > 0)
+ {
+ /* The ring buffer implicitly captures when checking availability.
+ * Direct access needs to explicitly capture it into temp storage. */
+ al::vector<char> temp(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;
+}
+
+ALCenum AlsaCapture::captureSamples(ALCvoid *buffer, ALCuint samples)
+{
+ if(mRing)
+ {
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+ }
+
+ 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, mBuffer.size());
+ if(static_cast<snd_pcm_uframes_t>(amt) > samples) amt = samples;
+
+ amt = snd_pcm_frames_to_bytes(mPcmHandle, amt);
+ memcpy(buffer, mBuffer.data(), amt);
+
+ 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(amt));
+
+ if(amt == -EAGAIN)
+ continue;
+ if((amt=snd_pcm_recover(mPcmHandle, amt, 1)) >= 0)
+ {
+ amt = snd_pcm_start(mPcmHandle);
+ if(amt >= 0)
+ amt = snd_pcm_avail_update(mPcmHandle);
+ }
+ if(amt < 0)
+ {
+ ERR("restore error: %s\n", snd_strerror(amt));
+ aluHandleDisconnect(mDevice, "Capture recovery failure: %s", snd_strerror(amt));
+ 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 = static_cast<ALbyte*>(buffer) + amt;
+ samples -= amt;
+ }
+ if(samples > 0)
+ memset(buffer, ((mDevice->FmtType == DevFmtUByte) ? 0x80 : 0),
+ snd_pcm_frames_to_bytes(mPcmHandle, samples));
+
+ return ALC_NO_ERROR;
+}
+
+ALCuint 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(avail));
+
+ if((avail=snd_pcm_recover(mPcmHandle, avail, 1)) >= 0)
+ {
+ if(mDoCapture)
+ avail = snd_pcm_start(mPcmHandle);
+ if(avail >= 0)
+ avail = snd_pcm_avail_update(mPcmHandle);
+ }
+ if(avail < 0)
+ {
+ ERR("restore error: %s\n", snd_strerror(avail));
+ aluHandleDisconnect(mDevice, "Capture recovery failure: %s", snd_strerror(avail));
+ }
+ }
+
+ if(!mRing)
+ {
+ if(avail < 0) avail = 0;
+ avail += snd_pcm_bytes_to_frames(mPcmHandle, mBuffer.size());
+ if(avail > mLastAvail) mLastAvail = avail;
+ return mLastAvail;
+ }
+
+ while(avail > 0)
+ {
+ auto vec = mRing->getWriteVector();
+ if(vec.first.len == 0) break;
+
+ snd_pcm_sframes_t amt{std::min<snd_pcm_sframes_t>(vec.first.len, avail)};
+ amt = snd_pcm_readi(mPcmHandle, vec.first.buf, amt);
+ if(amt < 0)
+ {
+ ERR("read error: %s\n", snd_strerror(amt));
+
+ if(amt == -EAGAIN)
+ continue;
+ if((amt=snd_pcm_recover(mPcmHandle, amt, 1)) >= 0)
+ {
+ if(mDoCapture)
+ amt = snd_pcm_start(mPcmHandle);
+ if(amt >= 0)
+ amt = snd_pcm_avail_update(mPcmHandle);
+ }
+ if(amt < 0)
+ {
+ ERR("restore error: %s\n", snd_strerror(amt));
+ aluHandleDisconnect(mDevice, "Capture recovery failure: %s", snd_strerror(amt));
+ break;
+ }
+ avail = amt;
+ continue;
+ }
+
+ mRing->writeAdvance(amt);
+ avail -= amt;
+ }
+
+ return mRing->readSpace();
+}
+
+ClockLatency AlsaCapture::getClockLatency()
+{
+ ClockLatency ret;
+
+ lock();
+ 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;
+ unlock();
+
+ 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 ALC_FALSE;
+ }
+
+ error = ALC_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); }
+
+void AlsaBackendFactory::probe(DevProbe 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 DevProbe::Playback:
+ PlaybackDevices = probe_devices(SND_PCM_STREAM_PLAYBACK);
+ std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+ break;
+
+ case DevProbe::Capture:
+ CaptureDevices = probe_devices(SND_PCM_STREAM_CAPTURE);
+ std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+ break;
+ }
+}
+
+BackendPtr AlsaBackendFactory::createBackend(ALCdevice *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
index 00000000..fb9de006
--- /dev/null
+++ b/alc/backends/alsa.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_ALSA_H
+#define BACKENDS_ALSA_H
+
+#include "backends/base.h"
+
+struct AlsaBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..a7d47c6d
--- /dev/null
+++ b/alc/backends/base.cpp
@@ -0,0 +1,58 @@
+
+#include "config.h"
+
+#include <cstdlib>
+
+#include <thread>
+
+#include "alcmain.h"
+#include "alu.h"
+
+#include "backends/base.h"
+
+
+ClockLatency GetClockLatency(ALCdevice *device)
+{
+ BackendBase *backend{device->Backend.get()};
+ ClockLatency ret{backend->getClockLatency()};
+ ret.Latency += device->FixedLatency;
+ return ret;
+}
+
+
+/* BackendBase method implementations. */
+BackendBase::BackendBase(ALCdevice *device) noexcept : mDevice{device}
+{ }
+
+BackendBase::~BackendBase() = default;
+
+ALCboolean BackendBase::reset()
+{ return ALC_FALSE; }
+
+ALCenum BackendBase::captureSamples(void*, ALCuint)
+{ return ALC_INVALID_DEVICE; }
+
+ALCuint BackendBase::availableSamples()
+{ return 0; }
+
+ClockLatency BackendBase::getClockLatency()
+{
+ ClockLatency ret;
+
+ ALuint refcount;
+ do {
+ while(((refcount=mDevice->MixCount.load(std::memory_order_acquire))&1))
+ std::this_thread::yield();
+ ret.ClockTime = GetDeviceClockTime(mDevice);
+ std::atomic_thread_fence(std::memory_order_acquire);
+ } while(refcount != mDevice->MixCount.load(std::memory_order_relaxed));
+
+ /* 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::chrono::seconds{maxi(mDevice->BufferSize-mDevice->UpdateSize, 0)};
+ ret.Latency /= mDevice->Frequency;
+
+ return ret;
+}
diff --git a/alc/backends/base.h b/alc/backends/base.h
new file mode 100644
index 00000000..437e31d9
--- /dev/null
+++ b/alc/backends/base.h
@@ -0,0 +1,78 @@
+#ifndef ALC_BACKENDS_BASE_H
+#define ALC_BACKENDS_BASE_H
+
+#include <memory>
+#include <chrono>
+#include <string>
+#include <mutex>
+
+#include "alcmain.h"
+
+
+struct ClockLatency {
+ std::chrono::nanoseconds ClockTime;
+ std::chrono::nanoseconds Latency;
+};
+
+/* Helper to get the current clock time from the device's ClockBase, and
+ * SamplesDone converted from the sample rate.
+ */
+inline std::chrono::nanoseconds GetDeviceClockTime(ALCdevice *device)
+{
+ using std::chrono::seconds;
+ using std::chrono::nanoseconds;
+
+ auto ns = nanoseconds{seconds{device->SamplesDone}} / device->Frequency;
+ return device->ClockBase + ns;
+}
+
+ClockLatency GetClockLatency(ALCdevice *device);
+
+struct BackendBase {
+ virtual ALCenum open(const ALCchar *name) = 0;
+
+ virtual ALCboolean reset();
+ virtual ALCboolean start() = 0;
+ virtual void stop() = 0;
+
+ virtual ALCenum captureSamples(void *buffer, ALCuint samples);
+ virtual ALCuint availableSamples();
+
+ virtual ClockLatency getClockLatency();
+
+ virtual void lock() { mMutex.lock(); }
+ virtual void unlock() { mMutex.unlock(); }
+
+ ALCdevice *mDevice;
+
+ std::recursive_mutex mMutex;
+
+ BackendBase(ALCdevice *device) noexcept;
+ virtual ~BackendBase();
+};
+using BackendPtr = std::unique_ptr<BackendBase>;
+using BackendUniqueLock = std::unique_lock<BackendBase>;
+using BackendLockGuard = std::lock_guard<BackendBase>;
+
+enum class BackendType {
+ Playback,
+ Capture
+};
+
+enum class DevProbe {
+ Playback,
+ Capture
+};
+
+
+struct BackendFactory {
+ virtual bool init() = 0;
+
+ virtual bool querySupport(BackendType type) = 0;
+
+ virtual void probe(DevProbe type, std::string *outnames) = 0;
+
+ virtual BackendPtr createBackend(ALCdevice *device, BackendType type) = 0;
+};
+
+#endif /* ALC_BACKENDS_BASE_H */
diff --git a/alc/backends/coreaudio.cpp b/alc/backends/coreaudio.cpp
new file mode 100644
index 00000000..b4b46382
--- /dev/null
+++ b/alc/backends/coreaudio.cpp
@@ -0,0 +1,709 @@
+/**
+ * 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 "backends/coreaudio.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "ringbuffer.h"
+#include "converter.h"
+#include "backends/base.h"
+
+#include <unistd.h>
+#include <AudioUnit/AudioUnit.h>
+#include <AudioToolbox/AudioToolbox.h>
+
+
+namespace {
+
+static const ALCchar ca_device[] = "CoreAudio Default";
+
+
+struct CoreAudioPlayback final : public BackendBase {
+ CoreAudioPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~CoreAudioPlayback() override;
+
+ static OSStatus MixerProcC(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags,
+ const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
+ AudioBufferList *ioData);
+ OSStatus MixerProc(AudioUnitRenderActionFlags *ioActionFlags,
+ const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
+ AudioBufferList *ioData);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ AudioUnit mAudioUnit;
+
+ ALuint mFrameSize{0u};
+ AudioStreamBasicDescription mFormat{}; // This is the OpenAL format as a CoreAudio ASBD
+
+ DEF_NEWDEL(CoreAudioPlayback)
+};
+
+CoreAudioPlayback::~CoreAudioPlayback()
+{
+ AudioUnitUninitialize(mAudioUnit);
+ AudioComponentInstanceDispose(mAudioUnit);
+}
+
+
+OSStatus CoreAudioPlayback::MixerProcC(void *inRefCon,
+ AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp,
+ UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData)
+{
+ return static_cast<CoreAudioPlayback*>(inRefCon)->MixerProc(ioActionFlags, inTimeStamp,
+ inBusNumber, inNumberFrames, ioData);
+}
+
+OSStatus CoreAudioPlayback::MixerProc(AudioUnitRenderActionFlags*,
+ const AudioTimeStamp*, UInt32, UInt32, AudioBufferList *ioData)
+{
+ lock();
+ aluMixData(mDevice, ioData->mBuffers[0].mData, ioData->mBuffers[0].mDataByteSize/mFrameSize);
+ unlock();
+ return noErr;
+}
+
+
+ALCenum CoreAudioPlayback::open(const ALCchar *name)
+{
+ if(!name)
+ name = ca_device;
+ else if(strcmp(name, ca_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ /* open the default output unit */
+ AudioComponentDescription desc{};
+ desc.componentType = kAudioUnitType_Output;
+#if TARGET_OS_IOS
+ desc.componentSubType = kAudioUnitSubType_RemoteIO;
+#else
+ desc.componentSubType = kAudioUnitSubType_DefaultOutput;
+#endif
+ desc.componentManufacturer = kAudioUnitManufacturer_Apple;
+ desc.componentFlags = 0;
+ desc.componentFlagsMask = 0;
+
+ AudioComponent comp{AudioComponentFindNext(NULL, &desc)};
+ if(comp == nullptr)
+ {
+ ERR("AudioComponentFindNext failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ OSStatus err{AudioComponentInstanceNew(comp, &mAudioUnit)};
+ if(err != noErr)
+ {
+ ERR("AudioComponentInstanceNew failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ /* init and start the default audio unit... */
+ err = AudioUnitInitialize(mAudioUnit);
+ if(err != noErr)
+ {
+ ERR("AudioUnitInitialize failed\n");
+ AudioComponentInstanceDispose(mAudioUnit);
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean CoreAudioPlayback::reset()
+{
+ OSStatus err{AudioUnitUninitialize(mAudioUnit)};
+ if(err != noErr)
+ ERR("-- AudioUnitUninitialize failed.\n");
+
+ /* retrieve default output unit's properties (output side) */
+ AudioStreamBasicDescription streamFormat{};
+ auto size = static_cast<UInt32>(sizeof(AudioStreamBasicDescription));
+ err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output,
+ 0, &streamFormat, &size);
+ if(err != noErr || size != sizeof(AudioStreamBasicDescription))
+ {
+ ERR("AudioUnitGetProperty failed\n");
+ return ALC_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
+
+ /* set default output unit's input side to match output side */
+ err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
+ 0, &streamFormat, size);
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_FALSE;
+ }
+
+ if(mDevice->Frequency != streamFormat.mSampleRate)
+ {
+ mDevice->BufferSize = static_cast<ALuint>(uint64_t{mDevice->BufferSize} *
+ streamFormat.mSampleRate / mDevice->Frequency);
+ mDevice->Frequency = streamFormat.mSampleRate;
+ }
+
+ /* FIXME: How to tell what channels are what in the output device, and how
+ * to specify what we're giving? eg, 6.0 vs 5.1 */
+ switch(streamFormat.mChannelsPerFrame)
+ {
+ case 1:
+ mDevice->FmtChans = DevFmtMono;
+ break;
+ case 2:
+ mDevice->FmtChans = DevFmtStereo;
+ break;
+ case 4:
+ mDevice->FmtChans = DevFmtQuad;
+ break;
+ case 6:
+ mDevice->FmtChans = DevFmtX51;
+ break;
+ case 7:
+ mDevice->FmtChans = DevFmtX61;
+ break;
+ case 8:
+ mDevice->FmtChans = DevFmtX71;
+ break;
+ default:
+ ERR("Unhandled channel count (%d), using Stereo\n", streamFormat.mChannelsPerFrame);
+ mDevice->FmtChans = DevFmtStereo;
+ streamFormat.mChannelsPerFrame = 2;
+ break;
+ }
+ SetDefaultWFXChannelOrder(mDevice);
+
+ /* use channel count and sample rate from the default output unit's current
+ * parameters, but reset everything else */
+ streamFormat.mFramesPerPacket = 1;
+ streamFormat.mFormatFlags = 0;
+ 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.mFormatID = kAudioFormatLinearPCM;
+ streamFormat.mFormatFlags |= kAudioFormatFlagsNativeEndian |
+ kLinearPCMFormatFlagIsPacked;
+
+ err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
+ 0, &streamFormat, sizeof(AudioStreamBasicDescription));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_FALSE;
+ }
+
+ /* setup callback */
+ mFrameSize = mDevice->frameSizeFromFmt();
+ AURenderCallbackStruct input{};
+ input.inputProc = CoreAudioPlayback::MixerProcC;
+ input.inputProcRefCon = this;
+
+ err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input, 0, &input, sizeof(AURenderCallbackStruct));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_FALSE;
+ }
+
+ /* init the default audio unit... */
+ err = AudioUnitInitialize(mAudioUnit);
+ if(err != noErr)
+ {
+ ERR("AudioUnitInitialize failed\n");
+ return ALC_FALSE;
+ }
+
+ return ALC_TRUE;
+}
+
+ALCboolean CoreAudioPlayback::start()
+{
+ OSStatus err{AudioOutputUnitStart(mAudioUnit)};
+ if(err != noErr)
+ {
+ ERR("AudioOutputUnitStart failed\n");
+ return ALC_FALSE;
+ }
+ return ALC_TRUE;
+}
+
+void CoreAudioPlayback::stop()
+{
+ OSStatus err{AudioOutputUnitStop(mAudioUnit)};
+ if(err != noErr)
+ ERR("AudioOutputUnitStop failed\n");
+}
+
+
+struct CoreAudioCapture final : public BackendBase {
+ CoreAudioCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~CoreAudioCapture() override;
+
+ static OSStatus RecordProcC(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags,
+ const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
+ AudioBufferList *ioData);
+ OSStatus RecordProc(AudioUnitRenderActionFlags *ioActionFlags,
+ const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber,
+ UInt32 inNumberFrames, AudioBufferList *ioData);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(void *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+
+ AudioUnit mAudioUnit{0};
+
+ ALuint mFrameSize{0u};
+ AudioStreamBasicDescription mFormat{}; // This is the OpenAL format as a CoreAudio ASBD
+
+ SampleConverterPtr mConverter;
+
+ RingBufferPtr mRing{nullptr};
+
+ DEF_NEWDEL(CoreAudioCapture)
+};
+
+CoreAudioCapture::~CoreAudioCapture()
+{
+ if(mAudioUnit)
+ AudioComponentInstanceDispose(mAudioUnit);
+ mAudioUnit = 0;
+}
+
+
+OSStatus CoreAudioCapture::RecordProcC(void *inRefCon,
+ AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp,
+ UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData)
+{
+ return static_cast<CoreAudioCapture*>(inRefCon)->RecordProc(ioActionFlags, inTimeStamp,
+ inBusNumber, inNumberFrames, ioData);
+}
+
+OSStatus CoreAudioCapture::RecordProc(AudioUnitRenderActionFlags*,
+ const AudioTimeStamp *inTimeStamp, UInt32, UInt32 inNumberFrames,
+ AudioBufferList*)
+{
+ AudioUnitRenderActionFlags flags = 0;
+ union {
+ ALbyte _[sizeof(AudioBufferList) + sizeof(AudioBuffer)*2];
+ AudioBufferList list;
+ } audiobuf = { { 0 } };
+
+ auto rec_vec = mRing->getWriteVector();
+ inNumberFrames = minz(inNumberFrames, rec_vec.first.len+rec_vec.second.len);
+
+ // Fill the ringbuffer's two segments with data from the input device
+ if(rec_vec.first.len >= inNumberFrames)
+ {
+ audiobuf.list.mNumberBuffers = 1;
+ audiobuf.list.mBuffers[0].mNumberChannels = mFormat.mChannelsPerFrame;
+ audiobuf.list.mBuffers[0].mData = rec_vec.first.buf;
+ audiobuf.list.mBuffers[0].mDataByteSize = inNumberFrames * mFormat.mBytesPerFrame;
+ }
+ else
+ {
+ const size_t remaining{inNumberFrames-rec_vec.first.len};
+ audiobuf.list.mNumberBuffers = 2;
+ audiobuf.list.mBuffers[0].mNumberChannels = mFormat.mChannelsPerFrame;
+ audiobuf.list.mBuffers[0].mData = rec_vec.first.buf;
+ audiobuf.list.mBuffers[0].mDataByteSize = rec_vec.first.len * mFormat.mBytesPerFrame;
+ audiobuf.list.mBuffers[1].mNumberChannels = mFormat.mChannelsPerFrame;
+ audiobuf.list.mBuffers[1].mData = rec_vec.second.buf;
+ audiobuf.list.mBuffers[1].mDataByteSize = remaining * mFormat.mBytesPerFrame;
+ }
+ OSStatus err{AudioUnitRender(mAudioUnit, &flags, inTimeStamp, audiobuf.list.mNumberBuffers,
+ inNumberFrames, &audiobuf.list)};
+ if(err != noErr)
+ {
+ ERR("AudioUnitRender error: %d\n", err);
+ return err;
+ }
+
+ mRing->writeAdvance(inNumberFrames);
+ return noErr;
+}
+
+
+ALCenum CoreAudioCapture::open(const ALCchar *name)
+{
+ AudioStreamBasicDescription requestedFormat; // The application requested format
+ AudioStreamBasicDescription hardwareFormat; // The hardware format
+ AudioStreamBasicDescription outputFormat; // The AudioUnit output format
+ AURenderCallbackStruct input;
+ AudioComponentDescription desc;
+ UInt32 outputFrameCount;
+ UInt32 propertySize;
+ AudioObjectPropertyAddress propertyAddress;
+ UInt32 enableIO;
+ AudioComponent comp;
+ OSStatus err;
+
+ if(!name)
+ name = ca_device;
+ else if(strcmp(name, ca_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ desc.componentType = kAudioUnitType_Output;
+#if TARGET_OS_IOS
+ desc.componentSubType = kAudioUnitSubType_RemoteIO;
+#else
+ desc.componentSubType = kAudioUnitSubType_HALOutput;
+#endif
+ desc.componentManufacturer = kAudioUnitManufacturer_Apple;
+ desc.componentFlags = 0;
+ desc.componentFlagsMask = 0;
+
+ // Search for component with given description
+ comp = AudioComponentFindNext(NULL, &desc);
+ if(comp == NULL)
+ {
+ ERR("AudioComponentFindNext failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Open the component
+ err = AudioComponentInstanceNew(comp, &mAudioUnit);
+ if(err != noErr)
+ {
+ ERR("AudioComponentInstanceNew failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Turn off AudioUnit output
+ enableIO = 0;
+ err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_EnableIO,
+ kAudioUnitScope_Output, 0, &enableIO, sizeof(ALuint));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Turn on AudioUnit input
+ enableIO = 1;
+ err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_EnableIO,
+ kAudioUnitScope_Input, 1, &enableIO, sizeof(ALuint));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+#if !TARGET_OS_IOS
+ {
+ // Get the default input device
+ AudioDeviceID inputDevice = kAudioDeviceUnknown;
+
+ propertySize = sizeof(AudioDeviceID);
+ propertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+
+ err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &propertySize, &inputDevice);
+ if(err != noErr)
+ {
+ ERR("AudioObjectGetPropertyData failed\n");
+ return ALC_INVALID_VALUE;
+ }
+ if(inputDevice == kAudioDeviceUnknown)
+ {
+ ERR("No input device found\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Track the input device
+ err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_CurrentDevice,
+ kAudioUnitScope_Global, 0, &inputDevice, sizeof(AudioDeviceID));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_INVALID_VALUE;
+ }
+ }
+#endif
+
+ // set capture callback
+ input.inputProc = CoreAudioCapture::RecordProcC;
+ input.inputProcRefCon = this;
+
+ err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_SetInputCallback,
+ kAudioUnitScope_Global, 0, &input, sizeof(AURenderCallbackStruct));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Initialize the device
+ err = AudioUnitInitialize(mAudioUnit);
+ if(err != noErr)
+ {
+ ERR("AudioUnitInitialize failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Get the hardware format
+ propertySize = sizeof(AudioStreamBasicDescription);
+ err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
+ 1, &hardwareFormat, &propertySize);
+ if(err != noErr || propertySize != sizeof(AudioStreamBasicDescription))
+ {
+ ERR("AudioUnitGetProperty failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Set up the requested format description
+ switch(mDevice->FmtType)
+ {
+ case DevFmtUByte:
+ requestedFormat.mBitsPerChannel = 8;
+ requestedFormat.mFormatFlags = kAudioFormatFlagIsPacked;
+ break;
+ case DevFmtShort:
+ requestedFormat.mBitsPerChannel = 16;
+ requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+ break;
+ case DevFmtInt:
+ requestedFormat.mBitsPerChannel = 32;
+ requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+ break;
+ case DevFmtFloat:
+ requestedFormat.mBitsPerChannel = 32;
+ requestedFormat.mFormatFlags = kAudioFormatFlagIsPacked;
+ break;
+ case DevFmtByte:
+ case DevFmtUShort:
+ case DevFmtUInt:
+ ERR("%s samples not supported\n", DevFmtTypeString(mDevice->FmtType));
+ return ALC_INVALID_VALUE;
+ }
+
+ switch(mDevice->FmtChans)
+ {
+ case DevFmtMono:
+ requestedFormat.mChannelsPerFrame = 1;
+ break;
+ case DevFmtStereo:
+ requestedFormat.mChannelsPerFrame = 2;
+ break;
+
+ case DevFmtQuad:
+ case DevFmtX51:
+ case DevFmtX51Rear:
+ case DevFmtX61:
+ case DevFmtX71:
+ case DevFmtAmbi3D:
+ ERR("%s not supported\n", DevFmtChannelsString(mDevice->FmtChans));
+ return ALC_INVALID_VALUE;
+ }
+
+ 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
+ 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,
+ 1, (void*)&outputFormat, sizeof(outputFormat));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ // Set the AudioUnit output format frame count
+ uint64_t FrameCount64{mDevice->UpdateSize};
+ FrameCount64 = (FrameCount64*outputFormat.mSampleRate + mDevice->Frequency-1) /
+ mDevice->Frequency;
+ FrameCount64 += MAX_RESAMPLE_PADDING*2;
+ if(FrameCount64 > std::numeric_limits<uint32_t>::max()/2)
+ {
+ ERR("FrameCount too large\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ outputFrameCount = static_cast<uint32_t>(FrameCount64);
+ err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice,
+ kAudioUnitScope_Output, 0, &outputFrameCount, sizeof(outputFrameCount));
+ if(err != noErr)
+ {
+ ERR("AudioUnitSetProperty failed: %d\n", err);
+ return ALC_INVALID_VALUE;
+ }
+
+ // Set up sample converter if needed
+ if(outputFormat.mSampleRate != mDevice->Frequency)
+ mConverter = CreateSampleConverter(mDevice->FmtType, mDevice->FmtType,
+ mFormat.mChannelsPerFrame, hardwareFormat.mSampleRate, mDevice->Frequency,
+ BSinc24Resampler);
+
+ mRing = CreateRingBuffer(outputFrameCount, mFrameSize, false);
+ if(!mRing) return ALC_INVALID_VALUE;
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+
+ALCboolean CoreAudioCapture::start()
+{
+ OSStatus err{AudioOutputUnitStart(mAudioUnit)};
+ if(err != noErr)
+ {
+ ERR("AudioOutputUnitStart failed\n");
+ return ALC_FALSE;
+ }
+ return ALC_TRUE;
+}
+
+void CoreAudioCapture::stop()
+{
+ OSStatus err{AudioOutputUnitStop(mAudioUnit)};
+ if(err != noErr)
+ ERR("AudioOutputUnitStop failed\n");
+}
+
+ALCenum CoreAudioCapture::captureSamples(void *buffer, ALCuint samples)
+{
+ if(!mConverter)
+ {
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+ }
+
+ auto rec_vec = mRing->getReadVector();
+ const void *src0{rec_vec.first.buf};
+ auto src0len = static_cast<ALsizei>(rec_vec.first.len);
+ auto got = static_cast<ALuint>(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<ALsizei>(rec_vec.second.len);
+ got += static_cast<ALuint>(mConverter->convert(&src1, &src1len,
+ static_cast<char*>(buffer)+got, samples-got));
+ total_read += rec_vec.second.len - src1len;
+ }
+
+ mRing->readAdvance(total_read);
+ return ALC_NO_ERROR;
+}
+
+ALCuint CoreAudioCapture::availableSamples()
+{
+ if(!mConverter) return mRing->readSpace();
+ return mConverter->availableOut(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; }
+
+void CoreAudioBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ case DevProbe::Capture:
+ /* Includes null char. */
+ outnames->append(ca_device, sizeof(ca_device));
+ break;
+ }
+}
+
+BackendPtr CoreAudioBackendFactory::createBackend(ALCdevice *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
index 00000000..37b9ebe5
--- /dev/null
+++ b/alc/backends/coreaudio.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_COREAUDIO_H
+#define BACKENDS_COREAUDIO_H
+
+#include "backends/base.h"
+
+struct CoreAudioBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..5a156d54
--- /dev/null
+++ b/alc/backends/dsound.cpp
@@ -0,0 +1,938 @@
+/**
+ * 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 "backends/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 "alcmain.h"
+#include "alu.h"
+#include "ringbuffer.h"
+#include "compat.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 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)
+{
+ return std::find_if(list.cbegin(), list.cend(),
+ [&name](const DevMap &entry) -> bool
+ { return entry.name == name; }
+ ) != list.cend();
+}
+
+BOOL CALLBACK DSoundEnumDevices(GUID *guid, const WCHAR *desc, const WCHAR*, void *data)
+{
+ 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(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~DSoundPlayback() override;
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ IDirectSound *mDS{nullptr};
+ IDirectSoundBuffer *mPrimaryBuffer{nullptr};
+ IDirectSoundBuffer *mBuffer{nullptr};
+ IDirectSoundNotify *mNotifies{nullptr};
+ HANDLE mNotifyEvent{nullptr};
+
+ std::atomic<bool> mKillNow{true};
+ std::thread mThread;
+
+ DEF_NEWDEL(DSoundPlayback)
+};
+
+DSoundPlayback::~DSoundPlayback()
+{
+ if(mNotifies)
+ mNotifies->Release();
+ mNotifies = nullptr;
+ if(mBuffer)
+ mBuffer->Release();
+ mBuffer = nullptr;
+ if(mPrimaryBuffer)
+ mPrimaryBuffer->Release();
+ mPrimaryBuffer = nullptr;
+
+ if(mDS)
+ mDS->Release();
+ mDS = nullptr;
+ if(mNotifyEvent)
+ CloseHandle(mNotifyEvent);
+ mNotifyEvent = nullptr;
+}
+
+
+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);
+ aluHandleDisconnect(mDevice, "Failure retrieving playback buffer info: 0x%lx", err);
+ return 1;
+ }
+
+ ALsizei 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);
+ aluHandleDisconnect(mDevice, "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))
+ {
+ lock();
+ aluMixData(mDevice, WritePtr1, WriteCnt1/FrameSize);
+ if(WriteCnt2 > 0)
+ aluMixData(mDevice, WritePtr2, WriteCnt2/FrameSize);
+ unlock();
+
+ mBuffer->Unlock(WritePtr1, WriteCnt1, WritePtr2, WriteCnt2);
+ }
+ else
+ {
+ ERR("Buffer lock error: %#lx\n", err);
+ aluHandleDisconnect(mDevice, "Failed to lock output buffer: 0x%lx", err);
+ return 1;
+ }
+
+ // Update old write cursor location
+ LastCursor += WriteCnt1+WriteCnt2;
+ LastCursor %= DSBCaps.dwBufferBytes;
+ }
+
+ return 0;
+}
+
+ALCenum DSoundPlayback::open(const ALCchar *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())
+ return ALC_INVALID_VALUE;
+ guid = &iter->guid;
+ }
+
+ hr = DS_OK;
+ mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+ if(!mNotifyEvent) hr = E_FAIL;
+
+ //DirectSound Init code
+ if(SUCCEEDED(hr))
+ hr = DirectSoundCreate(guid, &mDS, nullptr);
+ if(SUCCEEDED(hr))
+ hr = mDS->SetCooperativeLevel(GetForegroundWindow(), DSSCL_PRIORITY);
+ if(FAILED(hr))
+ {
+ ERR("Device init failed: 0x%08lx\n", hr);
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean DSoundPlayback::reset()
+{
+ if(mNotifies)
+ mNotifies->Release();
+ mNotifies = nullptr;
+ if(mBuffer)
+ mBuffer->Release();
+ mBuffer = nullptr;
+ if(mPrimaryBuffer)
+ mPrimaryBuffer->Release();
+ mPrimaryBuffer = nullptr;
+
+ switch(mDevice->FmtType)
+ {
+ case DevFmtByte:
+ mDevice->FmtType = DevFmtUByte;
+ break;
+ case DevFmtFloat:
+ if(mDevice->Flags.get<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(SUCCEEDED(hr))
+ {
+ speakers = DSSPEAKER_CONFIG(speakers);
+ if(!mDevice->Flags.get<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)
+ mDevice->FmtChans = DevFmtX51;
+ else if(speakers == DSSPEAKER_5POINT1_BACK)
+ mDevice->FmtChans = DevFmtX51Rear;
+ else if(speakers == DSSPEAKER_7POINT1 || speakers == DSSPEAKER_7POINT1_SURROUND)
+ mDevice->FmtChans = DevFmtX71;
+ else
+ ERR("Unknown system speaker config: 0x%lx\n", speakers);
+ }
+ mDevice->IsHeadphones = (mDevice->FmtChans == DevFmtStereo &&
+ speakers == DSSPEAKER_HEADPHONE);
+
+ switch(mDevice->FmtChans)
+ {
+ case DevFmtMono:
+ OutputType.dwChannelMask = SPEAKER_FRONT_CENTER;
+ break;
+ case DevFmtAmbi3D:
+ mDevice->FmtChans = DevFmtStereo;
+ /*fall-through*/
+ case DevFmtStereo:
+ OutputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT;
+ break;
+ case DevFmtQuad:
+ OutputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_BACK_LEFT |
+ SPEAKER_BACK_RIGHT;
+ break;
+ case DevFmtX51:
+ OutputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_SIDE_LEFT |
+ SPEAKER_SIDE_RIGHT;
+ break;
+ case DevFmtX51Rear:
+ OutputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_BACK_LEFT |
+ SPEAKER_BACK_RIGHT;
+ break;
+ case DevFmtX61:
+ OutputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_BACK_CENTER |
+ SPEAKER_SIDE_LEFT |
+ SPEAKER_SIDE_RIGHT;
+ break;
+ case DevFmtX71:
+ OutputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_BACK_LEFT |
+ SPEAKER_BACK_RIGHT |
+ SPEAKER_SIDE_LEFT |
+ SPEAKER_SIDE_RIGHT;
+ break;
+ }
+
+retry_open:
+ hr = S_OK;
+ OutputType.Format.wFormatTag = WAVE_FORMAT_PCM;
+ OutputType.Format.nChannels = mDevice->channelsFromFmt();
+ OutputType.Format.wBitsPerSample = mDevice->bytesFromFmt() * 8;
+ OutputType.Format.nBlockAlign = 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;
+
+ if(mPrimaryBuffer)
+ mPrimaryBuffer->Release();
+ mPrimaryBuffer = nullptr;
+ }
+ else
+ {
+ if(SUCCEEDED(hr) && !mPrimaryBuffer)
+ {
+ DSBUFFERDESC DSBDescription{};
+ DSBDescription.dwSize = sizeof(DSBDescription);
+ DSBDescription.dwFlags = DSBCAPS_PRIMARYBUFFER;
+ hr = mDS->CreateSoundBuffer(&DSBDescription, &mPrimaryBuffer, nullptr);
+ }
+ if(SUCCEEDED(hr))
+ hr = mPrimaryBuffer->SetFormat(&OutputType.Format);
+ }
+
+ if(SUCCEEDED(hr))
+ {
+ ALuint 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, 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))
+ {
+ auto Notifies = static_cast<IDirectSoundNotify*>(ptr);
+ mNotifies = Notifies;
+
+ ALuint num_updates{mDevice->BufferSize / mDevice->UpdateSize};
+ assert(num_updates <= MAX_UPDATES);
+
+ std::array<DSBPOSITIONNOTIFY,MAX_UPDATES> nots;
+ for(ALuint i{0};i < num_updates;++i)
+ {
+ nots[i].dwOffset = i * mDevice->UpdateSize * OutputType.Format.nBlockAlign;
+ nots[i].hEventNotify = mNotifyEvent;
+ }
+ if(Notifies->SetNotificationPositions(num_updates, nots.data()) != DS_OK)
+ hr = E_FAIL;
+ }
+ }
+
+ if(FAILED(hr))
+ {
+ if(mNotifies)
+ mNotifies->Release();
+ mNotifies = nullptr;
+ if(mBuffer)
+ mBuffer->Release();
+ mBuffer = nullptr;
+ if(mPrimaryBuffer)
+ mPrimaryBuffer->Release();
+ mPrimaryBuffer = nullptr;
+ return ALC_FALSE;
+ }
+
+ ResetEvent(mNotifyEvent);
+ SetDefaultWFXChannelOrder(mDevice);
+
+ return ALC_TRUE;
+}
+
+ALCboolean DSoundPlayback::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&DSoundPlayback::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Failed to start mixing thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~DSoundCapture() override;
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(void *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+
+ IDirectSoundCapture *mDSC{nullptr};
+ IDirectSoundCaptureBuffer *mDSCbuffer{nullptr};
+ DWORD mBufferBytes{0u};
+ DWORD mCursor{0u};
+
+ RingBufferPtr mRing;
+
+ DEF_NEWDEL(DSoundCapture)
+};
+
+DSoundCapture::~DSoundCapture()
+{
+ if(mDSCbuffer)
+ {
+ mDSCbuffer->Stop();
+ mDSCbuffer->Release();
+ mDSCbuffer = nullptr;
+ }
+
+ if(mDSC)
+ mDSC->Release();
+ mDSC = nullptr;
+}
+
+
+ALCenum DSoundCapture::open(const ALCchar *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())
+ return ALC_INVALID_VALUE;
+ guid = &iter->guid;
+ }
+
+ switch(mDevice->FmtType)
+ {
+ case DevFmtByte:
+ case DevFmtUShort:
+ case DevFmtUInt:
+ WARN("%s capture samples not supported\n", DevFmtTypeString(mDevice->FmtType));
+ return ALC_INVALID_ENUM;
+
+ case DevFmtUByte:
+ case DevFmtShort:
+ case DevFmtInt:
+ case DevFmtFloat:
+ break;
+ }
+
+ WAVEFORMATEXTENSIBLE InputType{};
+ switch(mDevice->FmtChans)
+ {
+ case DevFmtMono:
+ InputType.dwChannelMask = SPEAKER_FRONT_CENTER;
+ break;
+ case DevFmtStereo:
+ InputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT;
+ break;
+ case DevFmtQuad:
+ InputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_BACK_LEFT |
+ SPEAKER_BACK_RIGHT;
+ break;
+ case DevFmtX51:
+ InputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_SIDE_LEFT |
+ SPEAKER_SIDE_RIGHT;
+ break;
+ case DevFmtX51Rear:
+ InputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_BACK_LEFT |
+ SPEAKER_BACK_RIGHT;
+ break;
+ case DevFmtX61:
+ InputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_BACK_CENTER |
+ SPEAKER_SIDE_LEFT |
+ SPEAKER_SIDE_RIGHT;
+ break;
+ case DevFmtX71:
+ InputType.dwChannelMask = SPEAKER_FRONT_LEFT |
+ SPEAKER_FRONT_RIGHT |
+ SPEAKER_FRONT_CENTER |
+ SPEAKER_LOW_FREQUENCY |
+ SPEAKER_BACK_LEFT |
+ SPEAKER_BACK_RIGHT |
+ SPEAKER_SIDE_LEFT |
+ SPEAKER_SIDE_RIGHT;
+ break;
+ case DevFmtAmbi3D:
+ WARN("%s capture not supported\n", DevFmtChannelsString(mDevice->FmtChans));
+ return ALC_INVALID_ENUM;
+ }
+
+ InputType.Format.wFormatTag = WAVE_FORMAT_PCM;
+ InputType.Format.nChannels = mDevice->channelsFromFmt();
+ InputType.Format.wBitsPerSample = mDevice->bytesFromFmt() * 8;
+ InputType.Format.nBlockAlign = 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);
+ }
+
+ ALuint 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, nullptr);
+ if(SUCCEEDED(hr))
+ mDSC->CreateCaptureBuffer(&DSCBDescription, &mDSCbuffer, nullptr);
+ if(SUCCEEDED(hr))
+ {
+ mRing = CreateRingBuffer(mDevice->BufferSize, InputType.Format.nBlockAlign, false);
+ if(!mRing) hr = DSERR_OUTOFMEMORY;
+ }
+
+ if(FAILED(hr))
+ {
+ ERR("Device init failed: 0x%08lx\n", hr);
+
+ mRing = nullptr;
+ if(mDSCbuffer)
+ mDSCbuffer->Release();
+ mDSCbuffer = nullptr;
+ if(mDSC)
+ mDSC->Release();
+ mDSC = nullptr;
+
+ return ALC_INVALID_VALUE;
+ }
+
+ mBufferBytes = DSCBDescription.dwBufferBytes;
+ SetDefaultWFXChannelOrder(mDevice);
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean DSoundCapture::start()
+{
+ HRESULT hr{mDSCbuffer->Start(DSCBSTART_LOOPING)};
+ if(FAILED(hr))
+ {
+ ERR("start failed: 0x%08lx\n", hr);
+ aluHandleDisconnect(mDevice, "Failure starting capture: 0x%lx", hr);
+ return ALC_FALSE;
+ }
+ return ALC_TRUE;
+}
+
+void DSoundCapture::stop()
+{
+ HRESULT hr{mDSCbuffer->Stop()};
+ if(FAILED(hr))
+ {
+ ERR("stop failed: 0x%08lx\n", hr);
+ aluHandleDisconnect(mDevice, "Failure stopping capture: 0x%lx", hr);
+ }
+}
+
+ALCenum DSoundCapture::captureSamples(void *buffer, ALCuint samples)
+{
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+}
+
+ALCuint DSoundCapture::availableSamples()
+{
+ if(!mDevice->Connected.load(std::memory_order_acquire))
+ return static_cast<ALCuint>(mRing->readSpace());
+
+ ALsizei FrameSize{mDevice->frameSizeFromFmt()};
+ DWORD BufferBytes{mBufferBytes};
+ DWORD LastCursor{mCursor};
+
+ DWORD ReadCursor;
+ void *ReadPtr1, *ReadPtr2;
+ DWORD ReadCnt1, ReadCnt2;
+ HRESULT hr{mDSCbuffer->GetCurrentPosition(nullptr, &ReadCursor)};
+ if(SUCCEEDED(hr))
+ {
+ DWORD NumBytes{(ReadCursor-LastCursor + BufferBytes) % BufferBytes};
+ if(!NumBytes) return static_cast<ALCubyte>(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 = (LastCursor+ReadCnt1+ReadCnt2) % BufferBytes;
+ }
+
+ if(FAILED(hr))
+ {
+ ERR("update failed: 0x%08lx\n", hr);
+ aluHandleDisconnect(mDevice, "Failure retrieving capture data: 0x%lx", hr);
+ }
+
+ return static_cast<ALCuint>(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); }
+
+void DSoundBackendFactory::probe(DevProbe 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 DevProbe::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 DevProbe::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();
+}
+
+BackendPtr DSoundBackendFactory::createBackend(ALCdevice *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
index 00000000..6bef0bfc
--- /dev/null
+++ b/alc/backends/dsound.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_DSOUND_H
+#define BACKENDS_DSOUND_H
+
+#include "backends/base.h"
+
+struct DSoundBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..3f81d08c
--- /dev/null
+++ b/alc/backends/jack.cpp
@@ -0,0 +1,562 @@
+/**
+ * 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 "backends/jack.h"
+
+#include <cstdlib>
+#include <cstdio>
+#include <memory.h>
+
+#include <thread>
+#include <functional>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "alconfig.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "compat.h"
+
+#include <jack/jack.h>
+#include <jack/ringbuffer.h>
+
+
+namespace {
+
+constexpr ALCchar jackDevice[] = "JACK Default";
+
+
+#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
+
+
+jack_options_t ClientOptions = JackNullOption;
+
+ALCboolean jack_load()
+{
+ ALCboolean error = ALC_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 ALC_FALSE;
+ }
+
+ error = ALC_FALSE;
+#define LOAD_FUNC(f) do { \
+ p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(jack_handle, #f)); \
+ if(p##f == nullptr) { \
+ error = ALC_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 JackPlayback final : public BackendBase {
+ JackPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~JackPlayback() override;
+
+ static int bufferSizeNotifyC(jack_nframes_t numframes, void *arg);
+ int bufferSizeNotify(jack_nframes_t numframes);
+
+ static int processC(jack_nframes_t numframes, void *arg);
+ int process(jack_nframes_t numframes);
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+ ClockLatency getClockLatency() override;
+
+ jack_client_t *mClient{nullptr};
+ jack_port_t *mPort[MAX_OUTPUT_CHANNELS]{};
+
+ RingBufferPtr mRing;
+ al::semaphore mSem;
+
+ std::atomic<bool> mKillNow{true};
+ std::thread mThread;
+
+ DEF_NEWDEL(JackPlayback)
+};
+
+JackPlayback::~JackPlayback()
+{
+ if(!mClient)
+ return;
+
+ std::for_each(std::begin(mPort), std::end(mPort),
+ [this](jack_port_t *port) -> void
+ { if(port) jack_port_unregister(mClient, port); }
+ );
+ std::fill(std::begin(mPort), std::end(mPort), nullptr);
+ jack_client_close(mClient);
+ mClient = nullptr;
+}
+
+
+int JackPlayback::bufferSizeNotifyC(jack_nframes_t numframes, void *arg)
+{ return static_cast<JackPlayback*>(arg)->bufferSizeNotify(numframes); }
+
+int JackPlayback::bufferSizeNotify(jack_nframes_t numframes)
+{
+ std::lock_guard<std::mutex> _{mDevice->StateLock};
+ mDevice->UpdateSize = numframes;
+ mDevice->BufferSize = numframes*2;
+
+ const char *devname{mDevice->DeviceName.c_str()};
+ ALuint bufsize{ConfigValueUInt(devname, "jack", "buffer-size").value_or(mDevice->UpdateSize)};
+ bufsize = maxu(NextPowerOf2(bufsize), mDevice->UpdateSize);
+ mDevice->BufferSize = bufsize + mDevice->UpdateSize;
+
+ TRACE("%u / %u buffer\n", mDevice->UpdateSize, mDevice->BufferSize);
+
+ mRing = nullptr;
+ mRing = CreateRingBuffer(bufsize, mDevice->frameSizeFromFmt(), true);
+ if(!mRing)
+ {
+ ERR("Failed to reallocate ringbuffer\n");
+ aluHandleDisconnect(mDevice, "Failed to reallocate %u-sample buffer", bufsize);
+ }
+ return 0;
+}
+
+
+int JackPlayback::processC(jack_nframes_t numframes, void *arg)
+{ return static_cast<JackPlayback*>(arg)->process(numframes); }
+
+int JackPlayback::process(jack_nframes_t numframes)
+{
+ jack_default_audio_sample_t *out[MAX_OUTPUT_CHANNELS];
+ ALsizei numchans{0};
+ for(auto port : mPort)
+ {
+ if(!port) break;
+ out[numchans++] = static_cast<float*>(jack_port_get_buffer(port, numframes));
+ }
+
+ auto data = mRing->getReadVector();
+ jack_nframes_t todo{minu(numframes, data.first.len)};
+ std::transform(out, out+numchans, out,
+ [&data,numchans,todo](ALfloat *outbuf) -> ALfloat*
+ {
+ const ALfloat *RESTRICT in = reinterpret_cast<ALfloat*>(data.first.buf);
+ std::generate_n(outbuf, todo,
+ [&in,numchans]() noexcept -> ALfloat
+ {
+ ALfloat ret{*in};
+ in += numchans;
+ return ret;
+ }
+ );
+ data.first.buf += sizeof(ALfloat);
+ return outbuf + todo;
+ }
+ );
+ jack_nframes_t total{todo};
+
+ todo = minu(numframes-total, data.second.len);
+ if(todo > 0)
+ {
+ std::transform(out, out+numchans, out,
+ [&data,numchans,todo](ALfloat *outbuf) -> ALfloat*
+ {
+ const ALfloat *RESTRICT in = reinterpret_cast<ALfloat*>(data.second.buf);
+ std::generate_n(outbuf, todo,
+ [&in,numchans]() noexcept -> ALfloat
+ {
+ ALfloat ret{*in};
+ in += numchans;
+ return ret;
+ }
+ );
+ data.second.buf += sizeof(ALfloat);
+ return outbuf + todo;
+ }
+ );
+ total += todo;
+ }
+
+ mRing->readAdvance(total);
+ mSem.post();
+
+ if(numframes > total)
+ {
+ todo = numframes-total;
+ std::transform(out, out+numchans, out,
+ [todo](ALfloat *outbuf) -> ALfloat*
+ {
+ std::fill_n(outbuf, todo, 0.0f);
+ return outbuf + todo;
+ }
+ );
+ }
+
+ return 0;
+}
+
+int JackPlayback::mixerProc()
+{
+ SetRTPriority();
+ althrd_setname(MIXER_THREAD_NAME);
+
+ lock();
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ if(mRing->writeSpace() < mDevice->UpdateSize)
+ {
+ unlock();
+ mSem.wait();
+ lock();
+ continue;
+ }
+
+ auto data = mRing->getWriteVector();
+ auto todo = static_cast<ALuint>(data.first.len + data.second.len);
+ todo -= todo%mDevice->UpdateSize;
+
+ ALuint len1{minu(data.first.len, todo)};
+ ALuint len2{minu(data.second.len, todo-len1)};
+
+ aluMixData(mDevice, data.first.buf, len1);
+ if(len2 > 0)
+ aluMixData(mDevice, data.second.buf, len2);
+ mRing->writeAdvance(todo);
+ }
+ unlock();
+
+ return 0;
+}
+
+
+ALCenum JackPlayback::open(const ALCchar *name)
+{
+ if(!name)
+ name = jackDevice;
+ else if(strcmp(name, jackDevice) != 0)
+ return ALC_INVALID_VALUE;
+
+ const char *client_name{"alsoft"};
+ jack_status_t status;
+ mClient = jack_client_open(client_name, ClientOptions, &status, nullptr);
+ if(mClient == nullptr)
+ {
+ ERR("jack_client_open() failed, status = 0x%02x\n", status);
+ return ALC_INVALID_VALUE;
+ }
+ if((status&JackServerStarted))
+ TRACE("JACK server started\n");
+ if((status&JackNameNotUnique))
+ {
+ client_name = jack_get_client_name(mClient);
+ TRACE("Client name not unique, got `%s' instead\n", client_name);
+ }
+
+ jack_set_process_callback(mClient, &JackPlayback::processC, this);
+ jack_set_buffer_size_callback(mClient, &JackPlayback::bufferSizeNotifyC, this);
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean JackPlayback::reset()
+{
+ std::for_each(std::begin(mPort), std::end(mPort),
+ [this](jack_port_t *port) -> void
+ { if(port) jack_port_unregister(mClient, port); }
+ );
+ std::fill(std::begin(mPort), std::end(mPort), 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);
+ mDevice->BufferSize = mDevice->UpdateSize * 2;
+
+ const char *devname{mDevice->DeviceName.c_str()};
+ ALuint 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;
+
+ ALsizei numchans{mDevice->channelsFromFmt()};
+ auto ports_end = std::begin(mPort) + numchans;
+ auto bad_port = std::find_if_not(std::begin(mPort), ports_end,
+ [this](jack_port_t *&port) -> bool
+ {
+ std::string name{"channel_" + std::to_string(&port - mPort + 1)};
+ port = jack_port_register(mClient, name.c_str(), JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsOutput, 0);
+ return port != nullptr;
+ }
+ );
+ if(bad_port != ports_end)
+ {
+ ERR("Not enough JACK ports available for %s output\n", DevFmtChannelsString(mDevice->FmtChans));
+ if(bad_port == std::begin(mPort)) return ALC_FALSE;
+
+ if(bad_port == std::begin(mPort)+1)
+ mDevice->FmtChans = DevFmtMono;
+ else
+ {
+ ports_end = mPort+2;
+ while(bad_port != ports_end)
+ {
+ jack_port_unregister(mClient, *(--bad_port));
+ *bad_port = nullptr;
+ }
+ mDevice->FmtChans = DevFmtStereo;
+ }
+ numchans = std::distance(std::begin(mPort), bad_port);
+ }
+
+ mRing = nullptr;
+ mRing = CreateRingBuffer(bufsize, mDevice->frameSizeFromFmt(), true);
+ if(!mRing)
+ {
+ ERR("Failed to allocate ringbuffer\n");
+ return ALC_FALSE;
+ }
+
+ SetDefaultChannelOrder(mDevice);
+
+ return ALC_TRUE;
+}
+
+ALCboolean JackPlayback::start()
+{
+ if(jack_activate(mClient))
+ {
+ ERR("Failed to activate client\n");
+ return ALC_FALSE;
+ }
+
+ const char **ports{jack_get_ports(mClient, nullptr, nullptr,
+ JackPortIsPhysical|JackPortIsInput)};
+ if(ports == nullptr)
+ {
+ ERR("No physical playback ports found\n");
+ jack_deactivate(mClient);
+ return ALC_FALSE;
+ }
+ std::mismatch(std::begin(mPort), std::end(mPort), ports,
+ [this](const jack_port_t *port, const char *pname) -> bool
+ {
+ if(!port) return false;
+ if(!pname)
+ {
+ ERR("No physical playback port for \"%s\"\n", jack_port_name(port));
+ return false;
+ }
+ if(jack_connect(mClient, jack_port_name(port), pname))
+ ERR("Failed to connect output port \"%s\" to \"%s\"\n", jack_port_name(port),
+ pname);
+ return true;
+ }
+ );
+ jack_free(ports);
+
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&JackPlayback::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ jack_deactivate(mClient);
+ return ALC_FALSE;
+}
+
+void JackPlayback::stop()
+{
+ if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+ return;
+
+ mSem.post();
+ mThread.join();
+
+ jack_deactivate(mClient);
+}
+
+
+ClockLatency JackPlayback::getClockLatency()
+{
+ ClockLatency ret;
+
+ lock();
+ ret.ClockTime = GetDeviceClockTime(mDevice);
+ ret.Latency = std::chrono::seconds{mRing->readSpace()};
+ ret.Latency /= mDevice->Frequency;
+ unlock();
+
+ 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", 0))
+ ClientOptions = static_cast<jack_options_t>(ClientOptions | JackNoStartServer);
+
+ void (*old_error_cb)(const char*){&jack_error_callback ? jack_error_callback : nullptr};
+ jack_set_error_function(jack_msg_handler);
+ jack_status_t status;
+ jack_client_t *client{jack_client_open("alsoft", ClientOptions, &status, nullptr)};
+ jack_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); }
+
+void JackBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ /* Includes null char. */
+ outnames->append(jackDevice, sizeof(jackDevice));
+ break;
+
+ case DevProbe::Capture:
+ break;
+ }
+}
+
+BackendPtr JackBackendFactory::createBackend(ALCdevice *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
index 00000000..10beebfb
--- /dev/null
+++ b/alc/backends/jack.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_JACK_H
+#define BACKENDS_JACK_H
+
+#include "backends/base.h"
+
+struct JackBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..4a1c641a
--- /dev/null
+++ b/alc/backends/loopback.cpp
@@ -0,0 +1,80 @@
+/**
+ * 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 "backends/loopback.h"
+
+#include "alcmain.h"
+#include "alu.h"
+
+
+namespace {
+
+struct LoopbackBackend final : public BackendBase {
+ LoopbackBackend(ALCdevice *device) noexcept : BackendBase{device} { }
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ DEF_NEWDEL(LoopbackBackend)
+};
+
+
+ALCenum LoopbackBackend::open(const ALCchar *name)
+{
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean LoopbackBackend::reset()
+{
+ SetDefaultWFXChannelOrder(mDevice);
+ return ALC_TRUE;
+}
+
+ALCboolean LoopbackBackend::start()
+{ return ALC_TRUE; }
+
+void LoopbackBackend::stop()
+{ }
+
+} // namespace
+
+
+bool LoopbackBackendFactory::init()
+{ return true; }
+
+bool LoopbackBackendFactory::querySupport(BackendType)
+{ return true; }
+
+void LoopbackBackendFactory::probe(DevProbe, std::string*)
+{ }
+
+BackendPtr LoopbackBackendFactory::createBackend(ALCdevice *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
index 00000000..09c085b8
--- /dev/null
+++ b/alc/backends/loopback.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_LOOPBACK_H
+#define BACKENDS_LOOPBACK_H
+
+#include "backends/base.h"
+
+struct LoopbackBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..ae58cb8b
--- /dev/null
+++ b/alc/backends/null.cpp
@@ -0,0 +1,184 @@
+/**
+ * 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 "backends/null.h"
+
+#include <exception>
+#include <atomic>
+#include <chrono>
+#include <cstdint>
+#include <cstring>
+#include <functional>
+#include <thread>
+
+#include "alcmain.h"
+#include "almalloc.h"
+#include "alu.h"
+#include "logging.h"
+#include "threads.h"
+
+
+namespace {
+
+using std::chrono::seconds;
+using std::chrono::milliseconds;
+using std::chrono::nanoseconds;
+
+constexpr ALCchar nullDevice[] = "No Output";
+
+
+struct NullBackend final : public BackendBase {
+ NullBackend(ALCdevice *device) noexcept : BackendBase{device} { }
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean 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)
+ {
+ lock();
+ aluMixData(mDevice, nullptr, mDevice->UpdateSize);
+ unlock();
+ 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;
+}
+
+
+ALCenum NullBackend::open(const ALCchar *name)
+{
+ if(!name)
+ name = nullDevice;
+ else if(strcmp(name, nullDevice) != 0)
+ return ALC_INVALID_VALUE;
+
+ mDevice->DeviceName = name;
+
+ return ALC_NO_ERROR;
+}
+
+ALCboolean NullBackend::reset()
+{
+ SetDefaultWFXChannelOrder(mDevice);
+ return ALC_TRUE;
+}
+
+ALCboolean NullBackend::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&NullBackend::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Failed to start mixing thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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); }
+
+void NullBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ /* Includes null char. */
+ outnames->append(nullDevice, sizeof(nullDevice));
+ break;
+ case DevProbe::Capture:
+ break;
+ }
+}
+
+BackendPtr NullBackendFactory::createBackend(ALCdevice *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
index 00000000..f19d5b4d
--- /dev/null
+++ b/alc/backends/null.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_NULL_H
+#define BACKENDS_NULL_H
+
+#include "backends/base.h"
+
+struct NullBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+
+ static BackendFactory &getFactory();
+};
+
+#endif /* BACKENDS_NULL_H */
diff --git a/alc/backends/opensl.cpp b/alc/backends/opensl.cpp
new file mode 100644
index 00000000..b34dc0cb
--- /dev/null
+++ b/alc/backends/opensl.cpp
@@ -0,0 +1,936 @@
+/*
+ * 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 "backends/opensl.h"
+
+#include <stdlib.h>
+#include <jni.h>
+
+#include <new>
+#include <array>
+#include <thread>
+#include <functional>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "compat.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 ALCchar opensl_device[] = "OpenSL";
+
+
+SLuint32 GetChannelMask(DevFmtChannels chans)
+{
+ 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 DevFmtX51Rear: return SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT|
+ SL_SPEAKER_FRONT_CENTER|SL_SPEAKER_LOW_FREQUENCY|
+ SL_SPEAKER_BACK_LEFT|SL_SPEAKER_BACK_RIGHT;
+ case DevFmtX61: return SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT|
+ SL_SPEAKER_FRONT_CENTER|SL_SPEAKER_LOW_FREQUENCY|
+ SL_SPEAKER_BACK_CENTER|
+ SL_SPEAKER_SIDE_LEFT|SL_SPEAKER_SIDE_RIGHT;
+ case DevFmtX71: 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 DevFmtAmbi3D:
+ break;
+ }
+ return 0;
+}
+
+#ifdef SL_ANDROID_DATAFORMAT_PCM_EX
+SLuint32 GetTypeRepresentation(DevFmtType type)
+{
+ 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
+
+const char *res_str(SLresult result)
+{
+ 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";
+}
+
+#define PRINTERR(x, s) do { \
+ if(UNLIKELY((x) != SL_RESULT_SUCCESS)) \
+ ERR("%s: %s\n", (s), res_str((x))); \
+} while(0)
+
+
+struct OpenSLPlayback final : public BackendBase {
+ OpenSLPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~OpenSLPlayback() override;
+
+ static void processC(SLAndroidSimpleBufferQueueItf bq, void *context);
+ void process(SLAndroidSimpleBufferQueueItf bq);
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean 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;
+
+ ALsizei 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::processC(SLAndroidSimpleBufferQueueItf bq, void *context)
+{ static_cast<OpenSLPlayback*>(context)->process(bq); }
+
+void OpenSLPlayback::process(SLAndroidSimpleBufferQueueItf)
+{
+ /* 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");
+ }
+
+ lock();
+ if(SL_RESULT_SUCCESS != result)
+ aluHandleDisconnect(mDevice, "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)
+ {
+ aluHandleDisconnect(mDevice, "Failed to start platback: 0x%08x", result);
+ break;
+ }
+
+ if(mRing->writeSpace() == 0)
+ {
+ unlock();
+ mSem.wait();
+ lock();
+ continue;
+ }
+ }
+
+ auto data = mRing->getWriteVector();
+ aluMixData(mDevice, data.first.buf, data.first.len*mDevice->UpdateSize);
+ if(data.second.len > 0)
+ aluMixData(mDevice, data.second.buf, data.second.len*mDevice->UpdateSize);
+
+ size_t todo{data.first.len + data.second.len};
+ mRing->writeAdvance(todo);
+
+ 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)
+ {
+ aluHandleDisconnect(mDevice, "Failed to queue audio: 0x%08x", result);
+ break;
+ }
+
+ data.first.len--;
+ data.first.buf += mDevice->UpdateSize*mFrameSize;
+ }
+ }
+ unlock();
+
+ return 0;
+}
+
+
+ALCenum OpenSLPlayback::open(const ALCchar *name)
+{
+ if(!name)
+ name = opensl_device;
+ else if(strcmp(name, opensl_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ // 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;
+
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean OpenSLPlayback::reset()
+{
+ SLDataLocator_AndroidSimpleBufferQueue loc_bufq;
+ SLDataLocator_OutputMix loc_outmix;
+ SLDataSource audioSrc;
+ SLDataSink audioSnk;
+ 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(mDevice);
+ 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 }};
+
+ loc_bufq.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
+ loc_bufq.numBuffers = mDevice->BufferSize / mDevice->UpdateSize;
+
+#ifdef SL_ANDROID_DATAFORMAT_PCM_EX
+ SLAndroidDataFormat_PCM_EX format_pcm{};
+ format_pcm.formatType = SL_ANDROID_DATAFORMAT_PCM_EX;
+ format_pcm.numChannels = mDevice->channelsFromFmt();
+ format_pcm.sampleRate = mDevice->Frequency * 1000;
+ format_pcm.bitsPerSample = mDevice->bytesFromFmt() * 8;
+ format_pcm.containerSize = format_pcm.bitsPerSample;
+ format_pcm.channelMask = GetChannelMask(mDevice->FmtChans);
+ format_pcm.endianness = IS_LITTLE_ENDIAN ? SL_BYTEORDER_LITTLEENDIAN :
+ SL_BYTEORDER_BIGENDIAN;
+ format_pcm.representation = GetTypeRepresentation(mDevice->FmtType);
+#else
+ 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 = IS_LITTLE_ENDIAN ? SL_BYTEORDER_LITTLEENDIAN :
+ SL_BYTEORDER_BIGENDIAN;
+#endif
+
+ audioSrc.pLocator = &loc_bufq;
+ audioSrc.pFormat = &format_pcm;
+
+ loc_outmix.locatorType = SL_DATALOCATOR_OUTPUTMIX;
+ loc_outmix.outputMix = mOutputMix;
+ audioSnk.pLocator = &loc_outmix;
+ audioSnk.pFormat = nullptr;
+
+
+ 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 ALuint num_updates{mDevice->BufferSize / mDevice->UpdateSize};
+ try {
+ mRing = CreateRingBuffer(num_updates, mFrameSize*mDevice->UpdateSize, true);
+ }
+ catch(std::exception& e) {
+ ERR("Failed allocating ring buffer %ux%ux%u: %s\n", mDevice->UpdateSize,
+ num_updates, mFrameSize, e.what());
+ result = SL_RESULT_MEMORY_FAILURE;
+ }
+ }
+
+ if(SL_RESULT_SUCCESS != result)
+ {
+ if(mBufferQueueObj)
+ VCALL0(mBufferQueueObj,Destroy)();
+ mBufferQueueObj = nullptr;
+
+ return ALC_FALSE;
+ }
+
+ return ALC_TRUE;
+}
+
+ALCboolean OpenSLPlayback::start()
+{
+ mRing->reset();
+
+ SLAndroidSimpleBufferQueueItf bufferQueue;
+ SLresult result{VCALL(mBufferQueueObj,GetInterface)(SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+ &bufferQueue)};
+ PRINTERR(result, "bufferQueue->GetInterface");
+ if(SL_RESULT_SUCCESS != result)
+ return ALC_FALSE;
+
+ result = VCALL(bufferQueue,RegisterCallback)(&OpenSLPlayback::processC, this);
+ PRINTERR(result, "bufferQueue->RegisterCallback");
+ if(SL_RESULT_SUCCESS != result) return ALC_FALSE;
+
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread(std::mem_fn(&OpenSLPlayback::mixerProc), this);
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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");
+ }
+}
+
+ClockLatency OpenSLPlayback::getClockLatency()
+{
+ ClockLatency ret;
+
+ lock();
+ ret.ClockTime = GetDeviceClockTime(mDevice);
+ ret.Latency = std::chrono::seconds{mRing->readSpace() * mDevice->UpdateSize};
+ ret.Latency /= mDevice->Frequency;
+ unlock();
+
+ return ret;
+}
+
+
+struct OpenSLCapture final : public BackendBase {
+ OpenSLCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~OpenSLCapture() override;
+
+ static void processC(SLAndroidSimpleBufferQueueItf bq, void *context);
+ void process(SLAndroidSimpleBufferQueueItf bq);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(void *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+
+ /* engine interfaces */
+ SLObjectItf mEngineObj{nullptr};
+ SLEngineItf mEngine;
+
+ /* recording interfaces */
+ SLObjectItf mRecordObj{nullptr};
+
+ RingBufferPtr mRing{nullptr};
+ ALCuint mSplOffset{0u};
+
+ ALsizei 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::processC(SLAndroidSimpleBufferQueueItf bq, void *context)
+{ static_cast<OpenSLCapture*>(context)->process(bq); }
+
+void OpenSLCapture::process(SLAndroidSimpleBufferQueueItf)
+{
+ /* A new chunk has been written into the ring buffer, advance it. */
+ mRing->writeAdvance(1);
+}
+
+
+ALCenum OpenSLCapture::open(const ALCchar* name)
+{
+ if(!name)
+ name = opensl_device;
+ else if(strcmp(name, opensl_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ 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 */
+ ALsizei length{maxi(mDevice->BufferSize, mDevice->Frequency/10)};
+ /* Ensure the per-chunk length is at least 10ms, and no more than 50ms. */
+ ALsizei update_len{clampi(mDevice->BufferSize/3, mDevice->Frequency/100,
+ mDevice->Frequency/100*5)};
+ ALsizei num_updates{(length+update_len-1) / update_len};
+
+ try {
+ mRing = CreateRingBuffer(num_updates, update_len*mFrameSize, false);
+
+ mDevice->UpdateSize = update_len;
+ mDevice->BufferSize = mRing->writeSpace() * update_len;
+ }
+ catch(std::exception& e) {
+ ERR("Failed to allocate ring buffer: %s\n", e.what());
+ result = SL_RESULT_MEMORY_FAILURE;
+ }
+ }
+ 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;
+
+#ifdef SL_ANDROID_DATAFORMAT_PCM_EX
+ SLAndroidDataFormat_PCM_EX format_pcm{};
+ format_pcm.formatType = SL_ANDROID_DATAFORMAT_PCM_EX;
+ format_pcm.numChannels = mDevice->channelsFromFmt();
+ format_pcm.sampleRate = mDevice->Frequency * 1000;
+ format_pcm.bitsPerSample = mDevice->bytesFromFmt() * 8;
+ format_pcm.containerSize = format_pcm.bitsPerSample;
+ format_pcm.channelMask = GetChannelMask(mDevice->FmtChans);
+ format_pcm.endianness = IS_LITTLE_ENDIAN ? SL_BYTEORDER_LITTLEENDIAN : SL_BYTEORDER_BIGENDIAN;
+ format_pcm.representation = GetTypeRepresentation(mDevice->FmtType);
+#else
+ 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 = IS_LITTLE_ENDIAN ? SL_BYTEORDER_LITTLEENDIAN : SL_BYTEORDER_BIGENDIAN;
+#endif
+
+ SLDataSink audioSnk{};
+ 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 ALuint chunk_size{mDevice->UpdateSize * mFrameSize};
+
+ auto data = mRing->getWriteVector();
+ 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;
+
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean 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)
+ {
+ aluHandleDisconnect(mDevice, "Failed to start capture: 0x%08x", result);
+ return ALC_FALSE;
+ }
+
+ return ALC_TRUE;
+}
+
+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");
+ }
+}
+
+ALCenum OpenSLCapture::captureSamples(void* buffer, ALCuint samples)
+{
+ ALsizei chunk_size = mDevice->UpdateSize * mFrameSize;
+ SLAndroidSimpleBufferQueueItf bufferQueue;
+ SLresult result;
+ ALCuint i;
+
+ result = VCALL(mRecordObj,GetInterface)(SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &bufferQueue);
+ PRINTERR(result, "recordObj->GetInterface");
+
+ /* Read the desired samples from the ring buffer then advance its read
+ * pointer.
+ */
+ auto data = mRing->getReadVector();
+ for(i = 0;i < samples;)
+ {
+ ALCuint rem{minu(samples - i, mDevice->UpdateSize - mSplOffset)};
+ memcpy((ALCbyte*)buffer + i*mFrameSize, data.first.buf + mSplOffset*mFrameSize,
+ rem * mFrameSize);
+
+ mSplOffset += rem;
+ if(mSplOffset == mDevice->UpdateSize)
+ {
+ /* Finished a chunk, reset the offset and advance the read pointer. */
+ mSplOffset = 0;
+
+ mRing->readAdvance(1);
+ result = VCALL(bufferQueue,Enqueue)(data.first.buf, chunk_size);
+ PRINTERR(result, "bufferQueue->Enqueue");
+ if(SL_RESULT_SUCCESS != result) break;
+
+ data.first.len--;
+ if(!data.first.len)
+ data.first = data.second;
+ else
+ data.first.buf += chunk_size;
+ }
+
+ i += rem;
+ }
+
+ if(SL_RESULT_SUCCESS != result)
+ {
+ aluHandleDisconnect(mDevice, "Failed to update capture buffer: 0x%08x", result);
+ return ALC_INVALID_DEVICE;
+ }
+
+ return ALC_NO_ERROR;
+}
+
+ALCuint OpenSLCapture::availableSamples()
+{ return mRing->readSpace()*mDevice->UpdateSize - mSplOffset; }
+
+} // namespace
+
+bool OSLBackendFactory::init() { return true; }
+
+bool OSLBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+void OSLBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ case DevProbe::Capture:
+ /* Includes null char. */
+ outnames->append(opensl_device, sizeof(opensl_device));
+ break;
+ }
+}
+
+BackendPtr OSLBackendFactory::createBackend(ALCdevice *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
index 00000000..809aa339
--- /dev/null
+++ b/alc/backends/opensl.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_OSL_H
+#define BACKENDS_OSL_H
+
+#include "backends/base.h"
+
+struct OSLBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..8cfe9e96
--- /dev/null
+++ b/alc/backends/oss.cpp
@@ -0,0 +1,751 @@
+/**
+ * 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 "backends/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 "AL/al.h"
+
+#include "alcmain.h"
+#include "alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alu.h"
+#include "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;
+};
+
+bool checkName(const al::vector<DevMap> &list, const std::string &name)
+{
+ return std::find_if(list.cbegin(), list.cend(),
+ [&name](const DevMap &entry) -> bool
+ { return entry.name == name; }
+ ) != list.cend();
+}
+
+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, const char *handle, size_t hlen, const char *path, size_t plen)
+{
+#ifdef ALC_OSS_DEVNODE_TRUC
+ for(size_t i{0};i < plen;i++)
+ {
+ if(path[i] == '.')
+ {
+ if(strncmp(path + i, handle + hlen + i - plen, plen - i) == 0)
+ hlen = hlen + i - plen;
+ plen = i;
+ }
+ }
+#endif
+ if(handle[0] == '\0')
+ {
+ handle = path;
+ hlen = plen;
+ }
+
+ std::string basename{handle, hlen};
+ basename.erase(std::find(basename.begin(), basename.end(), '\0'), basename.end());
+ std::string devname{path, plen};
+ devname.erase(std::find(devname.begin(), devname.end(), '\0'), devname.end());
+
+ auto iter = std::find_if(list->cbegin(), list->cend(),
+ [&devname](const DevMap &entry) -> bool
+ { return entry.device_name == devname; }
+ );
+ if(iter != list->cend())
+ return;
+
+ int count{1};
+ std::string newname{basename};
+ while(checkName(PlaybackDevices, 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;
+
+ const char *handle;
+ size_t len;
+ if(ai.handle[0] != '\0')
+ {
+ len = strnlen(ai.handle, sizeof(ai.handle));
+ handle = ai.handle;
+ }
+ else
+ {
+ len = strnlen(ai.name, sizeof(ai.name));
+ handle = ai.name;
+ }
+
+ ALCossListAppend(devlist, handle, len, ai.devnode,
+ strnlen(ai.devnode, sizeof(ai.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
+
+int log2i(ALCuint x)
+{
+ int y = 0;
+ while (x > 1)
+ {
+ x >>= 1;
+ y++;
+ }
+ return y;
+}
+
+
+struct OSSPlayback final : public BackendBase {
+ OSSPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~OSSPlayback() override;
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ int mFd{-1};
+
+ al::vector<ALubyte> 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 int frame_size{mDevice->frameSizeFromFmt()};
+
+ lock();
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ pollfd pollitem{};
+ pollitem.fd = mFd;
+ pollitem.events = POLLOUT;
+
+ unlock();
+ int pret{poll(&pollitem, 1, 1000)};
+ lock();
+ if(pret < 0)
+ {
+ if(errno == EINTR || errno == EAGAIN)
+ continue;
+ ERR("poll failed: %s\n", strerror(errno));
+ aluHandleDisconnect(mDevice, "Failed waiting for playback buffer: %s", strerror(errno));
+ break;
+ }
+ else if(pret == 0)
+ {
+ WARN("poll timeout\n");
+ continue;
+ }
+
+ ALubyte *write_ptr{mMixData.data()};
+ size_t to_write{mMixData.size()};
+ aluMixData(mDevice, write_ptr, to_write/frame_size);
+ 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));
+ aluHandleDisconnect(mDevice, "Failed writing playback samples: %s",
+ strerror(errno));
+ break;
+ }
+
+ to_write -= wrote;
+ write_ptr += wrote;
+ }
+ }
+ unlock();
+
+ return 0;
+}
+
+
+ALCenum OSSPlayback::open(const ALCchar *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())
+ return ALC_INVALID_VALUE;
+ devname = iter->device_name.c_str();
+ }
+
+ mFd = ::open(devname, O_WRONLY);
+ if(mFd == -1)
+ {
+ ERR("Could not open %s: %s\n", devname, strerror(errno));
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean OSSPlayback::reset()
+{
+ int numFragmentsLogSize;
+ int log2FragmentSize;
+ unsigned int periods;
+ audio_buf_info info;
+ ALuint frameSize;
+ int numChannels;
+ int ossFormat;
+ int ossSpeed;
+ const char *err;
+
+ 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;
+ }
+
+ periods = mDevice->BufferSize / mDevice->UpdateSize;
+ numChannels = mDevice->channelsFromFmt();
+ ossSpeed = mDevice->Frequency;
+ frameSize = numChannels * mDevice->bytesFromFmt();
+ /* According to the OSS spec, 16 bytes (log2(16)) is the minimum. */
+ log2FragmentSize = maxi(log2i(mDevice->UpdateSize*frameSize), 4);
+ numFragmentsLogSize = (periods << 16) | log2FragmentSize;
+
+#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 ALC_FALSE;
+ }
+#undef CHECKERR
+
+ if(mDevice->channelsFromFmt() != numChannels)
+ {
+ ERR("Failed to set %s, got %d channels instead\n", DevFmtChannelsString(mDevice->FmtChans),
+ numChannels);
+ return ALC_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 ALC_FALSE;
+ }
+
+ mDevice->Frequency = ossSpeed;
+ mDevice->UpdateSize = info.fragsize / frameSize;
+ mDevice->BufferSize = info.fragments * mDevice->UpdateSize;
+
+ SetDefaultChannelOrder(mDevice);
+
+ mMixData.resize(mDevice->UpdateSize * mDevice->frameSizeFromFmt());
+
+ return ALC_TRUE;
+}
+
+ALCboolean OSSPlayback::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&OSSPlayback::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~OSScapture() override;
+
+ int recordProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(ALCvoid *buffer, ALCuint samples) override;
+ ALCuint 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 int 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));
+ aluHandleDisconnect(mDevice, "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));
+ aluHandleDisconnect(mDevice, "Failed reading capture samples: %s",
+ strerror(errno));
+ break;
+ }
+ mRing->writeAdvance(amt/frame_size);
+ }
+ }
+
+ return 0;
+}
+
+
+ALCenum OSScapture::open(const ALCchar *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())
+ return ALC_INVALID_VALUE;
+ devname = iter->device_name.c_str();
+ }
+
+ mFd = ::open(devname, O_RDONLY);
+ if(mFd == -1)
+ {
+ ERR("Could not open %s: %s\n", devname, strerror(errno));
+ return ALC_INVALID_VALUE;
+ }
+
+ 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:
+ ERR("%s capture samples not supported\n", DevFmtTypeString(mDevice->FmtType));
+ return ALC_INVALID_VALUE;
+ }
+
+ int periods{4};
+ int numChannels{mDevice->channelsFromFmt()};
+ int frameSize{numChannels * mDevice->bytesFromFmt()};
+ int ossSpeed{static_cast<int>(mDevice->Frequency)};
+ int log2FragmentSize{log2i(mDevice->BufferSize * frameSize / periods)};
+
+ /* according to the OSS spec, 16 bytes are the minimum */
+ log2FragmentSize = std::max(log2FragmentSize, 4);
+ int numFragmentsLogSize{(periods << 16) | log2FragmentSize};
+
+ audio_buf_info info;
+ const char *err;
+#define CHECKERR(func) if((func) < 0) { \
+ err = #func; \
+ goto err; \
+}
+ 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));
+ if(0)
+ {
+ err:
+ ERR("%s failed: %s\n", err, strerror(errno));
+ close(mFd);
+ mFd = -1;
+ return ALC_INVALID_VALUE;
+ }
+#undef CHECKERR
+
+ if(mDevice->channelsFromFmt() != numChannels)
+ {
+ ERR("Failed to set %s, got %d channels instead\n", DevFmtChannelsString(mDevice->FmtChans),
+ numChannels);
+ close(mFd);
+ mFd = -1;
+ return ALC_INVALID_VALUE;
+ }
+
+ 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);
+ close(mFd);
+ mFd = -1;
+ return ALC_INVALID_VALUE;
+ }
+
+ mRing = CreateRingBuffer(mDevice->BufferSize, frameSize, false);
+ if(!mRing)
+ {
+ ERR("Ring buffer create failed\n");
+ close(mFd);
+ mFd = -1;
+ return ALC_OUT_OF_MEMORY;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean OSScapture::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&OSScapture::recordProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create record thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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));
+}
+
+ALCenum OSScapture::captureSamples(ALCvoid *buffer, ALCuint samples)
+{
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+}
+
+ALCuint OSScapture::availableSamples()
+{ return 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); }
+
+void OSSBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ auto add_device = [outnames](const DevMap &entry) -> void
+ {
+#ifdef HAVE_STAT
+ struct stat buf;
+ if(stat(entry.device_name.c_str(), &buf) == 0)
+#endif
+ {
+ /* Includes null char. */
+ outnames->append(entry.name.c_str(), entry.name.length()+1);
+ }
+ };
+
+ switch(type)
+ {
+ case DevProbe::Playback:
+ PlaybackDevices.clear();
+ ALCossListPopulate(&PlaybackDevices, DSP_CAP_OUTPUT);
+ std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+ break;
+
+ case DevProbe::Capture:
+ CaptureDevices.clear();
+ ALCossListPopulate(&CaptureDevices, DSP_CAP_INPUT);
+ std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+ break;
+ }
+}
+
+BackendPtr OSSBackendFactory::createBackend(ALCdevice *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
index 00000000..9e63d7b6
--- /dev/null
+++ b/alc/backends/oss.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_OSS_H
+#define BACKENDS_OSS_H
+
+#include "backends/base.h"
+
+struct OSSBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+
+ static BackendFactory &getFactory();
+};
+
+#endif /* BACKENDS_OSS_H */
diff --git a/alc/backends/portaudio.cpp b/alc/backends/portaudio.cpp
new file mode 100644
index 00000000..73e972c5
--- /dev/null
+++ b/alc/backends/portaudio.cpp
@@ -0,0 +1,463 @@
+/**
+ * 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 "backends/portaudio.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "alconfig.h"
+#include "ringbuffer.h"
+#include "compat.h"
+
+#include <portaudio.h>
+
+
+namespace {
+
+constexpr ALCchar 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(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~PortPlayback() override;
+
+ static int writeCallbackC(const void *inputBuffer, void *outputBuffer,
+ unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo,
+ const PaStreamCallbackFlags statusFlags, void *userData);
+ int writeCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
+ const PaStreamCallbackTimeInfo *timeInfo, const PaStreamCallbackFlags statusFlags);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ PaStream *mStream{nullptr};
+ PaStreamParameters mParams{};
+ ALuint 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::writeCallbackC(const void *inputBuffer, void *outputBuffer,
+ unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo,
+ const PaStreamCallbackFlags statusFlags, void *userData)
+{
+ return static_cast<PortPlayback*>(userData)->writeCallback(inputBuffer, outputBuffer,
+ framesPerBuffer, timeInfo, statusFlags);
+}
+
+int PortPlayback::writeCallback(const void*, void *outputBuffer,
+ unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo*,
+ const PaStreamCallbackFlags)
+{
+ lock();
+ aluMixData(mDevice, outputBuffer, framesPerBuffer);
+ unlock();
+ return 0;
+}
+
+
+ALCenum PortPlayback::open(const ALCchar *name)
+{
+ if(!name)
+ name = pa_device;
+ else if(strcmp(name, pa_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ mUpdateSize = mDevice->UpdateSize;
+
+ auto devidopt = ConfigValueInt(nullptr, "port", "device");
+ if(devidopt && *devidopt >= 0) mParams.device = *devidopt;
+ else mParams.device = Pa_GetDefaultOutputDevice();
+ mParams.suggestedLatency = mDevice->BufferSize / static_cast<double>(mDevice->Frequency);
+ mParams.hostApiSpecificStreamInfo = nullptr;
+
+ mParams.channelCount = ((mDevice->FmtChans == DevFmtMono) ? 1 : 2);
+
+ switch(mDevice->FmtType)
+ {
+ case DevFmtByte:
+ mParams.sampleFormat = paInt8;
+ break;
+ case DevFmtUByte:
+ mParams.sampleFormat = paUInt8;
+ break;
+ case DevFmtUShort:
+ /* fall-through */
+ case DevFmtShort:
+ mParams.sampleFormat = paInt16;
+ break;
+ case DevFmtUInt:
+ /* fall-through */
+ case DevFmtInt:
+ mParams.sampleFormat = paInt32;
+ break;
+ case DevFmtFloat:
+ mParams.sampleFormat = paFloat32;
+ break;
+ }
+
+retry_open:
+ PaError err{Pa_OpenStream(&mStream, nullptr, &mParams, mDevice->Frequency, mDevice->UpdateSize,
+ paNoFlag, &PortPlayback::writeCallbackC, this)};
+ if(err != paNoError)
+ {
+ if(mParams.sampleFormat == paFloat32)
+ {
+ mParams.sampleFormat = paInt16;
+ goto retry_open;
+ }
+ ERR("Pa_OpenStream() returned an error: %s\n", Pa_GetErrorText(err));
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+
+}
+
+ALCboolean PortPlayback::reset()
+{
+ const PaStreamInfo *streamInfo{Pa_GetStreamInfo(mStream)};
+ mDevice->Frequency = 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 ALC_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 ALC_FALSE;
+ }
+ SetDefaultChannelOrder(mDevice);
+
+ return ALC_TRUE;
+}
+
+ALCboolean PortPlayback::start()
+{
+ PaError err{Pa_StartStream(mStream)};
+ if(err != paNoError)
+ {
+ ERR("Pa_StartStream() returned an error: %s\n", Pa_GetErrorText(err));
+ return ALC_FALSE;
+ }
+ return ALC_TRUE;
+}
+
+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(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~PortCapture() override;
+
+ static int readCallbackC(const void *inputBuffer, void *outputBuffer,
+ unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo,
+ const PaStreamCallbackFlags statusFlags, void *userData);
+ int readCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
+ const PaStreamCallbackTimeInfo *timeInfo, const PaStreamCallbackFlags statusFlags);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(ALCvoid *buffer, ALCuint samples) override;
+ ALCuint 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::readCallbackC(const void *inputBuffer, void *outputBuffer,
+ unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo,
+ const PaStreamCallbackFlags statusFlags, void* userData)
+{
+ return static_cast<PortCapture*>(userData)->readCallback(inputBuffer, outputBuffer,
+ framesPerBuffer, timeInfo, statusFlags);
+}
+
+int PortCapture::readCallback(const void *inputBuffer, void*,
+ unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo*,
+ const PaStreamCallbackFlags)
+{
+ mRing->write(inputBuffer, framesPerBuffer);
+ return 0;
+}
+
+
+ALCenum PortCapture::open(const ALCchar *name)
+{
+ if(!name)
+ name = pa_device;
+ else if(strcmp(name, pa_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ ALuint samples{mDevice->BufferSize};
+ samples = maxu(samples, 100 * mDevice->Frequency / 1000);
+ ALsizei frame_size{mDevice->frameSizeFromFmt()};
+
+ mRing = CreateRingBuffer(samples, frame_size, false);
+ if(!mRing) return ALC_INVALID_VALUE;
+
+ 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:
+ ERR("%s samples not supported\n", DevFmtTypeString(mDevice->FmtType));
+ return ALC_INVALID_VALUE;
+ }
+ mParams.channelCount = mDevice->channelsFromFmt();
+
+ PaError err{Pa_OpenStream(&mStream, &mParams, nullptr, mDevice->Frequency,
+ paFramesPerBufferUnspecified, paNoFlag, &PortCapture::readCallbackC, this)};
+ if(err != paNoError)
+ {
+ ERR("Pa_OpenStream() returned an error: %s\n", Pa_GetErrorText(err));
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+
+ALCboolean PortCapture::start()
+{
+ PaError err{Pa_StartStream(mStream)};
+ if(err != paNoError)
+ {
+ ERR("Error starting stream: %s\n", Pa_GetErrorText(err));
+ return ALC_FALSE;
+ }
+ return ALC_TRUE;
+}
+
+void PortCapture::stop()
+{
+ PaError err{Pa_StopStream(mStream)};
+ if(err != paNoError)
+ ERR("Error stopping stream: %s\n", Pa_GetErrorText(err));
+}
+
+
+ALCuint PortCapture::availableSamples()
+{ return mRing->readSpace(); }
+
+ALCenum PortCapture::captureSamples(ALCvoid *buffer, ALCuint samples)
+{
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+}
+
+} // 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); }
+
+void PortBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ case DevProbe::Capture:
+ /* Includes null char. */
+ outnames->append(pa_device, sizeof(pa_device));
+ break;
+ }
+}
+
+BackendPtr PortBackendFactory::createBackend(ALCdevice *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
index 00000000..082e9020
--- /dev/null
+++ b/alc/backends/portaudio.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_PORTAUDIO_H
+#define BACKENDS_PORTAUDIO_H
+
+#include "backends/base.h"
+
+struct PortBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..da209c8d
--- /dev/null
+++ b/alc/backends/pulseaudio.cpp
@@ -0,0 +1,1532 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2009 by Konstantinos Natsakis <[email protected]>
+ * Copyright (C) 2010 by Chris Robinson <[email protected]>
+ * 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 "backends/pulseaudio.h"
+
+#include <poll.h>
+#include <cstring>
+
+#include <array>
+#include <string>
+#include <vector>
+#include <atomic>
+#include <thread>
+#include <algorithm>
+#include <condition_variable>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "alconfig.h"
+#include "compat.h"
+#include "alexcpt.h"
+
+#include <pulse/pulseaudio.h>
+
+
+namespace {
+
+#ifdef HAVE_DYNLOAD
+#define PULSE_FUNCS(MAGIC) \
+ MAGIC(pa_mainloop_new); \
+ MAGIC(pa_mainloop_free); \
+ MAGIC(pa_mainloop_set_poll_func); \
+ MAGIC(pa_mainloop_run); \
+ MAGIC(pa_mainloop_get_api); \
+ 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_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_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_mainloop_new ppa_mainloop_new
+#define pa_mainloop_free ppa_mainloop_free
+#define pa_mainloop_set_poll_func ppa_mainloop_set_poll_func
+#define pa_mainloop_run ppa_mainloop_run
+#define pa_mainloop_get_api ppa_mainloop_get_api
+#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_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_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
+ }
+};
+
+size_t ChannelFromPulse(pa_channel_position_t chan)
+{
+ switch(chan)
+ {
+ case PA_CHANNEL_POSITION_INVALID: break;
+ case PA_CHANNEL_POSITION_MONO: return FrontCenter;
+ case PA_CHANNEL_POSITION_FRONT_LEFT: return FrontLeft;
+ case PA_CHANNEL_POSITION_FRONT_RIGHT: return FrontRight;
+ case PA_CHANNEL_POSITION_FRONT_CENTER: return FrontCenter;
+ case PA_CHANNEL_POSITION_REAR_CENTER: return BackCenter;
+ case PA_CHANNEL_POSITION_REAR_LEFT: return BackLeft;
+ case PA_CHANNEL_POSITION_REAR_RIGHT: return BackRight;
+ case PA_CHANNEL_POSITION_LFE: return LFE;
+ case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER: break;
+ case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER: break;
+ case PA_CHANNEL_POSITION_SIDE_LEFT: return SideLeft;
+ case PA_CHANNEL_POSITION_SIDE_RIGHT: return SideRight;
+ case PA_CHANNEL_POSITION_AUX0: return Aux0;
+ case PA_CHANNEL_POSITION_AUX1: return Aux1;
+ case PA_CHANNEL_POSITION_AUX2: return Aux2;
+ case PA_CHANNEL_POSITION_AUX3: return Aux3;
+ case PA_CHANNEL_POSITION_AUX4: return Aux4;
+ case PA_CHANNEL_POSITION_AUX5: return Aux5;
+ case PA_CHANNEL_POSITION_AUX6: return Aux6;
+ case PA_CHANNEL_POSITION_AUX7: return Aux7;
+ case PA_CHANNEL_POSITION_AUX8: return Aux8;
+ case PA_CHANNEL_POSITION_AUX9: return Aux9;
+ case PA_CHANNEL_POSITION_AUX10: return Aux10;
+ case PA_CHANNEL_POSITION_AUX11: return Aux11;
+ case PA_CHANNEL_POSITION_AUX12: return Aux12;
+ case PA_CHANNEL_POSITION_AUX13: return Aux13;
+ case PA_CHANNEL_POSITION_AUX14: return Aux14;
+ case PA_CHANNEL_POSITION_AUX15: return Aux15;
+ case PA_CHANNEL_POSITION_AUX16: break;
+ case PA_CHANNEL_POSITION_AUX17: break;
+ case PA_CHANNEL_POSITION_AUX18: break;
+ case PA_CHANNEL_POSITION_AUX19: break;
+ case PA_CHANNEL_POSITION_AUX20: break;
+ case PA_CHANNEL_POSITION_AUX21: break;
+ case PA_CHANNEL_POSITION_AUX22: break;
+ case PA_CHANNEL_POSITION_AUX23: break;
+ case PA_CHANNEL_POSITION_AUX24: break;
+ case PA_CHANNEL_POSITION_AUX25: break;
+ case PA_CHANNEL_POSITION_AUX26: break;
+ case PA_CHANNEL_POSITION_AUX27: break;
+ case PA_CHANNEL_POSITION_AUX28: break;
+ case PA_CHANNEL_POSITION_AUX29: break;
+ case PA_CHANNEL_POSITION_AUX30: break;
+ case PA_CHANNEL_POSITION_AUX31: break;
+ case PA_CHANNEL_POSITION_TOP_CENTER: break;
+ case PA_CHANNEL_POSITION_TOP_FRONT_LEFT: return UpperFrontLeft;
+ case PA_CHANNEL_POSITION_TOP_FRONT_RIGHT: return UpperFrontRight;
+ case PA_CHANNEL_POSITION_TOP_FRONT_CENTER: break;
+ case PA_CHANNEL_POSITION_TOP_REAR_LEFT: return UpperBackLeft;
+ case PA_CHANNEL_POSITION_TOP_REAR_RIGHT: return UpperBackRight;
+ case PA_CHANNEL_POSITION_TOP_REAR_CENTER: break;
+ case PA_CHANNEL_POSITION_MAX: break;
+ }
+ throw al::backend_exception{ALC_INVALID_VALUE, "Unexpected channel enum %d", chan};
+}
+
+void SetChannelOrderFromMap(ALCdevice *device, const pa_channel_map &chanmap)
+{
+ device->RealOut.ChannelIndex.fill(-1);
+ for(int i{0};i < chanmap.channels;++i)
+ device->RealOut.ChannelIndex[ChannelFromPulse(chanmap.map[i])] = i;
+}
+
+
+/* *grumble* Don't use enums for bitflags. */
+inline pa_stream_flags_t operator|(pa_stream_flags_t lhs, pa_stream_flags_t rhs)
+{ return pa_stream_flags_t(int(lhs) | int(rhs)); }
+inline pa_stream_flags_t& operator|=(pa_stream_flags_t &lhs, pa_stream_flags_t rhs)
+{
+ lhs = pa_stream_flags_t(int(lhs) | int(rhs));
+ return lhs;
+}
+inline pa_context_flags_t& operator|=(pa_context_flags_t &lhs, pa_context_flags_t rhs)
+{
+ lhs = pa_context_flags_t(int(lhs) | int(rhs));
+ return lhs;
+}
+
+inline pa_stream_flags_t& operator&=(pa_stream_flags_t &lhs, int rhs)
+{
+ lhs = pa_stream_flags_t(int(lhs) & rhs);
+ return lhs;
+}
+
+
+/* Global flags and properties */
+pa_context_flags_t pulse_ctx_flags;
+
+pa_mainloop *pulse_mainloop{nullptr};
+
+std::mutex pulse_lock;
+std::condition_variable pulse_condvar;
+
+int pulse_poll_func(struct pollfd *ufds, unsigned long nfds, int timeout, void *userdata)
+{
+ auto plock = static_cast<std::unique_lock<std::mutex>*>(userdata);
+ plock->unlock();
+ int r{poll(ufds, nfds, timeout)};
+ plock->lock();
+ return r;
+}
+
+int pulse_mainloop_thread()
+{
+ SetRTPriority();
+
+ std::unique_lock<std::mutex> plock{pulse_lock};
+ pulse_mainloop = pa_mainloop_new();
+
+ pa_mainloop_set_poll_func(pulse_mainloop, pulse_poll_func, &plock);
+ pulse_condvar.notify_all();
+
+ int ret{};
+ pa_mainloop_run(pulse_mainloop, &ret);
+
+ pa_mainloop_free(pulse_mainloop);
+ pulse_mainloop = nullptr;
+
+ return ret;
+}
+
+
+/* PulseAudio Event Callbacks */
+void context_state_callback(pa_context *context, void* /*pdata*/)
+{
+ pa_context_state_t state{pa_context_get_state(context)};
+ if(state == PA_CONTEXT_READY || !PA_CONTEXT_IS_GOOD(state))
+ pulse_condvar.notify_all();
+}
+
+void stream_state_callback(pa_stream *stream, void* /*pdata*/)
+{
+ pa_stream_state_t state{pa_stream_get_state(stream)};
+ if(state == PA_STREAM_READY || !PA_STREAM_IS_GOOD(state))
+ pulse_condvar.notify_all();
+}
+
+void stream_success_callback(pa_stream* /*stream*/, int /*success*/, void* /*pdata*/)
+{
+ pulse_condvar.notify_all();
+}
+
+void wait_for_operation(pa_operation *op, std::unique_lock<std::mutex> &plock)
+{
+ if(op)
+ {
+ while(pa_operation_get_state(op) == PA_OPERATION_RUNNING)
+ pulse_condvar.wait(plock);
+ pa_operation_unref(op);
+ }
+}
+
+
+pa_context *connect_context(std::unique_lock<std::mutex> &plock)
+{
+ const char *name{"OpenAL Soft"};
+
+ const PathNamePair &binname = GetProcBinary();
+ if(!binname.fname.empty())
+ name = binname.fname.c_str();
+
+ if(UNLIKELY(!pulse_mainloop))
+ {
+ std::thread{pulse_mainloop_thread}.detach();
+ while(!pulse_mainloop)
+ pulse_condvar.wait(plock);
+ }
+
+ pa_context *context{pa_context_new(pa_mainloop_get_api(pulse_mainloop), name)};
+ if(!context) throw al::backend_exception{ALC_OUT_OF_MEMORY, "pa_context_new() failed"};
+
+ pa_context_set_state_callback(context, context_state_callback, nullptr);
+
+ 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;
+ }
+
+ pulse_condvar.wait(plock);
+ }
+ }
+ pa_context_set_state_callback(context, nullptr, nullptr);
+
+ if(err < 0)
+ {
+ pa_context_unref(context);
+ throw al::backend_exception{ALC_INVALID_VALUE, "Context did not connect (%s)",
+ pa_strerror(err)};
+ }
+
+ return context;
+}
+
+
+void pulse_close(pa_context *context, pa_stream *stream)
+{
+ std::lock_guard<std::mutex> _{pulse_lock};
+ 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);
+}
+
+
+struct DevMap {
+ std::string name;
+ std::string device_name;
+};
+
+bool checkName(const al::vector<DevMap> &list, const std::string &name)
+{
+ return std::find_if(list.cbegin(), list.cend(),
+ [&name](const DevMap &entry) -> bool
+ { return entry.name == name; }
+ ) != list.cend();
+}
+
+al::vector<DevMap> PlaybackDevices;
+al::vector<DevMap> CaptureDevices;
+
+
+pa_stream *pulse_connect_stream(const char *device_name, std::unique_lock<std::mutex> &plock,
+ 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{ALC_OUT_OF_MEMORY, "pa_stream_new() failed (%s)",
+ pa_strerror(pa_context_errno(context))};
+
+ pa_stream_set_state_callback(stream, stream_state_callback, nullptr);
+
+ 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{ALC_INVALID_VALUE, "%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))
+ {
+ int err{pa_context_errno(context)};
+ pa_stream_unref(stream);
+ throw al::backend_exception{ALC_INVALID_VALUE, "%s did not get ready (%s)", stream_id,
+ pa_strerror(err)};
+ }
+
+ pulse_condvar.wait(plock);
+ }
+ pa_stream_set_state_callback(stream, nullptr, nullptr);
+
+ return stream;
+}
+
+
+void device_sink_callback(pa_context*, const pa_sink_info *info, int eol, void*)
+{
+ if(eol)
+ {
+ pulse_condvar.notify_all();
+ return;
+ }
+
+ /* Skip this device is if it's already in the list. */
+ if(std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+ [info](const DevMap &entry) -> bool
+ { return entry.device_name == info->name; }
+ ) != 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 probePlaybackDevices()
+{
+ PlaybackDevices.clear();
+
+ try {
+ std::unique_lock<std::mutex> plock{pulse_lock};
+
+ pa_context *context{connect_context(plock)};
+
+ const pa_stream_flags_t flags{PA_STREAM_FIX_FORMAT | PA_STREAM_FIX_RATE |
+ PA_STREAM_FIX_CHANNELS | PA_STREAM_DONT_MOVE};
+
+ pa_sample_spec spec{};
+ spec.format = PA_SAMPLE_S16NE;
+ spec.rate = 44100;
+ spec.channels = 2;
+
+ pa_stream *stream{pulse_connect_stream(nullptr, plock, context, flags, nullptr, &spec,
+ nullptr, BackendType::Playback)};
+ pa_operation *op{pa_context_get_sink_info_by_name(context,
+ pa_stream_get_device_name(stream), device_sink_callback, nullptr)};
+ wait_for_operation(op, plock);
+
+ pa_stream_disconnect(stream);
+ pa_stream_unref(stream);
+ stream = nullptr;
+
+ op = pa_context_get_sink_info_list(context, device_sink_callback, nullptr);
+ wait_for_operation(op, plock);
+
+ pa_context_disconnect(context);
+ pa_context_unref(context);
+ }
+ catch(std::exception &e) {
+ ERR("Error enumerating devices: %s\n", e.what());
+ }
+}
+
+
+void device_source_callback(pa_context*, const pa_source_info *info, int eol, void*)
+{
+ if(eol)
+ {
+ pulse_condvar.notify_all();
+ return;
+ }
+
+ /* Skip this device is if it's already in the list. */
+ if(std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+ [info](const DevMap &entry) -> bool
+ { return entry.device_name == info->name; }
+ ) != 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 probeCaptureDevices()
+{
+ CaptureDevices.clear();
+
+ try {
+ std::unique_lock<std::mutex> plock{pulse_lock};
+
+ pa_context *context{connect_context(plock)};
+
+ const pa_stream_flags_t flags{PA_STREAM_FIX_FORMAT | PA_STREAM_FIX_RATE |
+ PA_STREAM_FIX_CHANNELS | PA_STREAM_DONT_MOVE};
+
+ pa_sample_spec spec{};
+ spec.format = PA_SAMPLE_S16NE;
+ spec.rate = 44100;
+ spec.channels = 1;
+
+ pa_stream *stream{pulse_connect_stream(nullptr, plock, context, flags, nullptr, &spec, nullptr,
+ BackendType::Capture)};
+ pa_operation *op{pa_context_get_source_info_by_name(context,
+ pa_stream_get_device_name(stream), device_source_callback, nullptr)};
+ wait_for_operation(op, plock);
+
+ pa_stream_disconnect(stream);
+ pa_stream_unref(stream);
+ stream = nullptr;
+
+ op = pa_context_get_source_info_list(context, device_source_callback, nullptr);
+ wait_for_operation(op, plock);
+
+ pa_context_disconnect(context);
+ pa_context_unref(context);
+ }
+ catch(std::exception &e) {
+ ERR("Error enumerating devices: %s\n", e.what());
+ }
+}
+
+
+struct PulsePlayback final : public BackendBase {
+ PulsePlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~PulsePlayback() override;
+
+ static void bufferAttrCallbackC(pa_stream *stream, void *pdata);
+ void bufferAttrCallback(pa_stream *stream);
+
+ static void contextStateCallbackC(pa_context *context, void *pdata);
+ void contextStateCallback(pa_context *context);
+
+ static void streamStateCallbackC(pa_stream *stream, void *pdata);
+ void streamStateCallback(pa_stream *stream);
+
+ static void streamWriteCallbackC(pa_stream *stream, size_t nbytes, void *pdata);
+ void streamWriteCallback(pa_stream *stream, size_t nbytes);
+
+ static void sinkInfoCallbackC(pa_context *context, const pa_sink_info *info, int eol, void *pdata);
+ void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int eol);
+
+ static void sinkNameCallbackC(pa_context *context, const pa_sink_info *info, int eol, void *pdata);
+ void sinkNameCallback(pa_context *context, const pa_sink_info *info, int eol);
+
+ static void streamMovedCallbackC(pa_stream *stream, void *pdata);
+ void streamMovedCallback(pa_stream *stream);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+ ClockLatency getClockLatency() override;
+ void lock() override;
+ void unlock() override;
+
+ std::string mDeviceName;
+
+ pa_buffer_attr mAttr;
+ pa_sample_spec mSpec;
+
+ pa_stream *mStream{nullptr};
+ pa_context *mContext{nullptr};
+
+ ALuint mFrameSize{0u};
+
+ DEF_NEWDEL(PulsePlayback)
+};
+
+PulsePlayback::~PulsePlayback()
+{
+ if(!mContext)
+ return;
+
+ pulse_close(mContext, mStream);
+ mContext = nullptr;
+ mStream = nullptr;
+}
+
+
+void PulsePlayback::bufferAttrCallbackC(pa_stream *stream, void *pdata)
+{ static_cast<PulsePlayback*>(pdata)->bufferAttrCallback(stream); }
+
+void PulsePlayback::bufferAttrCallback(pa_stream *stream)
+{
+ /* 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::contextStateCallbackC(pa_context *context, void *pdata)
+{ static_cast<PulsePlayback*>(pdata)->contextStateCallback(context); }
+
+void PulsePlayback::contextStateCallback(pa_context *context)
+{
+ if(pa_context_get_state(context) == PA_CONTEXT_FAILED)
+ {
+ ERR("Received context failure!\n");
+ aluHandleDisconnect(mDevice, "Playback state failure");
+ }
+ pulse_condvar.notify_all();
+}
+
+void PulsePlayback::streamStateCallbackC(pa_stream *stream, void *pdata)
+{ static_cast<PulsePlayback*>(pdata)->streamStateCallback(stream); }
+
+void PulsePlayback::streamStateCallback(pa_stream *stream)
+{
+ if(pa_stream_get_state(stream) == PA_STREAM_FAILED)
+ {
+ ERR("Received stream failure!\n");
+ aluHandleDisconnect(mDevice, "Playback stream failure");
+ }
+ pulse_condvar.notify_all();
+}
+
+void PulsePlayback::streamWriteCallbackC(pa_stream *stream, size_t nbytes, void *pdata)
+{ static_cast<PulsePlayback*>(pdata)->streamWriteCallback(stream, nbytes); }
+
+void PulsePlayback::streamWriteCallback(pa_stream *stream, size_t nbytes)
+{
+ void *buf{pa_xmalloc(nbytes)};
+ aluMixData(mDevice, buf, nbytes/mFrameSize);
+
+ int ret{pa_stream_write(stream, buf, nbytes, pa_xfree, 0, PA_SEEK_RELATIVE)};
+ if(UNLIKELY(ret != PA_OK))
+ ERR("Failed to write to stream: %d, %s\n", ret, pa_strerror(ret));
+}
+
+void PulsePlayback::sinkInfoCallbackC(pa_context *context, const pa_sink_info *info, int eol, void *pdata)
+{ static_cast<PulsePlayback*>(pdata)->sinkInfoCallback(context, info, eol); }
+
+void PulsePlayback::sinkInfoCallback(pa_context*, const pa_sink_info *info, int eol)
+{
+ struct ChannelMap {
+ DevFmtChannels chans;
+ pa_channel_map map;
+ };
+ static constexpr std::array<ChannelMap,7> chanmaps{{
+ { DevFmtX71, X71ChanMap },
+ { DevFmtX61, X61ChanMap },
+ { DevFmtX51, X51ChanMap },
+ { DevFmtX51Rear, X51RearChanMap },
+ { DevFmtQuad, QuadChanMap },
+ { DevFmtStereo, StereoChanMap },
+ { DevFmtMono, MonoChanMap }
+ }};
+
+ if(eol)
+ {
+ pulse_condvar.notify_all();
+ return;
+ }
+
+ auto chanmap = std::find_if(chanmaps.cbegin(), chanmaps.cend(),
+ [info](const ChannelMap &chanmap) -> bool
+ { return pa_channel_map_superset(&info->channel_map, &chanmap.map); }
+ );
+ if(chanmap != chanmaps.cend())
+ {
+ if(!mDevice->Flags.get<ChannelsRequest>())
+ mDevice->FmtChans = chanmap->chans;
+ }
+ else
+ {
+ 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->IsHeadphones = (mDevice->FmtChans == DevFmtStereo &&
+ info->active_port && strcmp(info->active_port->name, "analog-output-headphones") == 0);
+}
+
+void PulsePlayback::sinkNameCallbackC(pa_context *context, const pa_sink_info *info, int eol, void *pdata)
+{ static_cast<PulsePlayback*>(pdata)->sinkNameCallback(context, info, eol); }
+
+void PulsePlayback::sinkNameCallback(pa_context*, const pa_sink_info *info, int eol)
+{
+ if(eol)
+ {
+ pulse_condvar.notify_all();
+ return;
+ }
+ mDevice->DeviceName = info->description;
+}
+
+void PulsePlayback::streamMovedCallbackC(pa_stream *stream, void *pdata)
+{ static_cast<PulsePlayback*>(pdata)->streamMovedCallback(stream); }
+
+void PulsePlayback::streamMovedCallback(pa_stream *stream)
+{
+ mDeviceName = pa_stream_get_device_name(stream);
+ TRACE("Stream moved to %s\n", mDeviceName.c_str());
+}
+
+
+ALCenum PulsePlayback::open(const ALCchar *name)
+{
+ const char *pulse_name{nullptr};
+ const char *dev_name{nullptr};
+
+ if(name)
+ {
+ if(PlaybackDevices.empty())
+ 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{ALC_INVALID_VALUE, "Device name \"%s\" not found", name};
+ pulse_name = iter->device_name.c_str();
+ dev_name = iter->name.c_str();
+ }
+
+ std::unique_lock<std::mutex> plock{pulse_lock};
+
+ mContext = connect_context(plock);
+ pa_context_set_state_callback(mContext, &PulsePlayback::contextStateCallbackC, this);
+
+ pa_stream_flags_t flags{PA_STREAM_FIX_FORMAT | PA_STREAM_FIX_RATE | PA_STREAM_FIX_CHANNELS};
+ if(!GetConfigValueBool(nullptr, "pulse", "allow-moves", 1))
+ flags |= PA_STREAM_DONT_MOVE;
+
+ pa_sample_spec spec{};
+ spec.format = PA_SAMPLE_S16NE;
+ spec.rate = 44100;
+ spec.channels = 2;
+
+ if(!pulse_name)
+ {
+ pulse_name = getenv("ALSOFT_PULSE_DEFAULT");
+ if(pulse_name && !pulse_name[0]) pulse_name = nullptr;
+ }
+ TRACE("Connecting to \"%s\"\n", pulse_name ? pulse_name : "(default)");
+ mStream = pulse_connect_stream(pulse_name, plock, mContext, flags, nullptr, &spec, nullptr,
+ BackendType::Playback);
+
+ pa_stream_set_moved_callback(mStream, &PulsePlayback::streamMovedCallbackC, this);
+ mFrameSize = pa_frame_size(pa_stream_get_sample_spec(mStream));
+
+ mDeviceName = pa_stream_get_device_name(mStream);
+ if(!dev_name)
+ {
+ pa_operation *op{pa_context_get_sink_info_by_name(mContext, mDeviceName.c_str(),
+ &PulsePlayback::sinkNameCallbackC, this)};
+ wait_for_operation(op, plock);
+ }
+ else
+ mDevice->DeviceName = dev_name;
+
+ return ALC_NO_ERROR;
+}
+
+ALCboolean PulsePlayback::reset()
+{
+ std::unique_lock<std::mutex> plock{pulse_lock};
+
+ 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;
+ }
+
+ pa_operation *op{pa_context_get_sink_info_by_name(mContext, mDeviceName.c_str(),
+ &PulsePlayback::sinkInfoCallbackC, this)};
+ wait_for_operation(op, plock);
+
+ 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", 1))
+ flags |= PA_STREAM_DONT_MOVE;
+ if(GetConfigValueBool(mDevice->DeviceName.c_str(), "pulse", "adjust-latency", 0))
+ {
+ /* 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", 0) ||
+ !mDevice->Flags.get<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 = X51ChanMap;
+ break;
+ case DevFmtX51Rear:
+ chanmap = X51RearChanMap;
+ break;
+ case DevFmtX61:
+ chanmap = X61ChanMap;
+ break;
+ case DevFmtX71:
+ chanmap = X71ChanMap;
+ break;
+ }
+ SetChannelOrderFromMap(mDevice, chanmap);
+
+ 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 = mDevice->channelsFromFmt();
+ if(pa_sample_spec_valid(&mSpec) == 0)
+ throw al::backend_exception{ALC_INVALID_VALUE, "Invalid sample spec"};
+
+ mAttr.maxlength = -1;
+ mAttr.tlength = mDevice->BufferSize * pa_frame_size(&mSpec);
+ mAttr.prebuf = 0;
+ mAttr.minreq = mDevice->UpdateSize * pa_frame_size(&mSpec);
+ mAttr.fragsize = -1;
+
+ mStream = pulse_connect_stream(mDeviceName.c_str(), plock, mContext, flags, &mAttr, &mSpec,
+ &chanmap, BackendType::Playback);
+
+ pa_stream_set_state_callback(mStream, &PulsePlayback::streamStateCallbackC, this);
+ pa_stream_set_moved_callback(mStream, &PulsePlayback::streamMovedCallbackC, this);
+
+ mSpec = *(pa_stream_get_sample_spec(mStream));
+ mFrameSize = 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 ALuint perlen{static_cast<ALuint>(clampd(scale*mDevice->UpdateSize + 0.5, 64.0,
+ 8192.0))};
+ const ALuint buflen{static_cast<ALuint>(clampd(scale*mDevice->BufferSize + 0.5, perlen*2,
+ std::numeric_limits<int>::max()/mFrameSize))};
+
+ mAttr.maxlength = -1;
+ mAttr.tlength = buflen * mFrameSize;
+ mAttr.prebuf = 0;
+ mAttr.minreq = perlen * mFrameSize;
+
+ op = pa_stream_set_buffer_attr(mStream, &mAttr, stream_success_callback, nullptr);
+ wait_for_operation(op, plock);
+
+ mDevice->Frequency = mSpec.rate;
+ }
+
+ pa_stream_set_buffer_attr_callback(mStream, &PulsePlayback::bufferAttrCallbackC, this);
+ bufferAttrCallback(mStream);
+
+ mDevice->BufferSize = mAttr.tlength / mFrameSize;
+ mDevice->UpdateSize = mAttr.minreq / mFrameSize;
+
+ /* HACK: prebuf should be 0 as that's what we set it to. However on some
+ * systems it comes back as non-0, so we have to make sure the device will
+ * write enough audio to start playback. The lack of manual start control
+ * may have unintended consequences, but it's better than not starting at
+ * all.
+ */
+ if(mAttr.prebuf != 0)
+ {
+ ALuint len{mAttr.prebuf / mFrameSize};
+ if(len <= mDevice->BufferSize)
+ ERR("Non-0 prebuf, %u samples (%u bytes), device has %u samples\n",
+ len, mAttr.prebuf, mDevice->BufferSize);
+ }
+
+ return ALC_TRUE;
+}
+
+ALCboolean PulsePlayback::start()
+{
+ std::unique_lock<std::mutex> plock{pulse_lock};
+
+ pa_stream_set_write_callback(mStream, &PulsePlayback::streamWriteCallbackC, this);
+ pa_operation *op{pa_stream_cork(mStream, 0, stream_success_callback, nullptr)};
+ wait_for_operation(op, plock);
+
+ return ALC_TRUE;
+}
+
+void PulsePlayback::stop()
+{
+ std::unique_lock<std::mutex> plock{pulse_lock};
+
+ pa_stream_set_write_callback(mStream, nullptr, nullptr);
+ pa_operation *op{pa_stream_cork(mStream, 1, stream_success_callback, nullptr)};
+ wait_for_operation(op, plock);
+}
+
+
+ClockLatency PulsePlayback::getClockLatency()
+{
+ ClockLatency ret;
+ pa_usec_t latency;
+ int neg, err;
+
+ { std::lock_guard<std::mutex> _{pulse_lock};
+ ret.ClockTime = GetDeviceClockTime(mDevice);
+ err = pa_stream_get_latency(mStream, &latency, &neg);
+ }
+
+ if(UNLIKELY(err != 0))
+ {
+ /* FIXME: 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. Should we wait, possibly stalling the app, or give a
+ * dummy value? Either way, it shouldn't be 0. */
+ if(err != -PA_ERR_NODATA)
+ ERR("Failed to get stream latency: 0x%x\n", err);
+ latency = 0;
+ neg = 0;
+ }
+ else if(UNLIKELY(neg))
+ latency = 0;
+ ret.Latency = std::chrono::microseconds{latency};
+
+ return ret;
+}
+
+
+void PulsePlayback::lock()
+{ pulse_lock.lock(); }
+
+void PulsePlayback::unlock()
+{ pulse_lock.unlock(); }
+
+
+struct PulseCapture final : public BackendBase {
+ PulseCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~PulseCapture() override;
+
+ static void contextStateCallbackC(pa_context *context, void *pdata);
+ void contextStateCallback(pa_context *context);
+
+ static void streamStateCallbackC(pa_stream *stream, void *pdata);
+ void streamStateCallback(pa_stream *stream);
+
+ static void sourceNameCallbackC(pa_context *context, const pa_source_info *info, int eol, void *pdata);
+ void sourceNameCallback(pa_context *context, const pa_source_info *info, int eol);
+
+ static void streamMovedCallbackC(pa_stream *stream, void *pdata);
+ void streamMovedCallback(pa_stream *stream);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(ALCvoid *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+ ClockLatency getClockLatency() override;
+ void lock() override;
+ void unlock() override;
+
+ std::string mDeviceName;
+
+ ALCuint mLastReadable{0u};
+ al::byte mSilentVal{};
+
+ al::span<const al::byte> mCapBuffer;
+ ssize_t mCapLen{0};
+
+ pa_buffer_attr mAttr{};
+ pa_sample_spec mSpec{};
+
+ pa_stream *mStream{nullptr};
+ pa_context *mContext{nullptr};
+
+ DEF_NEWDEL(PulseCapture)
+};
+
+PulseCapture::~PulseCapture()
+{
+ if(!mContext)
+ return;
+
+ pulse_close(mContext, mStream);
+ mContext = nullptr;
+ mStream = nullptr;
+}
+
+void PulseCapture::contextStateCallbackC(pa_context *context, void *pdata)
+{ static_cast<PulseCapture*>(pdata)->contextStateCallback(context); }
+
+void PulseCapture::contextStateCallback(pa_context *context)
+{
+ if(pa_context_get_state(context) == PA_CONTEXT_FAILED)
+ {
+ ERR("Received context failure!\n");
+ aluHandleDisconnect(mDevice, "Capture state failure");
+ }
+ pulse_condvar.notify_all();
+}
+
+void PulseCapture::streamStateCallbackC(pa_stream *stream, void *pdata)
+{ static_cast<PulseCapture*>(pdata)->streamStateCallback(stream); }
+
+void PulseCapture::streamStateCallback(pa_stream *stream)
+{
+ if(pa_stream_get_state(stream) == PA_STREAM_FAILED)
+ {
+ ERR("Received stream failure!\n");
+ aluHandleDisconnect(mDevice, "Capture stream failure");
+ }
+ pulse_condvar.notify_all();
+}
+
+void PulseCapture::sourceNameCallbackC(pa_context *context, const pa_source_info *info, int eol, void *pdata)
+{ static_cast<PulseCapture*>(pdata)->sourceNameCallback(context, info, eol); }
+
+void PulseCapture::sourceNameCallback(pa_context*, const pa_source_info *info, int eol)
+{
+ if(eol)
+ {
+ pulse_condvar.notify_all();
+ return;
+ }
+ mDevice->DeviceName = info->description;
+}
+
+void PulseCapture::streamMovedCallbackC(pa_stream *stream, void *pdata)
+{ static_cast<PulseCapture*>(pdata)->streamMovedCallback(stream); }
+
+void PulseCapture::streamMovedCallback(pa_stream *stream)
+{
+ mDeviceName = pa_stream_get_device_name(stream);
+ TRACE("Stream moved to %s\n", mDeviceName.c_str());
+}
+
+
+ALCenum PulseCapture::open(const ALCchar *name)
+{
+ const char *pulse_name{nullptr};
+ if(name)
+ {
+ if(CaptureDevices.empty())
+ 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{ALC_INVALID_VALUE, "Device name \"%s\" not found", name};
+ pulse_name = iter->device_name.c_str();
+ mDevice->DeviceName = iter->name;
+ }
+
+ std::unique_lock<std::mutex> plock{pulse_lock};
+
+ mContext = connect_context(plock);
+ pa_context_set_state_callback(mContext, &PulseCapture::contextStateCallbackC, this);
+
+ 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 DevFmtX51Rear:
+ chanmap = X51RearChanMap;
+ break;
+ case DevFmtX61:
+ chanmap = X61ChanMap;
+ break;
+ case DevFmtX71:
+ chanmap = X71ChanMap;
+ break;
+ case DevFmtAmbi3D:
+ throw al::backend_exception{ALC_INVALID_VALUE, "%s capture samples not supported",
+ DevFmtChannelsString(mDevice->FmtChans)};
+ }
+ SetChannelOrderFromMap(mDevice, chanmap);
+
+ 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{ALC_INVALID_VALUE, "%s capture samples not supported",
+ DevFmtTypeString(mDevice->FmtType)};
+ }
+ mSpec.rate = mDevice->Frequency;
+ mSpec.channels = mDevice->channelsFromFmt();
+ if(pa_sample_spec_valid(&mSpec) == 0)
+ throw al::backend_exception{ALC_INVALID_VALUE, "Invalid sample format"};
+
+ ALuint samples{mDevice->BufferSize};
+ samples = maxu(samples, 100 * mDevice->Frequency / 1000);
+
+ mAttr.minreq = -1;
+ mAttr.prebuf = -1;
+ mAttr.maxlength = samples * pa_frame_size(&mSpec);
+ mAttr.tlength = -1;
+ mAttr.fragsize = minu(samples, 50*mDevice->Frequency/1000) * pa_frame_size(&mSpec);
+
+ pa_stream_flags_t flags{PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY};
+ if(!GetConfigValueBool(nullptr, "pulse", "allow-moves", 1))
+ flags |= PA_STREAM_DONT_MOVE;
+
+ TRACE("Connecting to \"%s\"\n", pulse_name ? pulse_name : "(default)");
+ mStream = pulse_connect_stream(pulse_name, plock, mContext, flags, &mAttr, &mSpec, &chanmap,
+ BackendType::Capture);
+
+ pa_stream_set_moved_callback(mStream, &PulseCapture::streamMovedCallbackC, this);
+ pa_stream_set_state_callback(mStream, &PulseCapture::streamStateCallbackC, this);
+
+ mDeviceName = pa_stream_get_device_name(mStream);
+ if(mDevice->DeviceName.empty())
+ {
+ pa_operation *op{pa_context_get_source_info_by_name(mContext, mDeviceName.c_str(),
+ &PulseCapture::sourceNameCallbackC, this)};
+ wait_for_operation(op, plock);
+ }
+
+ return ALC_NO_ERROR;
+}
+
+ALCboolean PulseCapture::start()
+{
+ std::unique_lock<std::mutex> plock{pulse_lock};
+ pa_operation *op{pa_stream_cork(mStream, 0, stream_success_callback, nullptr)};
+ wait_for_operation(op, plock);
+ return ALC_TRUE;
+}
+
+void PulseCapture::stop()
+{
+ std::unique_lock<std::mutex> plock{pulse_lock};
+ pa_operation *op{pa_stream_cork(mStream, 1, stream_success_callback, nullptr)};
+ wait_for_operation(op, plock);
+}
+
+ALCenum PulseCapture::captureSamples(ALCvoid *buffer, ALCuint samples)
+{
+ al::span<al::byte> dstbuf{static_cast<al::byte*>(buffer), samples * pa_frame_size(&mSpec)};
+
+ /* Capture is done in fragment-sized chunks, so we loop until we get all
+ * that's available */
+ mLastReadable -= dstbuf.size();
+ std::lock_guard<std::mutex> _{pulse_lock};
+ while(!dstbuf.empty())
+ {
+ if(mCapBuffer.empty())
+ {
+ if(UNLIKELY(!mDevice->Connected.load(std::memory_order_acquire)))
+ break;
+ const pa_stream_state_t state{pa_stream_get_state(mStream)};
+ if(UNLIKELY(!PA_STREAM_IS_GOOD(state)))
+ {
+ aluHandleDisconnect(mDevice, "Bad capture state: %u", state);
+ break;
+ }
+ const void *capbuf;
+ size_t caplen;
+ if(UNLIKELY(pa_stream_peek(mStream, &capbuf, &caplen) < 0))
+ {
+ aluHandleDisconnect(mDevice, "Failed retrieving capture samples: %s",
+ pa_strerror(pa_context_errno(mContext)));
+ break;
+ }
+ if(caplen == 0) break;
+ if(UNLIKELY(!capbuf))
+ mCapLen = -static_cast<ssize_t>(caplen);
+ else
+ mCapLen = static_cast<ssize_t>(caplen);
+ mCapBuffer = {static_cast<const al::byte*>(capbuf), caplen};
+ }
+
+ const size_t rem{minz(dstbuf.size(), mCapBuffer.size())};
+ if(UNLIKELY(mCapLen < 0))
+ std::fill_n(dstbuf.begin(), rem, mSilentVal);
+ else
+ std::copy_n(mCapBuffer.begin(), rem, dstbuf.begin());
+ dstbuf = dstbuf.subspan(rem);
+ mCapBuffer = mCapBuffer.subspan(rem);
+
+ if(mCapBuffer.empty())
+ {
+ pa_stream_drop(mStream);
+ mCapLen = 0;
+ }
+ }
+ if(!dstbuf.empty())
+ std::fill(dstbuf.begin(), dstbuf.end(), mSilentVal);
+
+ return ALC_NO_ERROR;
+}
+
+ALCuint PulseCapture::availableSamples()
+{
+ size_t readable{mCapBuffer.size()};
+
+ if(mDevice->Connected.load(std::memory_order_acquire))
+ {
+ std::lock_guard<std::mutex> _{pulse_lock};
+ size_t got{pa_stream_readable_size(mStream)};
+ if(static_cast<ssize_t>(got) < 0)
+ {
+ ERR("pa_stream_readable_size() failed: %s\n", pa_strerror(got));
+ aluHandleDisconnect(mDevice, "Failed getting readable size: %s", pa_strerror(got));
+ }
+ else
+ {
+ const auto caplen = static_cast<size_t>(std::abs(mCapLen));
+ if(got > caplen) readable += got - caplen;
+ }
+ }
+
+ readable = std::min<size_t>(readable, std::numeric_limits<ALCuint>::max());
+ mLastReadable = std::max(mLastReadable, static_cast<ALCuint>(readable));
+ return mLastReadable / pa_frame_size(&mSpec);
+}
+
+
+ClockLatency PulseCapture::getClockLatency()
+{
+ ClockLatency ret;
+ pa_usec_t latency;
+ int neg, err;
+
+ { std::lock_guard<std::mutex> _{pulse_lock};
+ ret.ClockTime = GetDeviceClockTime(mDevice);
+ err = pa_stream_get_latency(mStream, &latency, &neg);
+ }
+
+ if(UNLIKELY(err != 0))
+ {
+ ERR("Failed to get stream latency: 0x%x\n", err);
+ latency = 0;
+ neg = 0;
+ }
+ else if(UNLIKELY(neg))
+ latency = 0;
+ ret.Latency = std::chrono::microseconds{latency};
+
+ return ret;
+}
+
+
+void PulseCapture::lock()
+{ pulse_lock.lock(); }
+
+void PulseCapture::unlock()
+{ pulse_lock.unlock(); }
+
+} // 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", 1))
+ pulse_ctx_flags |= PA_CONTEXT_NOAUTOSPAWN;
+
+ try {
+ std::unique_lock<std::mutex> plock{pulse_lock};
+ pa_context *context{connect_context(plock)};
+ 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; }
+
+void PulseBackendFactory::probe(DevProbe 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 DevProbe::Playback:
+ probePlaybackDevices();
+ std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+ break;
+
+ case DevProbe::Capture:
+ probeCaptureDevices();
+ std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+ break;
+ }
+}
+
+BackendPtr PulseBackendFactory::createBackend(ALCdevice *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
index 00000000..40f3e305
--- /dev/null
+++ b/alc/backends/pulseaudio.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_PULSEAUDIO_H
+#define BACKENDS_PULSEAUDIO_H
+
+#include "backends/base.h"
+
+class PulseBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+
+ static BackendFactory &getFactory();
+};
+
+#endif /* BACKENDS_PULSEAUDIO_H */
diff --git a/alc/backends/qsa.cpp b/alc/backends/qsa.cpp
new file mode 100644
index 00000000..64ed53aa
--- /dev/null
+++ b/alc/backends/qsa.cpp
@@ -0,0 +1,953 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2011-2013 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 "backends/qsa.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <sched.h>
+#include <errno.h>
+#include <memory.h>
+#include <poll.h>
+
+#include <thread>
+#include <memory>
+#include <algorithm>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "threads.h"
+
+#include <sys/asoundlib.h>
+#include <sys/neutrino.h>
+
+
+namespace {
+
+struct qsa_data {
+ snd_pcm_t* pcmHandle{nullptr};
+ int audio_fd{-1};
+
+ snd_pcm_channel_setup_t csetup{};
+ snd_pcm_channel_params_t cparams{};
+
+ ALvoid* buffer{nullptr};
+ ALsizei size{0};
+
+ std::atomic<ALenum> mKillNow{AL_TRUE};
+ std::thread mThread;
+};
+
+struct DevMap {
+ ALCchar* name;
+ int card;
+ int dev;
+};
+
+al::vector<DevMap> DeviceNameMap;
+al::vector<DevMap> CaptureNameMap;
+
+constexpr ALCchar qsaDevice[] = "QSA Default";
+
+constexpr struct {
+ int32_t format;
+} formatlist[] = {
+ {SND_PCM_SFMT_FLOAT_LE},
+ {SND_PCM_SFMT_S32_LE},
+ {SND_PCM_SFMT_U32_LE},
+ {SND_PCM_SFMT_S16_LE},
+ {SND_PCM_SFMT_U16_LE},
+ {SND_PCM_SFMT_S8},
+ {SND_PCM_SFMT_U8},
+ {0},
+};
+
+constexpr struct {
+ int32_t rate;
+} ratelist[] = {
+ {192000},
+ {176400},
+ {96000},
+ {88200},
+ {48000},
+ {44100},
+ {32000},
+ {24000},
+ {22050},
+ {16000},
+ {12000},
+ {11025},
+ {8000},
+ {0},
+};
+
+constexpr struct {
+ int32_t channels;
+} channellist[] = {
+ {8},
+ {7},
+ {6},
+ {4},
+ {2},
+ {1},
+ {0},
+};
+
+void deviceList(int type, al::vector<DevMap> *devmap)
+{
+ snd_ctl_t* handle;
+ snd_pcm_info_t pcminfo;
+ int max_cards, card, err, dev;
+ DevMap entry;
+ char name[1024];
+ snd_ctl_hw_info info;
+
+ max_cards = snd_cards();
+ if(max_cards < 0)
+ return;
+
+ std::for_each(devmap->begin(), devmap->end(),
+ [](const DevMap &entry) -> void
+ { free(entry.name); }
+ );
+ devmap->clear();
+
+ entry.name = strdup(qsaDevice);
+ entry.card = 0;
+ entry.dev = 0;
+ devmap->push_back(entry);
+
+ for(card = 0;card < max_cards;card++)
+ {
+ if((err=snd_ctl_open(&handle, card)) < 0)
+ continue;
+
+ if((err=snd_ctl_hw_info(handle, &info)) < 0)
+ {
+ snd_ctl_close(handle);
+ continue;
+ }
+
+ for(dev = 0;dev < (int)info.pcmdevs;dev++)
+ {
+ if((err=snd_ctl_pcm_info(handle, dev, &pcminfo)) < 0)
+ continue;
+
+ if((type==SND_PCM_CHANNEL_PLAYBACK && (pcminfo.flags&SND_PCM_INFO_PLAYBACK)) ||
+ (type==SND_PCM_CHANNEL_CAPTURE && (pcminfo.flags&SND_PCM_INFO_CAPTURE)))
+ {
+ snprintf(name, sizeof(name), "%s [%s] (hw:%d,%d)", info.name, pcminfo.name, card, dev);
+ entry.name = strdup(name);
+ entry.card = card;
+ entry.dev = dev;
+
+ devmap->push_back(entry);
+ TRACE("Got device \"%s\", card %d, dev %d\n", name, card, dev);
+ }
+ }
+ snd_ctl_close(handle);
+ }
+}
+
+
+/* Wrappers to use an old-style backend with the new interface. */
+struct PlaybackWrapper final : public BackendBase {
+ PlaybackWrapper(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~PlaybackWrapper() override;
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ std::unique_ptr<qsa_data> mExtraData;
+
+ DEF_NEWDEL(PlaybackWrapper)
+};
+
+
+FORCE_ALIGN static int qsa_proc_playback(void *ptr)
+{
+ PlaybackWrapper *self = static_cast<PlaybackWrapper*>(ptr);
+ ALCdevice *device = self->mDevice;
+ qsa_data *data = self->mExtraData.get();
+ snd_pcm_channel_status_t status;
+ sched_param param;
+ char* write_ptr;
+ ALint len;
+ int sret;
+
+ SetRTPriority();
+ althrd_setname(MIXER_THREAD_NAME);
+
+ /* Increase default 10 priority to 11 to avoid jerky sound */
+ SchedGet(0, 0, &param);
+ param.sched_priority=param.sched_curpriority+1;
+ SchedSet(0, 0, SCHED_NOCHANGE, &param);
+
+ const ALint frame_size = device->frameSizeFromFmt();
+
+ self->lock();
+ while(!data->mKillNow.load(std::memory_order_acquire))
+ {
+ pollfd pollitem{};
+ pollitem.fd = data->audio_fd;
+ pollitem.events = POLLOUT;
+
+ /* Select also works like time slice to OS */
+ self->unlock();
+ sret = poll(&pollitem, 1, 2000);
+ self->lock();
+ if(sret == -1)
+ {
+ if(errno == EINTR || errno == EAGAIN)
+ continue;
+ ERR("poll error: %s\n", strerror(errno));
+ aluHandleDisconnect(device, "Failed waiting for playback buffer: %s", strerror(errno));
+ break;
+ }
+ if(sret == 0)
+ {
+ ERR("poll timeout\n");
+ continue;
+ }
+
+ len = data->size;
+ write_ptr = static_cast<char*>(data->buffer);
+ aluMixData(device, write_ptr, len/frame_size);
+ while(len>0 && !data->mKillNow.load(std::memory_order_acquire))
+ {
+ int wrote = snd_pcm_plugin_write(data->pcmHandle, write_ptr, len);
+ if(wrote <= 0)
+ {
+ if(errno==EAGAIN || errno==EWOULDBLOCK)
+ continue;
+
+ memset(&status, 0, sizeof(status));
+ status.channel = SND_PCM_CHANNEL_PLAYBACK;
+
+ snd_pcm_plugin_status(data->pcmHandle, &status);
+
+ /* we need to reinitialize the sound channel if we've underrun the buffer */
+ if(status.status == SND_PCM_STATUS_UNDERRUN ||
+ status.status == SND_PCM_STATUS_READY)
+ {
+ if(snd_pcm_plugin_prepare(data->pcmHandle, SND_PCM_CHANNEL_PLAYBACK) < 0)
+ {
+ aluHandleDisconnect(device, "Playback recovery failed");
+ break;
+ }
+ }
+ }
+ else
+ {
+ write_ptr += wrote;
+ len -= wrote;
+ }
+ }
+ }
+ self->unlock();
+
+ return 0;
+}
+
+/************/
+/* Playback */
+/************/
+
+static ALCenum qsa_open_playback(PlaybackWrapper *self, const ALCchar* deviceName)
+{
+ ALCdevice *device = self->mDevice;
+ int card, dev;
+ int status;
+
+ std::unique_ptr<qsa_data> data{new qsa_data{}};
+ data->mKillNow.store(AL_TRUE, std::memory_order_relaxed);
+
+ if(!deviceName)
+ deviceName = qsaDevice;
+
+ if(strcmp(deviceName, qsaDevice) == 0)
+ status = snd_pcm_open_preferred(&data->pcmHandle, &card, &dev, SND_PCM_OPEN_PLAYBACK);
+ else
+ {
+ if(DeviceNameMap.empty())
+ deviceList(SND_PCM_CHANNEL_PLAYBACK, &DeviceNameMap);
+
+ auto iter = std::find_if(DeviceNameMap.begin(), DeviceNameMap.end(),
+ [deviceName](const DevMap &entry) -> bool
+ { return entry.name && strcmp(deviceName, entry.name) == 0; }
+ );
+ if(iter == DeviceNameMap.cend())
+ return ALC_INVALID_DEVICE;
+
+ status = snd_pcm_open(&data->pcmHandle, iter->card, iter->dev, SND_PCM_OPEN_PLAYBACK);
+ }
+
+ if(status < 0)
+ return ALC_INVALID_DEVICE;
+
+ data->audio_fd = snd_pcm_file_descriptor(data->pcmHandle, SND_PCM_CHANNEL_PLAYBACK);
+ if(data->audio_fd < 0)
+ {
+ snd_pcm_close(data->pcmHandle);
+ return ALC_INVALID_DEVICE;
+ }
+
+ device->DeviceName = deviceName;
+ self->mExtraData = std::move(data);
+
+ return ALC_NO_ERROR;
+}
+
+static void qsa_close_playback(PlaybackWrapper *self)
+{
+ qsa_data *data = self->mExtraData.get();
+
+ if (data->buffer!=NULL)
+ {
+ free(data->buffer);
+ data->buffer=NULL;
+ }
+
+ snd_pcm_close(data->pcmHandle);
+
+ self->mExtraData = nullptr;
+}
+
+static ALCboolean qsa_reset_playback(PlaybackWrapper *self)
+{
+ ALCdevice *device = self->mDevice;
+ qsa_data *data = self->mExtraData.get();
+ int32_t format=-1;
+
+ switch(device->FmtType)
+ {
+ case DevFmtByte:
+ format=SND_PCM_SFMT_S8;
+ break;
+ case DevFmtUByte:
+ format=SND_PCM_SFMT_U8;
+ break;
+ case DevFmtShort:
+ format=SND_PCM_SFMT_S16_LE;
+ break;
+ case DevFmtUShort:
+ format=SND_PCM_SFMT_U16_LE;
+ break;
+ case DevFmtInt:
+ format=SND_PCM_SFMT_S32_LE;
+ break;
+ case DevFmtUInt:
+ format=SND_PCM_SFMT_U32_LE;
+ break;
+ case DevFmtFloat:
+ format=SND_PCM_SFMT_FLOAT_LE;
+ break;
+ }
+
+ /* we actually don't want to block on writes */
+ snd_pcm_nonblock_mode(data->pcmHandle, 1);
+ /* Disable mmap to control data transfer to the audio device */
+ snd_pcm_plugin_set_disable(data->pcmHandle, PLUGIN_DISABLE_MMAP);
+ snd_pcm_plugin_set_disable(data->pcmHandle, PLUGIN_DISABLE_BUFFER_PARTIAL_BLOCKS);
+
+ // configure a sound channel
+ memset(&data->cparams, 0, sizeof(data->cparams));
+ data->cparams.channel=SND_PCM_CHANNEL_PLAYBACK;
+ data->cparams.mode=SND_PCM_MODE_BLOCK;
+ data->cparams.start_mode=SND_PCM_START_FULL;
+ data->cparams.stop_mode=SND_PCM_STOP_STOP;
+
+ data->cparams.buf.block.frag_size=device->UpdateSize * device->frameSizeFromFmt();
+ data->cparams.buf.block.frags_max=device->BufferSize / device->UpdateSize;
+ data->cparams.buf.block.frags_min=data->cparams.buf.block.frags_max;
+
+ data->cparams.format.interleave=1;
+ data->cparams.format.rate=device->Frequency;
+ data->cparams.format.voices=device->channelsFromFmt();
+ data->cparams.format.format=format;
+
+ if ((snd_pcm_plugin_params(data->pcmHandle, &data->cparams))<0)
+ {
+ int original_rate=data->cparams.format.rate;
+ int original_voices=data->cparams.format.voices;
+ int original_format=data->cparams.format.format;
+ int it;
+ int jt;
+
+ for (it=0; it<1; it++)
+ {
+ /* Check for second pass */
+ if (it==1)
+ {
+ original_rate=ratelist[0].rate;
+ original_voices=channellist[0].channels;
+ original_format=formatlist[0].format;
+ }
+
+ do {
+ /* At first downgrade sample format */
+ jt=0;
+ do {
+ if (formatlist[jt].format==data->cparams.format.format)
+ {
+ data->cparams.format.format=formatlist[jt+1].format;
+ break;
+ }
+ if (formatlist[jt].format==0)
+ {
+ data->cparams.format.format=0;
+ break;
+ }
+ jt++;
+ } while(1);
+
+ if (data->cparams.format.format==0)
+ {
+ data->cparams.format.format=original_format;
+
+ /* At secod downgrade sample rate */
+ jt=0;
+ do {
+ if (ratelist[jt].rate==data->cparams.format.rate)
+ {
+ data->cparams.format.rate=ratelist[jt+1].rate;
+ break;
+ }
+ if (ratelist[jt].rate==0)
+ {
+ data->cparams.format.rate=0;
+ break;
+ }
+ jt++;
+ } while(1);
+
+ if (data->cparams.format.rate==0)
+ {
+ data->cparams.format.rate=original_rate;
+ data->cparams.format.format=original_format;
+
+ /* At third downgrade channels number */
+ jt=0;
+ do {
+ if(channellist[jt].channels==data->cparams.format.voices)
+ {
+ data->cparams.format.voices=channellist[jt+1].channels;
+ break;
+ }
+ if (channellist[jt].channels==0)
+ {
+ data->cparams.format.voices=0;
+ break;
+ }
+ jt++;
+ } while(1);
+ }
+
+ if (data->cparams.format.voices==0)
+ {
+ break;
+ }
+ }
+
+ data->cparams.buf.block.frag_size=device->UpdateSize*
+ data->cparams.format.voices*
+ snd_pcm_format_width(data->cparams.format.format)/8;
+ data->cparams.buf.block.frags_max=device->NumUpdates;
+ data->cparams.buf.block.frags_min=device->NumUpdates;
+ if ((snd_pcm_plugin_params(data->pcmHandle, &data->cparams))<0)
+ {
+ continue;
+ }
+ else
+ {
+ break;
+ }
+ } while(1);
+
+ if (data->cparams.format.voices!=0)
+ {
+ break;
+ }
+ }
+
+ if (data->cparams.format.voices==0)
+ {
+ return ALC_FALSE;
+ }
+ }
+
+ if ((snd_pcm_plugin_prepare(data->pcmHandle, SND_PCM_CHANNEL_PLAYBACK))<0)
+ {
+ return ALC_FALSE;
+ }
+
+ memset(&data->csetup, 0, sizeof(data->csetup));
+ data->csetup.channel=SND_PCM_CHANNEL_PLAYBACK;
+ if (snd_pcm_plugin_setup(data->pcmHandle, &data->csetup)<0)
+ {
+ return ALC_FALSE;
+ }
+
+ /* now fill back to the our AL device */
+ device->Frequency=data->cparams.format.rate;
+
+ switch (data->cparams.format.voices)
+ {
+ case 1:
+ device->FmtChans=DevFmtMono;
+ break;
+ case 2:
+ device->FmtChans=DevFmtStereo;
+ break;
+ case 4:
+ device->FmtChans=DevFmtQuad;
+ break;
+ case 6:
+ device->FmtChans=DevFmtX51;
+ break;
+ case 7:
+ device->FmtChans=DevFmtX61;
+ break;
+ case 8:
+ device->FmtChans=DevFmtX71;
+ break;
+ default:
+ device->FmtChans=DevFmtMono;
+ break;
+ }
+
+ switch (data->cparams.format.format)
+ {
+ case SND_PCM_SFMT_S8:
+ device->FmtType=DevFmtByte;
+ break;
+ case SND_PCM_SFMT_U8:
+ device->FmtType=DevFmtUByte;
+ break;
+ case SND_PCM_SFMT_S16_LE:
+ device->FmtType=DevFmtShort;
+ break;
+ case SND_PCM_SFMT_U16_LE:
+ device->FmtType=DevFmtUShort;
+ break;
+ case SND_PCM_SFMT_S32_LE:
+ device->FmtType=DevFmtInt;
+ break;
+ case SND_PCM_SFMT_U32_LE:
+ device->FmtType=DevFmtUInt;
+ break;
+ case SND_PCM_SFMT_FLOAT_LE:
+ device->FmtType=DevFmtFloat;
+ break;
+ default:
+ device->FmtType=DevFmtShort;
+ break;
+ }
+
+ SetDefaultChannelOrder(device);
+
+ device->UpdateSize=data->csetup.buf.block.frag_size / device->frameSizeFromFmt();
+ device->NumUpdates=data->csetup.buf.block.frags;
+
+ data->size=data->csetup.buf.block.frag_size;
+ data->buffer=malloc(data->size);
+ if (!data->buffer)
+ {
+ return ALC_FALSE;
+ }
+
+ return ALC_TRUE;
+}
+
+static ALCboolean qsa_start_playback(PlaybackWrapper *self)
+{
+ qsa_data *data = self->mExtraData.get();
+
+ try {
+ data->mKillNow.store(AL_FALSE, std::memory_order_release);
+ data->mThread = std::thread(qsa_proc_playback, self);
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+static void qsa_stop_playback(PlaybackWrapper *self)
+{
+ qsa_data *data = self->mExtraData.get();
+
+ if(data->mKillNow.exchange(AL_TRUE, std::memory_order_acq_rel) || !data->mThread.joinable())
+ return;
+ data->mThread.join();
+}
+
+
+PlaybackWrapper::~PlaybackWrapper()
+{
+ if(mExtraData)
+ qsa_close_playback(this);
+}
+
+ALCenum PlaybackWrapper::open(const ALCchar *name)
+{ return qsa_open_playback(this, name); }
+
+ALCboolean PlaybackWrapper::reset()
+{ return qsa_reset_playback(this); }
+
+ALCboolean PlaybackWrapper::start()
+{ return qsa_start_playback(this); }
+
+void PlaybackWrapper::stop()
+{ qsa_stop_playback(this); }
+
+
+/***********/
+/* Capture */
+/***********/
+
+struct CaptureWrapper final : public BackendBase {
+ CaptureWrapper(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~CaptureWrapper() override;
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(void *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+
+ std::unique_ptr<qsa_data> mExtraData;
+
+ DEF_NEWDEL(CaptureWrapper)
+};
+
+static ALCenum qsa_open_capture(CaptureWrapper *self, const ALCchar *deviceName)
+{
+ ALCdevice *device = self->mDevice;
+ int card, dev;
+ int format=-1;
+ int status;
+
+ std::unique_ptr<qsa_data> data{new qsa_data{}};
+
+ if(!deviceName)
+ deviceName = qsaDevice;
+
+ if(strcmp(deviceName, qsaDevice) == 0)
+ status = snd_pcm_open_preferred(&data->pcmHandle, &card, &dev, SND_PCM_OPEN_CAPTURE);
+ else
+ {
+ if(CaptureNameMap.empty())
+ deviceList(SND_PCM_CHANNEL_CAPTURE, &CaptureNameMap);
+
+ auto iter = std::find_if(CaptureNameMap.cbegin(), CaptureNameMap.cend(),
+ [deviceName](const DevMap &entry) -> bool
+ { return entry.name && strcmp(deviceName, entry.name) == 0; }
+ );
+ if(iter == CaptureNameMap.cend())
+ return ALC_INVALID_DEVICE;
+
+ status = snd_pcm_open(&data->pcmHandle, iter->card, iter->dev, SND_PCM_OPEN_CAPTURE);
+ }
+
+ if(status < 0)
+ return ALC_INVALID_DEVICE;
+
+ data->audio_fd = snd_pcm_file_descriptor(data->pcmHandle, SND_PCM_CHANNEL_CAPTURE);
+ if(data->audio_fd < 0)
+ {
+ snd_pcm_close(data->pcmHandle);
+ return ALC_INVALID_DEVICE;
+ }
+
+ device->DeviceName = deviceName;
+
+ switch (device->FmtType)
+ {
+ case DevFmtByte:
+ format=SND_PCM_SFMT_S8;
+ break;
+ case DevFmtUByte:
+ format=SND_PCM_SFMT_U8;
+ break;
+ case DevFmtShort:
+ format=SND_PCM_SFMT_S16_LE;
+ break;
+ case DevFmtUShort:
+ format=SND_PCM_SFMT_U16_LE;
+ break;
+ case DevFmtInt:
+ format=SND_PCM_SFMT_S32_LE;
+ break;
+ case DevFmtUInt:
+ format=SND_PCM_SFMT_U32_LE;
+ break;
+ case DevFmtFloat:
+ format=SND_PCM_SFMT_FLOAT_LE;
+ break;
+ }
+
+ /* we actually don't want to block on reads */
+ snd_pcm_nonblock_mode(data->pcmHandle, 1);
+ /* Disable mmap to control data transfer to the audio device */
+ snd_pcm_plugin_set_disable(data->pcmHandle, PLUGIN_DISABLE_MMAP);
+
+ /* configure a sound channel */
+ memset(&data->cparams, 0, sizeof(data->cparams));
+ data->cparams.mode=SND_PCM_MODE_BLOCK;
+ data->cparams.channel=SND_PCM_CHANNEL_CAPTURE;
+ data->cparams.start_mode=SND_PCM_START_GO;
+ data->cparams.stop_mode=SND_PCM_STOP_STOP;
+
+ data->cparams.buf.block.frag_size=device->UpdateSize * device->frameSizeFromFmt();
+ data->cparams.buf.block.frags_max=device->NumUpdates;
+ data->cparams.buf.block.frags_min=device->NumUpdates;
+
+ data->cparams.format.interleave=1;
+ data->cparams.format.rate=device->Frequency;
+ data->cparams.format.voices=device->channelsFromFmt();
+ data->cparams.format.format=format;
+
+ if(snd_pcm_plugin_params(data->pcmHandle, &data->cparams) < 0)
+ {
+ snd_pcm_close(data->pcmHandle);
+ return ALC_INVALID_VALUE;
+ }
+
+ self->mExtraData = std::move(data);
+
+ return ALC_NO_ERROR;
+}
+
+static void qsa_close_capture(CaptureWrapper *self)
+{
+ qsa_data *data = self->mExtraData.get();
+
+ if (data->pcmHandle!=nullptr)
+ snd_pcm_close(data->pcmHandle);
+ data->pcmHandle = nullptr;
+
+ self->mExtraData = nullptr;
+}
+
+static void qsa_start_capture(CaptureWrapper *self)
+{
+ qsa_data *data = self->mExtraData.get();
+ int rstatus;
+
+ if ((rstatus=snd_pcm_plugin_prepare(data->pcmHandle, SND_PCM_CHANNEL_CAPTURE))<0)
+ {
+ ERR("capture prepare failed: %s\n", snd_strerror(rstatus));
+ return;
+ }
+
+ memset(&data->csetup, 0, sizeof(data->csetup));
+ data->csetup.channel=SND_PCM_CHANNEL_CAPTURE;
+ if ((rstatus=snd_pcm_plugin_setup(data->pcmHandle, &data->csetup))<0)
+ {
+ ERR("capture setup failed: %s\n", snd_strerror(rstatus));
+ return;
+ }
+
+ snd_pcm_capture_go(data->pcmHandle);
+}
+
+static void qsa_stop_capture(CaptureWrapper *self)
+{
+ qsa_data *data = self->mExtraData.get();
+ snd_pcm_capture_flush(data->pcmHandle);
+}
+
+static ALCuint qsa_available_samples(CaptureWrapper *self)
+{
+ ALCdevice *device = self->mDevice;
+ qsa_data *data = self->mExtraData.get();
+ snd_pcm_channel_status_t status;
+ ALint frame_size = device->frameSizeFromFmt();
+ ALint free_size;
+ int rstatus;
+
+ memset(&status, 0, sizeof (status));
+ status.channel=SND_PCM_CHANNEL_CAPTURE;
+ snd_pcm_plugin_status(data->pcmHandle, &status);
+ if ((status.status==SND_PCM_STATUS_OVERRUN) ||
+ (status.status==SND_PCM_STATUS_READY))
+ {
+ if ((rstatus=snd_pcm_plugin_prepare(data->pcmHandle, SND_PCM_CHANNEL_CAPTURE))<0)
+ {
+ ERR("capture prepare failed: %s\n", snd_strerror(rstatus));
+ aluHandleDisconnect(device, "Failed capture recovery: %s", snd_strerror(rstatus));
+ return 0;
+ }
+
+ snd_pcm_capture_go(data->pcmHandle);
+ return 0;
+ }
+
+ free_size=data->csetup.buf.block.frag_size*data->csetup.buf.block.frags;
+ free_size-=status.free;
+
+ return free_size/frame_size;
+}
+
+static ALCenum qsa_capture_samples(CaptureWrapper *self, ALCvoid *buffer, ALCuint samples)
+{
+ ALCdevice *device = self->mDevice;
+ qsa_data *data = self->mExtraData.get();
+ char* read_ptr;
+ snd_pcm_channel_status_t status;
+ int selectret;
+ int bytes_read;
+ ALint frame_size=device->frameSizeFromFmt();
+ ALint len=samples*frame_size;
+ int rstatus;
+
+ read_ptr = static_cast<char*>(buffer);
+
+ while (len>0)
+ {
+ pollfd pollitem{};
+ pollitem.fd = data->audio_fd;
+ pollitem.events = POLLOUT;
+
+ /* Select also works like time slice to OS */
+ bytes_read=0;
+ selectret = poll(&pollitem, 1, 2000);
+ switch (selectret)
+ {
+ case -1:
+ aluHandleDisconnect(device, "Failed to check capture samples");
+ return ALC_INVALID_DEVICE;
+ case 0:
+ break;
+ default:
+ bytes_read=snd_pcm_plugin_read(data->pcmHandle, read_ptr, len);
+ break;
+ }
+
+ if (bytes_read<=0)
+ {
+ if ((errno==EAGAIN) || (errno==EWOULDBLOCK))
+ {
+ continue;
+ }
+
+ memset(&status, 0, sizeof (status));
+ status.channel=SND_PCM_CHANNEL_CAPTURE;
+ snd_pcm_plugin_status(data->pcmHandle, &status);
+
+ /* we need to reinitialize the sound channel if we've overrun the buffer */
+ if ((status.status==SND_PCM_STATUS_OVERRUN) ||
+ (status.status==SND_PCM_STATUS_READY))
+ {
+ if ((rstatus=snd_pcm_plugin_prepare(data->pcmHandle, SND_PCM_CHANNEL_CAPTURE))<0)
+ {
+ ERR("capture prepare failed: %s\n", snd_strerror(rstatus));
+ aluHandleDisconnect(device, "Failed capture recovery: %s",
+ snd_strerror(rstatus));
+ return ALC_INVALID_DEVICE;
+ }
+ snd_pcm_capture_go(data->pcmHandle);
+ }
+ }
+ else
+ {
+ read_ptr+=bytes_read;
+ len-=bytes_read;
+ }
+ }
+
+ return ALC_NO_ERROR;
+}
+
+
+CaptureWrapper::~CaptureWrapper()
+{
+ if(mExtraData)
+ qsa_close_capture(this);
+}
+
+ALCenum CaptureWrapper::open(const ALCchar *name)
+{ return qsa_open_capture(this, name); }
+
+ALCboolean CaptureWrapper::start()
+{ qsa_start_capture(this); return ALC_TRUE; }
+
+void CaptureWrapper::stop()
+{ qsa_stop_capture(this); }
+
+ALCenum CaptureWrapper::captureSamples(void *buffer, ALCuint samples)
+{ return qsa_capture_samples(this, buffer, samples); }
+
+ALCuint CaptureWrapper::availableSamples()
+{ return qsa_available_samples(this); }
+
+} // namespace
+
+
+bool QSABackendFactory::init()
+{ return true; }
+
+bool QSABackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+void QSABackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ auto add_device = [outnames](const DevMap &entry) -> void
+ {
+ const char *n = entry.name;
+ if(n && n[0])
+ outnames->append(n, strlen(n)+1);
+ };
+
+ switch (type)
+ {
+ case DevProbe::Playback:
+ deviceList(SND_PCM_CHANNEL_PLAYBACK, &DeviceNameMap);
+ std::for_each(DeviceNameMap.cbegin(), DeviceNameMap.cend(), add_device);
+ break;
+ case DevProbe::Capture:
+ deviceList(SND_PCM_CHANNEL_CAPTURE, &CaptureNameMap);
+ std::for_each(CaptureNameMap.cbegin(), CaptureNameMap.cend(), add_device);
+ break;
+ }
+}
+
+BackendPtr QSABackendFactory::createBackend(ALCdevice *device, BackendType type)
+{
+ if(type == BackendType::Playback)
+ return BackendPtr{new PlaybackWrapper{device}};
+ if(type == BackendType::Capture)
+ return BackendPtr{new CaptureWrapper{device}};
+ return nullptr;
+}
+
+BackendFactory &QSABackendFactory::getFactory()
+{
+ static QSABackendFactory factory{};
+ return factory;
+}
diff --git a/alc/backends/qsa.h b/alc/backends/qsa.h
new file mode 100644
index 00000000..da548bba
--- /dev/null
+++ b/alc/backends/qsa.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_QSA_H
+#define BACKENDS_QSA_H
+
+#include "backends/base.h"
+
+struct QSABackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+
+ static BackendFactory &getFactory();
+};
+
+#endif /* BACKENDS_QSA_H */
diff --git a/alc/backends/sdl2.cpp b/alc/backends/sdl2.cpp
new file mode 100644
index 00000000..29d27c05
--- /dev/null
+++ b/alc/backends/sdl2.cpp
@@ -0,0 +1,227 @@
+/**
+ * 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 "backends/sdl2.h"
+
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+
+#include "AL/al.h"
+
+#include "alcmain.h"
+#include "almalloc.h"
+#include "alu.h"
+#include "logging.h"
+
+#include <SDL2/SDL.h>
+
+
+namespace {
+
+#ifdef _WIN32
+#define DEVNAME_PREFIX "OpenAL Soft on "
+#else
+#define DEVNAME_PREFIX ""
+#endif
+
+constexpr ALCchar defaultDeviceName[] = DEVNAME_PREFIX "Default Device";
+
+struct Sdl2Backend final : public BackendBase {
+ Sdl2Backend(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~Sdl2Backend() override;
+
+ static void audioCallbackC(void *ptr, Uint8 *stream, int len);
+ void audioCallback(Uint8 *stream, int len);
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+ void lock() override;
+ void unlock() override;
+
+ SDL_AudioDeviceID mDeviceID{0u};
+ ALsizei mFrameSize{0};
+
+ ALuint mFrequency{0u};
+ DevFmtChannels mFmtChans{};
+ DevFmtType mFmtType{};
+ ALuint mUpdateSize{0u};
+
+ DEF_NEWDEL(Sdl2Backend)
+};
+
+Sdl2Backend::~Sdl2Backend()
+{
+ if(mDeviceID)
+ SDL_CloseAudioDevice(mDeviceID);
+ mDeviceID = 0;
+}
+
+void Sdl2Backend::audioCallbackC(void *ptr, Uint8 *stream, int len)
+{ static_cast<Sdl2Backend*>(ptr)->audioCallback(stream, len); }
+
+void Sdl2Backend::audioCallback(Uint8 *stream, int len)
+{
+ assert((len % mFrameSize) == 0);
+ aluMixData(mDevice, stream, len / mFrameSize);
+}
+
+ALCenum Sdl2Backend::open(const ALCchar *name)
+{
+ SDL_AudioSpec want{}, have{};
+ want.freq = 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 = mDevice->UpdateSize;
+ want.callback = &Sdl2Backend::audioCallbackC;
+ want.userdata = this;
+
+ /* Passing nullptr to SDL_OpenAudioDevice opens a default, which isn't
+ * necessarily the first in the list.
+ */
+ if(!name || strcmp(name, defaultDeviceName) == 0)
+ mDeviceID = SDL_OpenAudioDevice(nullptr, SDL_FALSE, &want, &have,
+ SDL_AUDIO_ALLOW_ANY_CHANGE);
+ else
+ {
+ const size_t prefix_len = strlen(DEVNAME_PREFIX);
+ if(strncmp(name, DEVNAME_PREFIX, prefix_len) == 0)
+ mDeviceID = SDL_OpenAudioDevice(name+prefix_len, SDL_FALSE, &want, &have,
+ SDL_AUDIO_ALLOW_ANY_CHANGE);
+ else
+ mDeviceID = SDL_OpenAudioDevice(name, SDL_FALSE, &want, &have,
+ SDL_AUDIO_ALLOW_ANY_CHANGE);
+ }
+ if(mDeviceID == 0)
+ return ALC_INVALID_VALUE;
+
+ mDevice->Frequency = have.freq;
+ if(have.channels == 1)
+ mDevice->FmtChans = DevFmtMono;
+ else if(have.channels == 2)
+ mDevice->FmtChans = DevFmtStereo;
+ else
+ {
+ ERR("Got unhandled SDL channel count: %d\n", (int)have.channels);
+ return ALC_INVALID_VALUE;
+ }
+ switch(have.format)
+ {
+ case AUDIO_U8: mDevice->FmtType = DevFmtUByte; break;
+ case AUDIO_S8: mDevice->FmtType = DevFmtByte; break;
+ case AUDIO_U16SYS: mDevice->FmtType = DevFmtUShort; break;
+ case AUDIO_S16SYS: mDevice->FmtType = DevFmtShort; break;
+ case AUDIO_S32SYS: mDevice->FmtType = DevFmtInt; break;
+ case AUDIO_F32SYS: mDevice->FmtType = DevFmtFloat; break;
+ default:
+ ERR("Got unsupported SDL format: 0x%04x\n", have.format);
+ return ALC_INVALID_VALUE;
+ }
+ mDevice->UpdateSize = have.samples;
+ mDevice->BufferSize = have.samples * 2; /* SDL always (tries to) use two periods. */
+
+ mFrameSize = mDevice->frameSizeFromFmt();
+ mFrequency = mDevice->Frequency;
+ mFmtChans = mDevice->FmtChans;
+ mFmtType = mDevice->FmtType;
+ mUpdateSize = mDevice->UpdateSize;
+
+ mDevice->DeviceName = name ? name : defaultDeviceName;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean Sdl2Backend::reset()
+{
+ mDevice->Frequency = mFrequency;
+ mDevice->FmtChans = mFmtChans;
+ mDevice->FmtType = mFmtType;
+ mDevice->UpdateSize = mUpdateSize;
+ mDevice->BufferSize = mUpdateSize * 2;
+ SetDefaultWFXChannelOrder(mDevice);
+ return ALC_TRUE;
+}
+
+ALCboolean Sdl2Backend::start()
+{
+ SDL_PauseAudioDevice(mDeviceID, 0);
+ return ALC_TRUE;
+}
+
+void Sdl2Backend::stop()
+{ SDL_PauseAudioDevice(mDeviceID, 1); }
+
+void Sdl2Backend::lock()
+{ SDL_LockAudioDevice(mDeviceID); }
+
+void Sdl2Backend::unlock()
+{ SDL_UnlockAudioDevice(mDeviceID); }
+
+} // 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; }
+
+void SDL2BackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ if(type != DevProbe::Playback)
+ return;
+
+ 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);
+ }
+}
+
+BackendPtr SDL2BackendFactory::createBackend(ALCdevice *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
index 00000000..041d47ee
--- /dev/null
+++ b/alc/backends/sdl2.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_SDL2_H
+#define BACKENDS_SDL2_H
+
+#include "backends/base.h"
+
+struct SDL2BackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..587f67bb
--- /dev/null
+++ b/alc/backends/sndio.cpp
@@ -0,0 +1,495 @@
+/**
+ * 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 "backends/sndio.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <thread>
+#include <functional>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "threads.h"
+#include "vector.h"
+#include "ringbuffer.h"
+
+#include <sndio.h>
+
+
+namespace {
+
+static const ALCchar sndio_device[] = "SndIO Default";
+
+
+struct SndioPlayback final : public BackendBase {
+ SndioPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~SndioPlayback() override;
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ sio_hdl *mSndHandle{nullptr};
+
+ al::vector<ALubyte> mBuffer;
+
+ std::atomic<bool> mKillNow{true};
+ std::thread mThread;
+
+ DEF_NEWDEL(SndioPlayback)
+};
+
+SndioPlayback::~SndioPlayback()
+{
+ if(mSndHandle)
+ sio_close(mSndHandle);
+ mSndHandle = nullptr;
+}
+
+int SndioPlayback::mixerProc()
+{
+ SetRTPriority();
+ althrd_setname(MIXER_THREAD_NAME);
+
+ const ALsizei frameSize{mDevice->frameSizeFromFmt()};
+
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ auto WritePtr = static_cast<ALubyte*>(mBuffer.data());
+ size_t len{mBuffer.size()};
+
+ lock();
+ aluMixData(mDevice, WritePtr, len/frameSize);
+ unlock();
+ while(len > 0 && !mKillNow.load(std::memory_order_acquire))
+ {
+ size_t wrote{sio_write(mSndHandle, WritePtr, len)};
+ if(wrote == 0)
+ {
+ ERR("sio_write failed\n");
+ aluHandleDisconnect(mDevice, "Failed to write playback samples");
+ break;
+ }
+
+ len -= wrote;
+ WritePtr += wrote;
+ }
+ }
+
+ return 0;
+}
+
+
+ALCenum SndioPlayback::open(const ALCchar *name)
+{
+ if(!name)
+ name = sndio_device;
+ else if(strcmp(name, sndio_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ mSndHandle = sio_open(nullptr, SIO_PLAY, 0);
+ if(mSndHandle == nullptr)
+ {
+ ERR("Could not open device\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean SndioPlayback::reset()
+{
+ sio_par par;
+ sio_initpar(&par);
+
+ par.rate = mDevice->Frequency;
+ par.pchan = ((mDevice->FmtChans != DevFmtMono) ? 2 : 1);
+
+ switch(mDevice->FmtType)
+ {
+ case DevFmtByte:
+ par.bits = 8;
+ par.sig = 1;
+ break;
+ case DevFmtUByte:
+ par.bits = 8;
+ par.sig = 0;
+ break;
+ case DevFmtFloat:
+ 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;
+ }
+ par.le = SIO_LE_NATIVE;
+
+ par.round = mDevice->UpdateSize;
+ par.appbufsz = mDevice->BufferSize - mDevice->UpdateSize;
+ if(!par.appbufsz) par.appbufsz = mDevice->UpdateSize;
+
+ if(!sio_setpar(mSndHandle, &par) || !sio_getpar(mSndHandle, &par))
+ {
+ ERR("Failed to set device parameters\n");
+ return ALC_FALSE;
+ }
+
+ if(par.bits != par.bps*8)
+ {
+ ERR("Padded samples not supported (%u of %u bits)\n", par.bits, par.bps*8);
+ return ALC_FALSE;
+ }
+
+ mDevice->Frequency = par.rate;
+ mDevice->FmtChans = ((par.pchan==1) ? DevFmtMono : DevFmtStereo);
+
+ if(par.bits == 8 && par.sig == 1)
+ mDevice->FmtType = DevFmtByte;
+ else if(par.bits == 8 && par.sig == 0)
+ mDevice->FmtType = DevFmtUByte;
+ else if(par.bits == 16 && par.sig == 1)
+ mDevice->FmtType = DevFmtShort;
+ else if(par.bits == 16 && par.sig == 0)
+ mDevice->FmtType = DevFmtUShort;
+ else if(par.bits == 32 && par.sig == 1)
+ mDevice->FmtType = DevFmtInt;
+ else if(par.bits == 32 && par.sig == 0)
+ mDevice->FmtType = DevFmtUInt;
+ else
+ {
+ ERR("Unhandled sample format: %s %u-bit\n", (par.sig?"signed":"unsigned"), par.bits);
+ return ALC_FALSE;
+ }
+
+ SetDefaultChannelOrder(mDevice);
+
+ mDevice->UpdateSize = par.round;
+ mDevice->BufferSize = par.bufsz + par.round;
+
+ mBuffer.resize(mDevice->UpdateSize * mDevice->frameSizeFromFmt());
+ std::fill(mBuffer.begin(), mBuffer.end(), 0);
+
+ return ALC_TRUE;
+}
+
+ALCboolean SndioPlayback::start()
+{
+ if(!sio_start(mSndHandle))
+ {
+ ERR("Error starting playback\n");
+ return ALC_FALSE;
+ }
+
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&SndioPlayback::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ sio_stop(mSndHandle);
+ return ALC_FALSE;
+}
+
+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");
+}
+
+
+struct SndioCapture final : public BackendBase {
+ SndioCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~SndioCapture() override;
+
+ int recordProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(void *buffer, ALCuint samples) override;
+ ALCuint 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 ALsizei frameSize{mDevice->frameSizeFromFmt()};
+
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ auto data = mRing->getWriteVector();
+ size_t todo{data.first.len + data.second.len};
+ if(todo == 0)
+ {
+ static char junk[4096];
+ sio_read(mSndHandle, junk,
+ minz(sizeof(junk)/frameSize, mDevice->UpdateSize)*frameSize);
+ continue;
+ }
+
+ size_t total{0u};
+ data.first.len *= frameSize;
+ data.second.len *= frameSize;
+ todo = minz(todo, mDevice->UpdateSize) * frameSize;
+ while(total < todo)
+ {
+ if(!data.first.len)
+ data.first = data.second;
+
+ size_t got{sio_read(mSndHandle, data.first.buf, minz(todo-total, data.first.len))};
+ if(!got)
+ {
+ aluHandleDisconnect(mDevice, "Failed to read capture samples");
+ break;
+ }
+
+ data.first.buf += got;
+ data.first.len -= got;
+ total += got;
+ }
+ mRing->writeAdvance(total / frameSize);
+ }
+
+ return 0;
+}
+
+
+ALCenum SndioCapture::open(const ALCchar *name)
+{
+ if(!name)
+ name = sndio_device;
+ else if(strcmp(name, sndio_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ mSndHandle = sio_open(nullptr, SIO_REC, 0);
+ if(mSndHandle == nullptr)
+ {
+ ERR("Could not open device\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ sio_par par;
+ sio_initpar(&par);
+
+ switch(mDevice->FmtType)
+ {
+ case DevFmtByte:
+ par.bps = 1;
+ par.sig = 1;
+ break;
+ case DevFmtUByte:
+ par.bps = 1;
+ par.sig = 0;
+ break;
+ case DevFmtShort:
+ par.bps = 2;
+ par.sig = 1;
+ break;
+ case DevFmtUShort:
+ par.bps = 2;
+ par.sig = 0;
+ break;
+ case DevFmtInt:
+ par.bps = 4;
+ par.sig = 1;
+ break;
+ case DevFmtUInt:
+ par.bps = 4;
+ par.sig = 0;
+ break;
+ case DevFmtFloat:
+ ERR("%s capture samples not supported\n", DevFmtTypeString(mDevice->FmtType));
+ return ALC_INVALID_VALUE;
+ }
+ par.bits = par.bps * 8;
+ par.le = SIO_LE_NATIVE;
+ par.msb = SIO_LE_NATIVE ? 0 : 1;
+ par.rchan = mDevice->channelsFromFmt();
+ par.rate = mDevice->Frequency;
+
+ par.appbufsz = maxu(mDevice->BufferSize, mDevice->Frequency/10);
+ par.round = minu(par.appbufsz, mDevice->Frequency/40);
+
+ mDevice->UpdateSize = par.round;
+ mDevice->BufferSize = par.appbufsz;
+
+ if(!sio_setpar(mSndHandle, &par) || !sio_getpar(mSndHandle, &par))
+ {
+ ERR("Failed to set device parameters\n");
+ return ALC_INVALID_VALUE;
+ }
+
+ if(par.bits != par.bps*8)
+ {
+ ERR("Padded samples not supported (%u of %u bits)\n", par.bits, par.bps*8);
+ return ALC_INVALID_VALUE;
+ }
+
+ if(!((mDevice->FmtType == DevFmtByte && par.bits == 8 && par.sig != 0) ||
+ (mDevice->FmtType == DevFmtUByte && par.bits == 8 && par.sig == 0) ||
+ (mDevice->FmtType == DevFmtShort && par.bits == 16 && par.sig != 0) ||
+ (mDevice->FmtType == DevFmtUShort && par.bits == 16 && par.sig == 0) ||
+ (mDevice->FmtType == DevFmtInt && par.bits == 32 && par.sig != 0) ||
+ (mDevice->FmtType == DevFmtUInt && par.bits == 32 && par.sig == 0)) ||
+ mDevice->channelsFromFmt() != (ALsizei)par.rchan ||
+ mDevice->Frequency != par.rate)
+ {
+ ERR("Failed to set format %s %s %uhz, got %c%u %u-channel %uhz instead\n",
+ DevFmtTypeString(mDevice->FmtType), DevFmtChannelsString(mDevice->FmtChans),
+ mDevice->Frequency, par.sig?'s':'u', par.bits, par.rchan, par.rate);
+ return ALC_INVALID_VALUE;
+ }
+
+ mRing = CreateRingBuffer(mDevice->BufferSize, par.bps*par.rchan, false);
+ if(!mRing)
+ {
+ ERR("Failed to allocate %u-byte ringbuffer\n", mDevice->BufferSize*par.bps*par.rchan);
+ return ALC_OUT_OF_MEMORY;
+ }
+
+ SetDefaultChannelOrder(mDevice);
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean SndioCapture::start()
+{
+ if(!sio_start(mSndHandle))
+ {
+ ERR("Error starting playback\n");
+ return ALC_FALSE;
+ }
+
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&SndioCapture::recordProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create record thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ sio_stop(mSndHandle);
+ return ALC_FALSE;
+}
+
+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");
+}
+
+ALCenum SndioCapture::captureSamples(void *buffer, ALCuint samples)
+{
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+}
+
+ALCuint SndioCapture::availableSamples()
+{ return 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); }
+
+void SndIOBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ case DevProbe::Capture:
+ /* Includes null char. */
+ outnames->append(sndio_device, sizeof(sndio_device));
+ break;
+ }
+}
+
+BackendPtr SndIOBackendFactory::createBackend(ALCdevice *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
index 00000000..1ed63d5e
--- /dev/null
+++ b/alc/backends/sndio.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_SNDIO_H
+#define BACKENDS_SNDIO_H
+
+#include "backends/base.h"
+
+struct SndIOBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..584f6e66
--- /dev/null
+++ b/alc/backends/solaris.cpp
@@ -0,0 +1,302 @@
+/**
+ * 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 "backends/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 <thread>
+#include <functional>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "alconfig.h"
+#include "threads.h"
+#include "vector.h"
+#include "compat.h"
+
+#include <sys/audioio.h>
+
+
+namespace {
+
+constexpr ALCchar solaris_device[] = "Solaris Default";
+
+std::string solaris_driver{"/dev/audio"};
+
+
+struct SolarisBackend final : public BackendBase {
+ SolarisBackend(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~SolarisBackend() override;
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ int mFd{-1};
+
+ al::vector<ALubyte> 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 int frame_size{mDevice->frameSizeFromFmt()};
+
+ lock();
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ pollfd pollitem{};
+ pollitem.fd = mFd;
+ pollitem.events = POLLOUT;
+
+ unlock();
+ int pret{poll(&pollitem, 1, 1000)};
+ lock();
+ if(pret < 0)
+ {
+ if(errno == EINTR || errno == EAGAIN)
+ continue;
+ ERR("poll failed: %s\n", strerror(errno));
+ aluHandleDisconnect(mDevice, "Failed to wait for playback buffer: %s",
+ strerror(errno));
+ break;
+ }
+ else if(pret == 0)
+ {
+ WARN("poll timeout\n");
+ continue;
+ }
+
+ ALubyte *write_ptr{mBuffer.data()};
+ size_t to_write{mBuffer.size()};
+ aluMixData(mDevice, write_ptr, to_write/frame_size);
+ 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));
+ aluHandleDisconnect(mDevice, "Failed to write playback samples: %s",
+ strerror(errno));
+ break;
+ }
+
+ to_write -= wrote;
+ write_ptr += wrote;
+ }
+ }
+ unlock();
+
+ return 0;
+}
+
+
+ALCenum SolarisBackend::open(const ALCchar *name)
+{
+ if(!name)
+ name = solaris_device;
+ else if(strcmp(name, solaris_device) != 0)
+ return ALC_INVALID_VALUE;
+
+ mFd = ::open(solaris_driver.c_str(), O_WRONLY);
+ if(mFd == -1)
+ {
+ ERR("Could not open %s: %s\n", solaris_driver.c_str(), strerror(errno));
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean SolarisBackend::reset()
+{
+ audio_info_t info;
+ AUDIO_INITINFO(&info);
+
+ info.play.sample_rate = mDevice->Frequency;
+
+ if(mDevice->FmtChans != DevFmtMono)
+ mDevice->FmtChans = DevFmtStereo;
+ ALsizei numChannels{mDevice->channelsFromFmt()};
+ info.play.channels = numChannels;
+
+ 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;
+ }
+
+ ALsizei frameSize{numChannels * mDevice->bytesFromFmt()};
+ info.play.buffer_size = mDevice->BufferSize * frameSize;
+
+ if(ioctl(mFd, AUDIO_SETINFO, &info) < 0)
+ {
+ ERR("ioctl failed: %s\n", strerror(errno));
+ return ALC_FALSE;
+ }
+
+ if(mDevice->channelsFromFmt() != (ALsizei)info.play.channels)
+ {
+ ERR("Failed to set %s, got %u channels instead\n", DevFmtChannelsString(mDevice->FmtChans),
+ info.play.channels);
+ return ALC_FALSE;
+ }
+
+ if(!((info.play.precision == 8 && info.play.encoding == AUDIO_ENCODING_LINEAR8 && mDevice->FmtType == DevFmtUByte) ||
+ (info.play.precision == 8 && info.play.encoding == AUDIO_ENCODING_LINEAR && mDevice->FmtType == DevFmtByte) ||
+ (info.play.precision == 16 && info.play.encoding == AUDIO_ENCODING_LINEAR && mDevice->FmtType == DevFmtShort) ||
+ (info.play.precision == 32 && info.play.encoding == AUDIO_ENCODING_LINEAR && mDevice->FmtType == DevFmtInt)))
+ {
+ ERR("Could not set %s samples, got %d (0x%x)\n", DevFmtTypeString(mDevice->FmtType),
+ info.play.precision, info.play.encoding);
+ return ALC_FALSE;
+ }
+
+ mDevice->Frequency = info.play.sample_rate;
+ mDevice->BufferSize = info.play.buffer_size / frameSize;
+ mDevice->UpdateSize = mDevice->BufferSize / 2;
+
+ SetDefaultChannelOrder(mDevice);
+
+ mBuffer.resize(mDevice->UpdateSize * mDevice->frameSizeFromFmt());
+ std::fill(mBuffer.begin(), mBuffer.end(), 0);
+
+ return ALC_TRUE;
+}
+
+ALCboolean SolarisBackend::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&SolarisBackend::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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; }
+
+void SolarisBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ {
+#ifdef HAVE_STAT
+ struct stat buf;
+ if(stat(solaris_driver.c_str(), &buf) == 0)
+#endif
+ outnames->append(solaris_device, sizeof(solaris_device));
+ }
+ break;
+
+ case DevProbe::Capture:
+ break;
+ }
+}
+
+BackendPtr SolarisBackendFactory::createBackend(ALCdevice *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
index 00000000..98b10593
--- /dev/null
+++ b/alc/backends/solaris.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_SOLARIS_H
+#define BACKENDS_SOLARIS_H
+
+#include "backends/base.h"
+
+struct SolarisBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..bd009463
--- /dev/null
+++ b/alc/backends/wasapi.cpp
@@ -0,0 +1,1763 @@
+/**
+ * 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 "backends/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 <deque>
+#include <mutex>
+#include <atomic>
+#include <thread>
+#include <vector>
+#include <string>
+#include <future>
+#include <algorithm>
+#include <functional>
+#include <condition_variable>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "ringbuffer.h"
+#include "compat.h"
+#include "converter.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 {
+
+#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 X7DOT1_WIDE (SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT|SPEAKER_FRONT_CENTER|SPEAKER_LOW_FREQUENCY|SPEAKER_BACK_LEFT|SPEAKER_BACK_RIGHT|SPEAKER_FRONT_LEFT_OF_CENTER|SPEAKER_FRONT_RIGHT_OF_CENTER)
+
+#define REFTIME_PER_SEC ((REFERENCE_TIME)10000000)
+
+#define DEVNAME_HEAD "OpenAL Soft on "
+
+
+/* Scales the given value using 64-bit integer math, ceiling the result. */
+inline int64_t ScaleCeil(int64_t val, int64_t new_scale, int64_t old_scale)
+{
+ return (val*new_scale + old_scale-1) / old_scale;
+}
+
+
+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)
+{
+ return std::find_if(list.cbegin(), list.cend(),
+ [&name](const DevMap &entry) -> bool
+ { return entry.name == 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)
+{
+ std::string name{DEVNAME_HEAD};
+ std::string guid;
+
+ IPropertyStore *ps;
+ HRESULT hr = device->OpenPropertyStore(STGM_READ, &ps);
+ if(FAILED(hr))
+ {
+ WARN("OpenPropertyStore failed: 0x%08lx\n", hr);
+ return { name+"Unknown Device Name", "Unknown Device GUID" };
+ }
+
+ 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 += "Unknown Device Name";
+ }
+ else if(pvprop->vt == VT_LPWSTR)
+ name += wstr_to_utf8(pvprop->pwszVal);
+ else
+ {
+ WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvprop->vt);
+ name += "Unknown Device Name";
+ }
+
+ 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 = "Unknown Device GUID";
+ }
+ else if(pvprop->vt == VT_LPWSTR)
+ guid = wstr_to_utf8(pvprop->pwszVal);
+ else
+ {
+ WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvprop->vt);
+ guid = "Unknown Device GUID";
+ }
+
+ ps->Release();
+
+ return {name, guid};
+}
+
+void get_device_formfactor(IMMDevice *device, EndpointFormFactor *formfactor)
+{
+ IPropertyStore *ps;
+ HRESULT hr = device->OpenPropertyStore(STGM_READ, &ps);
+ if(FAILED(hr))
+ {
+ WARN("OpenPropertyStore failed: 0x%08lx\n", hr);
+ return;
+ }
+
+ PropVariant pvform;
+ hr = ps->GetValue(reinterpret_cast<const PROPERTYKEY&>(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)
+ *formfactor = UnknownFormFactor;
+ else
+ WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvform->vt);
+
+ ps->Release();
+}
+
+
+void add_device(IMMDevice *device, const WCHAR *devid, al::vector<DevMap> &list)
+{
+ std::string basename, guidstr;
+ std::tie(basename, guidstr) = get_device_name_and_guid(device);
+
+ int count{1};
+ std::string newname{basename};
+ while(checkName(list, newname))
+ {
+ newname = basename;
+ newname += " #";
+ newname += std::to_string(++count);
+ }
+ list.emplace_back(std::move(newname), std::move(guidstr), 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;
+
+ HRESULT hr = device->GetId(&devid);
+ if(FAILED(hr))
+ {
+ ERR("Failed to get device id: %lx\n", hr);
+ return nullptr;
+ }
+
+ return devid;
+}
+
+HRESULT probe_devices(IMMDeviceEnumerator *devenum, EDataFlow flowdir, al::vector<DevMap> &list)
+{
+ IMMDeviceCollection *coll;
+ HRESULT hr{devenum->EnumAudioEndpoints(flowdir, DEVICE_STATE_ACTIVE, &coll)};
+ if(FAILED(hr))
+ {
+ ERR("Failed to enumerate audio endpoints: 0x%08lx\n", hr);
+ return hr;
+ }
+
+ IMMDevice *defdev{nullptr};
+ WCHAR *defdevid{nullptr};
+ UINT count{0};
+ hr = coll->GetCount(&count);
+ if(SUCCEEDED(hr) && count > 0)
+ {
+ list.clear();
+ list.reserve(count);
+
+ hr = devenum->GetDefaultAudioEndpoint(flowdir, eMultimedia, &defdev);
+ }
+ if(SUCCEEDED(hr) && defdev != nullptr)
+ {
+ defdevid = get_device_id(defdev);
+ if(defdevid)
+ add_device(defdev, defdevid, list);
+ }
+
+ for(UINT i{0};i < count;++i)
+ {
+ IMMDevice *device;
+ hr = coll->Item(i, &device);
+ if(FAILED(hr)) continue;
+
+ WCHAR *devid{get_device_id(device)};
+ if(devid)
+ {
+ if(!defdevid || wcscmp(devid, defdevid) != 0)
+ add_device(device, devid, list);
+ CoTaskMemFree(devid);
+ }
+ device->Release();
+ }
+
+ if(defdev) defdev->Release();
+ if(defdevid) CoTaskMemFree(defdevid);
+ coll->Release();
+
+ return S_OK;
+}
+
+
+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)
+ {
+ 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; }
+ };
+
+ 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 : unsigned int {
+ OpenDevice,
+ ResetDevice,
+ StartDevice,
+ StopDevice,
+ CloseDevice,
+ EnumeratePlayback,
+ EnumerateCapture,
+ QuitThread,
+
+ Count
+};
+
+constexpr char MessageStr[static_cast<unsigned int>(MsgType::Count)][20]{
+ "Open Device",
+ "Reset Device",
+ "Start Device",
+ "Stop Device",
+ "Close Device",
+ "Enumerate Playback",
+ "Enumerate Capture",
+ "Quit"
+};
+
+
+/* Proxy interface used by the message handler. */
+struct WasapiProxy {
+ virtual HRESULT openProxy() = 0;
+ virtual void closeProxy() = 0;
+
+ virtual HRESULT resetProxy() = 0;
+ virtual HRESULT startProxy() = 0;
+ virtual void stopProxy() = 0;
+
+ struct Msg {
+ MsgType mType;
+ WasapiProxy *mProxy;
+ std::promise<HRESULT> mPromise;
+ };
+ static std::deque<Msg> mMsgQueue;
+ static std::mutex mMsgQueueLock;
+ static std::condition_variable mMsgQueueCond;
+
+ std::future<HRESULT> pushMessage(MsgType type)
+ {
+ std::promise<HRESULT> promise;
+ std::future<HRESULT> future{promise.get_future()};
+ { std::lock_guard<std::mutex> _{mMsgQueueLock};
+ mMsgQueue.emplace_back(Msg{type, this, 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, std::move(promise)});
+ }
+ mMsgQueueCond.notify_one();
+ return future;
+ }
+
+ static bool popMessage(Msg &msg)
+ {
+ std::unique_lock<std::mutex> lock{mMsgQueueLock};
+ while(mMsgQueue.empty())
+ mMsgQueueCond.wait(lock);
+ msg = std::move(mMsgQueue.front());
+ mMsgQueue.pop_front();
+ return msg.mType != MsgType::QuitThread;
+ }
+
+ static int messageHandler(std::promise<HRESULT> *promise);
+};
+std::deque<WasapiProxy::Msg> WasapiProxy::mMsgQueue;
+std::mutex WasapiProxy::mMsgQueueLock;
+std::condition_variable WasapiProxy::mMsgQueueCond;
+
+int WasapiProxy::messageHandler(std::promise<HRESULT> *promise)
+{
+ TRACE("Starting message thread\n");
+
+ HRESULT cohr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ if(FAILED(cohr))
+ {
+ WARN("Failed to initialize COM: 0x%08lx\n", cohr);
+ promise->set_value(cohr);
+ return 0;
+ }
+
+ void *ptr{};
+ HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IMMDeviceEnumerator, &ptr)};
+ if(FAILED(hr))
+ {
+ WARN("Failed to create IMMDeviceEnumerator instance: 0x%08lx\n", hr);
+ promise->set_value(hr);
+ CoUninitialize();
+ return 0;
+ }
+ auto Enumerator = static_cast<IMMDeviceEnumerator*>(ptr);
+ Enumerator->Release();
+ Enumerator = nullptr;
+ CoUninitialize();
+
+ TRACE("Message thread initialization complete\n");
+ promise->set_value(S_OK);
+ promise = nullptr;
+
+ TRACE("Starting message loop\n");
+ ALuint deviceCount{0};
+ Msg msg;
+ while(popMessage(msg))
+ {
+ TRACE("Got message \"%s\" (0x%04x, this=%p)\n",
+ MessageStr[static_cast<unsigned int>(msg.mType)], static_cast<unsigned int>(msg.mType),
+ msg.mProxy);
+
+ switch(msg.mType)
+ {
+ case MsgType::OpenDevice:
+ hr = cohr = S_OK;
+ if(++deviceCount == 1)
+ hr = cohr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ if(SUCCEEDED(hr))
+ hr = msg.mProxy->openProxy();
+ msg.mPromise.set_value(hr);
+
+ if(FAILED(hr))
+ {
+ if(--deviceCount == 0 && SUCCEEDED(cohr))
+ CoUninitialize();
+ }
+ 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);
+
+ if(--deviceCount == 0)
+ CoUninitialize();
+ continue;
+
+ case MsgType::EnumeratePlayback:
+ case MsgType::EnumerateCapture:
+ hr = cohr = S_OK;
+ if(++deviceCount == 1)
+ hr = cohr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ if(SUCCEEDED(hr))
+ hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, IID_IMMDeviceEnumerator, &ptr);
+ if(FAILED(hr))
+ msg.mPromise.set_value(hr);
+ else
+ {
+ Enumerator = static_cast<IMMDeviceEnumerator*>(ptr);
+
+ if(msg.mType == MsgType::EnumeratePlayback)
+ hr = probe_devices(Enumerator, eRender, PlaybackDevices);
+ else if(msg.mType == MsgType::EnumerateCapture)
+ hr = probe_devices(Enumerator, eCapture, CaptureDevices);
+ msg.mPromise.set_value(hr);
+
+ Enumerator->Release();
+ Enumerator = nullptr;
+ }
+
+ if(--deviceCount == 0 && SUCCEEDED(cohr))
+ CoUninitialize();
+ continue;
+
+ default:
+ ERR("Unexpected message: %u\n", static_cast<unsigned int>(msg.mType));
+ msg.mPromise.set_value(E_FAIL);
+ continue;
+ }
+ }
+ TRACE("Message loop finished\n");
+
+ return 0;
+}
+
+
+struct WasapiPlayback final : public BackendBase, WasapiProxy {
+ WasapiPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~WasapiPlayback() override;
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ HRESULT openProxy() override;
+ void closeProxy() override;
+
+ ALCboolean reset() override;
+ HRESULT resetProxy() override;
+ ALCboolean start() override;
+ HRESULT startProxy() override;
+ void stop() override;
+ void stopProxy() override;
+
+ ClockLatency getClockLatency() override;
+
+ std::wstring mDevId;
+
+ IMMDevice *mMMDev{nullptr};
+ IAudioClient *mClient{nullptr};
+ IAudioRenderClient *mRender{nullptr};
+ HANDLE mNotifyEvent{nullptr};
+
+ std::atomic<UINT32> mPadding{0u};
+
+ std::atomic<bool> mKillNow{true};
+ std::thread mThread;
+
+ DEF_NEWDEL(WasapiPlayback)
+};
+
+WasapiPlayback::~WasapiPlayback()
+{
+ pushMessage(MsgType::CloseDevice).wait();
+
+ 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);
+ aluHandleDisconnect(mDevice, "COM init failed: 0x%08lx", hr);
+ return 1;
+ }
+
+ SetRTPriority();
+ althrd_setname(MIXER_THREAD_NAME);
+
+ const ALuint update_size{mDevice->UpdateSize};
+ const UINT32 buffer_len{mDevice->BufferSize};
+ 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);
+ aluHandleDisconnect(mDevice, "Failed to retrieve buffer padding: 0x%08lx", hr);
+ break;
+ }
+ mPadding.store(written, std::memory_order_relaxed);
+
+ ALuint 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))
+ {
+ lock();
+ aluMixData(mDevice, buffer, len);
+ mPadding.store(written + len, std::memory_order_relaxed);
+ unlock();
+ hr = mRender->ReleaseBuffer(len, 0);
+ }
+ if(FAILED(hr))
+ {
+ ERR("Failed to buffer data: 0x%08lx\n", hr);
+ aluHandleDisconnect(mDevice, "Failed to send playback samples: 0x%08lx", hr);
+ break;
+ }
+ }
+ mPadding.store(0u, std::memory_order_release);
+
+ CoUninitialize();
+ return 0;
+}
+
+
+ALCenum WasapiPlayback::open(const ALCchar *name)
+{
+ HRESULT hr{S_OK};
+
+ mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+ if(mNotifyEvent == nullptr)
+ {
+ ERR("Failed to create notify events: %lu\n", GetLastError());
+ hr = E_FAIL;
+ }
+
+ if(SUCCEEDED(hr))
+ {
+ if(name)
+ {
+ if(PlaybackDevices.empty())
+ pushMessage(MsgType::EnumeratePlayback).wait();
+
+ hr = E_FAIL;
+ 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())
+ {
+ 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);
+ else
+ {
+ mDevId = iter->devid;
+ mDevice->DeviceName = iter->name;
+ hr = S_OK;
+ }
+ }
+ }
+
+ if(SUCCEEDED(hr))
+ hr = pushMessage(MsgType::OpenDevice).get();
+
+ if(FAILED(hr))
+ {
+ if(mNotifyEvent != nullptr)
+ CloseHandle(mNotifyEvent);
+ mNotifyEvent = nullptr;
+
+ mDevId.clear();
+
+ ERR("Device init failed: 0x%08lx\n", hr);
+ return ALC_INVALID_VALUE;
+ }
+
+ return ALC_NO_ERROR;
+}
+
+HRESULT WasapiPlayback::openProxy()
+{
+ void *ptr;
+ HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, IID_IMMDeviceEnumerator, &ptr)};
+ if(SUCCEEDED(hr))
+ {
+ auto Enumerator = static_cast<IMMDeviceEnumerator*>(ptr);
+ if(mDevId.empty())
+ hr = Enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &mMMDev);
+ else
+ hr = Enumerator->GetDevice(mDevId.c_str(), &mMMDev);
+ Enumerator->Release();
+ }
+ if(SUCCEEDED(hr))
+ hr = mMMDev->Activate(IID_IAudioClient, CLSCTX_INPROC_SERVER, nullptr, &ptr);
+ if(SUCCEEDED(hr))
+ {
+ mClient = static_cast<IAudioClient*>(ptr);
+ if(mDevice->DeviceName.empty())
+ mDevice->DeviceName = get_device_name_and_guid(mMMDev).first;
+ }
+
+ if(FAILED(hr))
+ {
+ if(mMMDev)
+ mMMDev->Release();
+ mMMDev = nullptr;
+ }
+
+ return hr;
+}
+
+void WasapiPlayback::closeProxy()
+{
+ if(mClient)
+ mClient->Release();
+ mClient = nullptr;
+
+ if(mMMDev)
+ mMMDev->Release();
+ mMMDev = nullptr;
+}
+
+
+ALCboolean WasapiPlayback::reset()
+{
+ HRESULT hr{pushMessage(MsgType::ResetDevice).get()};
+ return SUCCEEDED(hr) ? ALC_TRUE : ALC_FALSE;
+}
+
+HRESULT WasapiPlayback::resetProxy()
+{
+ if(mClient)
+ mClient->Release();
+ 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 = 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;
+ }
+
+ WAVEFORMATEXTENSIBLE OutputType;
+ if(!MakeExtensible(&OutputType, wfx))
+ {
+ CoTaskMemFree(wfx);
+ return E_FAIL;
+ }
+ CoTaskMemFree(wfx);
+ wfx = nullptr;
+
+ const REFERENCE_TIME per_time{mDevice->UpdateSize * REFTIME_PER_SEC / mDevice->Frequency};
+ const REFERENCE_TIME buf_time{mDevice->BufferSize * REFTIME_PER_SEC / mDevice->Frequency};
+
+ if(!mDevice->Flags.get<FrequencyRequest>())
+ mDevice->Frequency = OutputType.Format.nSamplesPerSec;
+ if(!mDevice->Flags.get<ChannelsRequest>())
+ {
+ if(OutputType.Format.nChannels == 1 && OutputType.dwChannelMask == MONO)
+ mDevice->FmtChans = DevFmtMono;
+ else if(OutputType.Format.nChannels == 2 && OutputType.dwChannelMask == STEREO)
+ mDevice->FmtChans = DevFmtStereo;
+ else if(OutputType.Format.nChannels == 4 && OutputType.dwChannelMask == QUAD)
+ mDevice->FmtChans = DevFmtQuad;
+ else if(OutputType.Format.nChannels == 6 && OutputType.dwChannelMask == X5DOT1)
+ mDevice->FmtChans = DevFmtX51;
+ else if(OutputType.Format.nChannels == 6 && OutputType.dwChannelMask == X5DOT1REAR)
+ mDevice->FmtChans = DevFmtX51Rear;
+ else if(OutputType.Format.nChannels == 7 && OutputType.dwChannelMask == X6DOT1)
+ mDevice->FmtChans = DevFmtX61;
+ else if(OutputType.Format.nChannels == 8 && (OutputType.dwChannelMask == X7DOT1 || OutputType.dwChannelMask == X7DOT1_WIDE))
+ mDevice->FmtChans = DevFmtX71;
+ else
+ ERR("Unhandled channel config: %d -- 0x%08lx\n", OutputType.Format.nChannels, OutputType.dwChannelMask);
+ }
+
+ 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 = X5DOT1;
+ break;
+ case DevFmtX51Rear:
+ OutputType.Format.nChannels = 6;
+ OutputType.dwChannelMask = X5DOT1REAR;
+ break;
+ case DevFmtX61:
+ OutputType.Format.nChannels = 7;
+ OutputType.dwChannelMask = X6DOT1;
+ break;
+ case DevFmtX71:
+ OutputType.Format.nChannels = 8;
+ OutputType.dwChannelMask = X7DOT1;
+ 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 = 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))
+ {
+ ERR("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;
+
+ mDevice->Frequency = OutputType.Format.nSamplesPerSec;
+ if(OutputType.Format.nChannels == 1 && OutputType.dwChannelMask == MONO)
+ mDevice->FmtChans = DevFmtMono;
+ else if(OutputType.Format.nChannels == 2 && OutputType.dwChannelMask == STEREO)
+ mDevice->FmtChans = DevFmtStereo;
+ else if(OutputType.Format.nChannels == 4 && OutputType.dwChannelMask == QUAD)
+ mDevice->FmtChans = DevFmtQuad;
+ else if(OutputType.Format.nChannels == 6 && OutputType.dwChannelMask == X5DOT1)
+ mDevice->FmtChans = DevFmtX51;
+ else if(OutputType.Format.nChannels == 6 && OutputType.dwChannelMask == X5DOT1REAR)
+ mDevice->FmtChans = DevFmtX51Rear;
+ else if(OutputType.Format.nChannels == 7 && OutputType.dwChannelMask == X6DOT1)
+ mDevice->FmtChans = DevFmtX61;
+ else if(OutputType.Format.nChannels == 8 && (OutputType.dwChannelMask == X7DOT1 || OutputType.dwChannelMask == X7DOT1_WIDE))
+ mDevice->FmtChans = DevFmtX71;
+ 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\n");
+ 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;
+ }
+
+ EndpointFormFactor formfactor = UnknownFormFactor;
+ get_device_formfactor(mMMDev, &formfactor);
+ mDevice->IsHeadphones = (mDevice->FmtChans == DevFmtStereo &&
+ (formfactor == Headphones || formfactor == Headset));
+
+ SetDefaultWFXChannelOrder(mDevice);
+
+ hr = mClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buf_time,
+ 0, &OutputType.Format, nullptr);
+ if(FAILED(hr))
+ {
+ ERR("Failed to initialize audio client: 0x%08lx\n", hr);
+ return hr;
+ }
+
+ UINT32 buffer_len, min_len;
+ REFERENCE_TIME min_per;
+ hr = mClient->GetDevicePeriod(&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);
+ min_len = (UINT32)ScaleCeil(min_per, mDevice->Frequency, REFTIME_PER_SEC);
+ min_len = minu(min_len, buffer_len/2);
+
+ mDevice->UpdateSize = min_len;
+ mDevice->BufferSize = buffer_len;
+
+ hr = mClient->SetEventHandle(mNotifyEvent);
+ if(FAILED(hr))
+ {
+ ERR("Failed to set event handle: 0x%08lx\n", hr);
+ return hr;
+ }
+
+ return hr;
+}
+
+
+ALCboolean WasapiPlayback::start()
+{
+ HRESULT hr{pushMessage(MsgType::StartDevice).get()};
+ return SUCCEEDED(hr) ? ALC_TRUE : ALC_FALSE;
+}
+
+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 = static_cast<IAudioRenderClient*>(ptr);
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&WasapiPlayback::mixerProc), this};
+ }
+ catch(...) {
+ mRender->Release();
+ 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->Release();
+ mRender = nullptr;
+ mClient->Stop();
+}
+
+
+ClockLatency WasapiPlayback::getClockLatency()
+{
+ ClockLatency ret;
+
+ lock();
+ ret.ClockTime = GetDeviceClockTime(mDevice);
+ ret.Latency = std::chrono::seconds{mPadding.load(std::memory_order_relaxed)};
+ ret.Latency /= mDevice->Frequency;
+ unlock();
+
+ return ret;
+}
+
+
+struct WasapiCapture final : public BackendBase, WasapiProxy {
+ WasapiCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~WasapiCapture() override;
+
+ int recordProc();
+
+ ALCenum open(const ALCchar *name) override;
+ HRESULT openProxy() override;
+ void closeProxy() override;
+
+ HRESULT resetProxy() override;
+ ALCboolean start() override;
+ HRESULT startProxy() override;
+ void stop() override;
+ void stopProxy() override;
+
+ ALCenum captureSamples(void *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+
+ std::wstring mDevId;
+
+ IMMDevice *mMMDev{nullptr};
+ IAudioClient *mClient{nullptr};
+ IAudioCaptureClient *mCapture{nullptr};
+ HANDLE mNotifyEvent{nullptr};
+
+ ChannelConverterPtr mChannelConv;
+ SampleConverterPtr mSampleConv;
+ RingBufferPtr mRing;
+
+ std::atomic<bool> mKillNow{true};
+ std::thread mThread;
+
+ DEF_NEWDEL(WasapiCapture)
+};
+
+WasapiCapture::~WasapiCapture()
+{
+ pushMessage(MsgType::CloseDevice).wait();
+
+ 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);
+ aluHandleDisconnect(mDevice, "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)
+ {
+ 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 ALvoid *srcdata{rdata};
+ auto srcframes = static_cast<ALsizei>(numsamples);
+
+ dstframes = mSampleConv->convert(&srcdata, &srcframes, data.first.buf,
+ static_cast<ALsizei>(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<ALsizei>(minz(data.second.len, INT_MAX)));
+ }
+ }
+ else
+ {
+ const auto framesize = static_cast<ALuint>(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))
+ {
+ aluHandleDisconnect(mDevice, "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;
+}
+
+
+ALCenum WasapiCapture::open(const ALCchar *name)
+{
+ HRESULT hr{S_OK};
+
+ mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+ if(mNotifyEvent == nullptr)
+ {
+ ERR("Failed to create notify event: %lu\n", GetLastError());
+ hr = E_FAIL;
+ }
+
+ if(SUCCEEDED(hr))
+ {
+ if(name)
+ {
+ if(CaptureDevices.empty())
+ pushMessage(MsgType::EnumerateCapture).wait();
+
+ hr = E_FAIL;
+ 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())
+ {
+ 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);
+ else
+ {
+ mDevId = iter->devid;
+ mDevice->DeviceName = iter->name;
+ hr = S_OK;
+ }
+ }
+ }
+
+ if(SUCCEEDED(hr))
+ hr = pushMessage(MsgType::OpenDevice).get();
+
+ if(FAILED(hr))
+ {
+ if(mNotifyEvent != nullptr)
+ CloseHandle(mNotifyEvent);
+ mNotifyEvent = nullptr;
+
+ mDevId.clear();
+
+ ERR("Device init failed: 0x%08lx\n", hr);
+ return ALC_INVALID_VALUE;
+ }
+
+ hr = pushMessage(MsgType::ResetDevice).get();
+ if(FAILED(hr))
+ {
+ if(hr == E_OUTOFMEMORY)
+ return ALC_OUT_OF_MEMORY;
+ return ALC_INVALID_VALUE;
+ }
+
+ return ALC_NO_ERROR;
+}
+
+HRESULT WasapiCapture::openProxy()
+{
+ void *ptr;
+ HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IMMDeviceEnumerator, &ptr)};
+ if(SUCCEEDED(hr))
+ {
+ auto Enumerator = static_cast<IMMDeviceEnumerator*>(ptr);
+ if(mDevId.empty())
+ hr = Enumerator->GetDefaultAudioEndpoint(eCapture, eMultimedia, &mMMDev);
+ else
+ hr = Enumerator->GetDevice(mDevId.c_str(), &mMMDev);
+ Enumerator->Release();
+ }
+ if(SUCCEEDED(hr))
+ hr = mMMDev->Activate(IID_IAudioClient, CLSCTX_INPROC_SERVER, nullptr, &ptr);
+ if(SUCCEEDED(hr))
+ {
+ mClient = static_cast<IAudioClient*>(ptr);
+ if(mDevice->DeviceName.empty())
+ mDevice->DeviceName = get_device_name_and_guid(mMMDev).first;
+ }
+
+ if(FAILED(hr))
+ {
+ if(mMMDev)
+ mMMDev->Release();
+ mMMDev = nullptr;
+ }
+
+ return hr;
+}
+
+void WasapiCapture::closeProxy()
+{
+ if(mClient)
+ mClient->Release();
+ mClient = nullptr;
+
+ if(mMMDev)
+ mMMDev->Release();
+ mMMDev = nullptr;
+}
+
+HRESULT WasapiCapture::resetProxy()
+{
+ if(mClient)
+ mClient->Release();
+ 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 = static_cast<IAudioClient*>(ptr);
+
+ // Make sure buffer is at least 100ms in size
+ REFERENCE_TIME buf_time{mDevice->BufferSize * REFTIME_PER_SEC / mDevice->Frequency};
+ buf_time = maxu64(buf_time, REFTIME_PER_SEC/10);
+
+ WAVEFORMATEXTENSIBLE OutputType;
+ OutputType.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ switch(mDevice->FmtChans)
+ {
+ case DevFmtMono:
+ OutputType.Format.nChannels = 1;
+ OutputType.dwChannelMask = MONO;
+ break;
+ 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 = X5DOT1;
+ break;
+ case DevFmtX51Rear:
+ OutputType.Format.nChannels = 6;
+ OutputType.dwChannelMask = X5DOT1REAR;
+ break;
+ case DevFmtX61:
+ OutputType.Format.nChannels = 7;
+ OutputType.dwChannelMask = X6DOT1;
+ break;
+ case DevFmtX71:
+ OutputType.Format.nChannels = 8;
+ OutputType.dwChannelMask = X7DOT1;
+ break;
+
+ case DevFmtAmbi3D:
+ return E_FAIL;
+ }
+ switch(mDevice->FmtType)
+ {
+ /* NOTE: Signedness doesn't matter, the converter will handle it. */
+ case DevFmtByte:
+ case DevFmtUByte:
+ OutputType.Format.wBitsPerSample = 8;
+ OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ break;
+ case DevFmtShort:
+ case DevFmtUShort:
+ OutputType.Format.wBitsPerSample = 16;
+ OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ break;
+ case DevFmtInt:
+ case DevFmtUInt:
+ OutputType.Format.wBitsPerSample = 32;
+ OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ break;
+ case DevFmtFloat:
+ OutputType.Format.wBitsPerSample = 32;
+ OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+ break;
+ }
+ OutputType.Samples.wValidBitsPerSample = OutputType.Format.wBitsPerSample;
+ OutputType.Format.nSamplesPerSec = mDevice->Frequency;
+
+ OutputType.Format.nBlockAlign = OutputType.Format.nChannels *
+ OutputType.Format.wBitsPerSample / 8;
+ OutputType.Format.nAvgBytesPerSec = OutputType.Format.nSamplesPerSec *
+ OutputType.Format.nBlockAlign;
+ OutputType.Format.cbSize = sizeof(OutputType) - sizeof(OutputType.Format);
+
+ TraceFormat("Requesting capture format", &OutputType.Format);
+ WAVEFORMATEX *wfx;
+ hr = mClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &OutputType.Format, &wfx);
+ if(FAILED(hr))
+ {
+ ERR("Failed to check format support: 0x%08lx\n", hr);
+ return hr;
+ }
+
+ mSampleConv = nullptr;
+ mChannelConv = nullptr;
+
+ if(wfx != nullptr)
+ {
+ TraceFormat("Got capture format", wfx);
+ if(!(wfx->nChannels == OutputType.Format.nChannels ||
+ (wfx->nChannels == 1 && OutputType.Format.nChannels == 2) ||
+ (wfx->nChannels == 2 && OutputType.Format.nChannels == 1)))
+ {
+ ERR("Failed to get matching format, wanted: %s %s %uhz, got: %d channel%s %d-bit %luhz\n",
+ DevFmtChannelsString(mDevice->FmtChans), DevFmtTypeString(mDevice->FmtType),
+ mDevice->Frequency, wfx->nChannels, (wfx->nChannels==1)?"":"s", wfx->wBitsPerSample,
+ wfx->nSamplesPerSec);
+ CoTaskMemFree(wfx);
+ return E_FAIL;
+ }
+
+ if(!MakeExtensible(&OutputType, wfx))
+ {
+ CoTaskMemFree(wfx);
+ return E_FAIL;
+ }
+ CoTaskMemFree(wfx);
+ wfx = nullptr;
+ }
+
+ DevFmtType srcType;
+ if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_PCM))
+ {
+ if(OutputType.Format.wBitsPerSample == 8)
+ srcType = DevFmtUByte;
+ else if(OutputType.Format.wBitsPerSample == 16)
+ srcType = DevFmtShort;
+ else if(OutputType.Format.wBitsPerSample == 32)
+ srcType = DevFmtInt;
+ else
+ {
+ ERR("Unhandled integer bit depth: %d\n", OutputType.Format.wBitsPerSample);
+ return E_FAIL;
+ }
+ }
+ else if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
+ {
+ if(OutputType.Format.wBitsPerSample == 32)
+ srcType = DevFmtFloat;
+ else
+ {
+ ERR("Unhandled float bit depth: %d\n", OutputType.Format.wBitsPerSample);
+ return E_FAIL;
+ }
+ }
+ else
+ {
+ ERR("Unhandled format sub-type\n");
+ return E_FAIL;
+ }
+
+ if(mDevice->FmtChans == DevFmtMono && OutputType.Format.nChannels == 2)
+ {
+ mChannelConv = CreateChannelConverter(srcType, DevFmtStereo, mDevice->FmtChans);
+ if(!mChannelConv)
+ {
+ ERR("Failed to create %s stereo-to-mono converter\n", DevFmtTypeString(srcType));
+ return E_FAIL;
+ }
+ TRACE("Created %s stereo-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 && OutputType.Format.nChannels == 1)
+ {
+ mChannelConv = CreateChannelConverter(srcType, DevFmtMono, mDevice->FmtChans);
+ if(!mChannelConv)
+ {
+ ERR("Failed to create %s mono-to-stereo converter\n", DevFmtTypeString(srcType));
+ return E_FAIL;
+ }
+ TRACE("Created %s mono-to-stereo converter\n", DevFmtTypeString(srcType));
+ srcType = DevFmtFloat;
+ }
+
+ if(mDevice->Frequency != OutputType.Format.nSamplesPerSec || mDevice->FmtType != srcType)
+ {
+ mSampleConv = CreateSampleConverter(srcType, mDevice->FmtType, mDevice->channelsFromFmt(),
+ OutputType.Format.nSamplesPerSec, mDevice->Frequency, BSinc24Resampler);
+ 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), OutputType.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), OutputType.Format.nSamplesPerSec);
+ }
+
+ hr = mClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buf_time,
+ 0, &OutputType.Format, nullptr);
+ if(FAILED(hr))
+ {
+ ERR("Failed to initialize audio client: 0x%08lx\n", hr);
+ return hr;
+ }
+
+ UINT32 buffer_len;
+ REFERENCE_TIME min_per;
+ hr = mClient->GetDevicePeriod(&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 = static_cast<ALuint>(ScaleCeil(min_per, mDevice->Frequency,
+ REFTIME_PER_SEC));
+ mDevice->BufferSize = buffer_len;
+
+ buffer_len = maxu(mDevice->BufferSize, buffer_len);
+ mRing = CreateRingBuffer(buffer_len, mDevice->frameSizeFromFmt(), false);
+ if(!mRing)
+ {
+ ERR("Failed to allocate capture ring buffer\n");
+ return E_OUTOFMEMORY;
+ }
+
+ hr = mClient->SetEventHandle(mNotifyEvent);
+ if(FAILED(hr))
+ {
+ ERR("Failed to set event handle: 0x%08lx\n", hr);
+ return hr;
+ }
+
+ return hr;
+}
+
+
+ALCboolean WasapiCapture::start()
+{
+ HRESULT hr{pushMessage(MsgType::StartDevice).get()};
+ return SUCCEEDED(hr) ? ALC_TRUE : ALC_FALSE;
+}
+
+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 = static_cast<IAudioCaptureClient*>(ptr);
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&WasapiCapture::recordProc), this};
+ }
+ catch(...) {
+ mCapture->Release();
+ 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->Release();
+ mCapture = nullptr;
+ mClient->Stop();
+ mClient->Reset();
+}
+
+
+ALCuint WasapiCapture::availableSamples()
+{ return (ALCuint)mRing->readSpace(); }
+
+ALCenum WasapiCapture::captureSamples(void *buffer, ALCuint samples)
+{
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+}
+
+} // namespace
+
+
+bool WasapiBackendFactory::init()
+{
+ static HRESULT InitResult{E_FAIL};
+
+ if(FAILED(InitResult)) try
+ {
+ std::promise<HRESULT> promise;
+ auto future = promise.get_future();
+
+ std::thread{&WasapiProxy::messageHandler, &promise}.detach();
+ InitResult = future.get();
+ }
+ catch(...) {
+ }
+
+ return SUCCEEDED(InitResult) ? ALC_TRUE : ALC_FALSE;
+}
+
+bool WasapiBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+void WasapiBackendFactory::probe(DevProbe 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);
+ };
+ HRESULT hr{};
+ switch(type)
+ {
+ case DevProbe::Playback:
+ hr = WasapiProxy::pushMessageStatic(MsgType::EnumeratePlayback).get();
+ if(SUCCEEDED(hr))
+ std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+ break;
+
+ case DevProbe::Capture:
+ hr = WasapiProxy::pushMessageStatic(MsgType::EnumerateCapture).get();
+ if(SUCCEEDED(hr))
+ std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+ break;
+ }
+}
+
+BackendPtr WasapiBackendFactory::createBackend(ALCdevice *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
index 00000000..067dd259
--- /dev/null
+++ b/alc/backends/wasapi.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_WASAPI_H
+#define BACKENDS_WASAPI_H
+
+#include "backends/base.h"
+
+struct WasapiBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..67ed7e79
--- /dev/null
+++ b/alc/backends/wave.cpp
@@ -0,0 +1,402 @@
+/**
+ * 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 "backends/wave.h"
+
+#include <algorithm>
+#include <atomic>
+#include <cerrno>
+#include <chrono>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <exception>
+#include <functional>
+#include <thread>
+
+#include "AL/al.h"
+
+#include "alcmain.h"
+#include "alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "alu.h"
+#include "compat.h"
+#include "logging.h"
+#include "threads.h"
+#include "vector.h"
+
+
+namespace {
+
+using std::chrono::seconds;
+using std::chrono::milliseconds;
+using std::chrono::nanoseconds;
+
+constexpr ALCchar waveDevice[] = "Wave File Writer";
+
+constexpr ALubyte SUBTYPE_PCM[]{
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa,
+ 0x00, 0x38, 0x9b, 0x71
+};
+constexpr ALubyte SUBTYPE_FLOAT[]{
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa,
+ 0x00, 0x38, 0x9b, 0x71
+};
+
+constexpr ALubyte SUBTYPE_BFORMAT_PCM[]{
+ 0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
+ 0xca, 0x00, 0x00, 0x00
+};
+
+constexpr ALubyte SUBTYPE_BFORMAT_FLOAT[]{
+ 0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
+ 0xca, 0x00, 0x00, 0x00
+};
+
+void fwrite16le(ALushort val, FILE *f)
+{
+ ALubyte data[2]{ static_cast<ALubyte>(val&0xff), static_cast<ALubyte>((val>>8)&0xff) };
+ fwrite(data, 1, 2, f);
+}
+
+void fwrite32le(ALuint val, FILE *f)
+{
+ ALubyte data[4]{ static_cast<ALubyte>(val&0xff), static_cast<ALubyte>((val>>8)&0xff),
+ static_cast<ALubyte>((val>>16)&0xff), static_cast<ALubyte>((val>>24)&0xff) };
+ fwrite(data, 1, 4, f);
+}
+
+
+struct WaveBackend final : public BackendBase {
+ WaveBackend(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~WaveBackend() override;
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ FILE *mFile{nullptr};
+ long mDataStart{-1};
+
+ al::vector<ALbyte> 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 ALsizei 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)
+ {
+ lock();
+ aluMixData(mDevice, mBuffer.data(), mDevice->UpdateSize);
+ unlock();
+ done += mDevice->UpdateSize;
+
+ if(!IS_LITTLE_ENDIAN)
+ {
+ const ALsizei bytesize{mDevice->bytesFromFmt()};
+ ALsizei i;
+
+ if(bytesize == 2)
+ {
+ ALushort *samples = reinterpret_cast<ALushort*>(mBuffer.data());
+ const auto len = static_cast<ALsizei>(mBuffer.size() / 2);
+ for(i = 0;i < len;i++)
+ {
+ ALushort samp = samples[i];
+ samples[i] = (samp>>8) | (samp<<8);
+ }
+ }
+ else if(bytesize == 4)
+ {
+ ALuint *samples = reinterpret_cast<ALuint*>(mBuffer.data());
+ const auto len = static_cast<ALsizei>(mBuffer.size() / 4);
+ for(i = 0;i < len;i++)
+ {
+ ALuint samp = samples[i];
+ samples[i] = (samp>>24) | ((samp>>8)&0x0000ff00) |
+ ((samp<<8)&0x00ff0000) | (samp<<24);
+ }
+ }
+ }
+
+ size_t fs{fwrite(mBuffer.data(), frameSize, mDevice->UpdateSize, mFile)};
+ (void)fs;
+ if(ferror(mFile))
+ {
+ ERR("Error writing to file\n");
+ aluHandleDisconnect(mDevice, "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};
+ start += s;
+ done -= mDevice->Frequency*s.count();
+ }
+ }
+
+ return 0;
+}
+
+ALCenum WaveBackend::open(const ALCchar *name)
+{
+ const char *fname{GetConfigValue(nullptr, "wave", "file", "")};
+ if(!fname[0]) return ALC_INVALID_VALUE;
+
+ if(!name)
+ name = waveDevice;
+ else if(strcmp(name, waveDevice) != 0)
+ return ALC_INVALID_VALUE;
+
+#ifdef _WIN32
+ {
+ std::wstring wname = utf8_to_wstr(fname);
+ mFile = _wfopen(wname.c_str(), L"wb");
+ }
+#else
+ mFile = fopen(fname, "wb");
+#endif
+ if(!mFile)
+ {
+ ERR("Could not open file '%s': %s\n", fname, strerror(errno));
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+
+ return ALC_NO_ERROR;
+}
+
+ALCboolean WaveBackend::reset()
+{
+ ALuint channels=0, bytes=0, chanmask=0;
+ int isbformat = 0;
+ size_t val;
+
+ fseek(mFile, 0, SEEK_SET);
+ clearerr(mFile);
+
+ if(GetConfigValueBool(nullptr, "wave", "bformat", 0))
+ {
+ 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 DevFmtX51Rear: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x010 | 0x020; 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 DevFmtAmbi3D:
+ /* .amb output requires FuMa */
+ mDevice->mAmbiOrder = mini(mDevice->mAmbiOrder, 3);
+ mDevice->mAmbiLayout = AmbiLayout::FuMa;
+ mDevice->mAmbiScale = AmbiNorm::FuMa;
+ isbformat = 1;
+ 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(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(channels * bytes, mFile);
+ // 16-bit val, bits per sample
+ fwrite16le(bytes * 8, mFile);
+ // 16-bit val, extra byte count
+ fwrite16le(22, mFile);
+ // 16-bit val, valid bits per sample
+ fwrite16le(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 ALC_FALSE;
+ }
+ mDataStart = ftell(mFile);
+
+ SetDefaultWFXChannelOrder(mDevice);
+
+ const ALuint bufsize{mDevice->frameSizeFromFmt() * mDevice->UpdateSize};
+ mBuffer.resize(bufsize);
+
+ return ALC_TRUE;
+}
+
+ALCboolean WaveBackend::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&WaveBackend::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Failed to start mixing thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+void WaveBackend::stop()
+{
+ if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+ return;
+ mThread.join();
+
+ long size{ftell(mFile)};
+ if(size > 0)
+ {
+ long dataLen{size - mDataStart};
+ if(fseek(mFile, mDataStart-4, SEEK_SET) == 0)
+ fwrite32le(dataLen, mFile); // 'data' header len
+ if(fseek(mFile, 4, SEEK_SET) == 0)
+ fwrite32le(size-8, mFile); // 'WAVE' header len
+ }
+}
+
+} // namespace
+
+
+bool WaveBackendFactory::init()
+{ return true; }
+
+bool WaveBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback; }
+
+void WaveBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ switch(type)
+ {
+ case DevProbe::Playback:
+ /* Includes null char. */
+ outnames->append(waveDevice, sizeof(waveDevice));
+ break;
+ case DevProbe::Capture:
+ break;
+ }
+}
+
+BackendPtr WaveBackendFactory::createBackend(ALCdevice *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
index 00000000..b9b62d7f
--- /dev/null
+++ b/alc/backends/wave.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_WAVE_H
+#define BACKENDS_WAVE_H
+
+#include "backends/base.h"
+
+struct WaveBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *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
index 00000000..cd32e95b
--- /dev/null
+++ b/alc/backends/winmm.cpp
@@ -0,0 +1,640 @@
+/**
+ * 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 "backends/winmm.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <memory.h>
+
+#include <windows.h>
+#include <mmsystem.h>
+
+#include <array>
+#include <atomic>
+#include <thread>
+#include <vector>
+#include <string>
+#include <algorithm>
+#include <functional>
+
+#include "alcmain.h"
+#include "alu.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "compat.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();
+
+ ALuint numdevs{waveOutGetNumDevs()};
+ PlaybackDevices.reserve(numdevs);
+ for(ALuint 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();
+
+ ALuint numdevs{waveInGetNumDevs()};
+ CaptureDevices.reserve(numdevs);
+ for(ALuint 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(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~WinMMPlayback() override;
+
+ static void CALLBACK waveOutProcC(HWAVEOUT device, UINT msg, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2);
+ void CALLBACK waveOutProc(HWAVEOUT device, UINT msg, DWORD_PTR param1, DWORD_PTR param2);
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ std::atomic<ALuint> mWritable{0u};
+ al::semaphore mSem;
+ int mIdx{0};
+ 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{});
+}
+
+
+void CALLBACK WinMMPlayback::waveOutProcC(HWAVEOUT device, UINT msg, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2)
+{ reinterpret_cast<WinMMPlayback*>(instance)->waveOutProc(device, msg, param1, param2); }
+
+/* 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)
+{
+ 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);
+
+ lock();
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ ALsizei todo = mWritable.load(std::memory_order_acquire);
+ if(todo < 1)
+ {
+ unlock();
+ mSem.wait();
+ lock();
+ continue;
+ }
+
+ int widx{mIdx};
+ do {
+ WAVEHDR &waveHdr = mWaveBuffer[widx];
+ widx = (widx+1) % mWaveBuffer.size();
+
+ aluMixData(mDevice, waveHdr.lpData, mDevice->UpdateSize);
+ mWritable.fetch_sub(1, std::memory_order_acq_rel);
+ waveOutWrite(mOutHdl, &waveHdr, sizeof(WAVEHDR));
+ } while(--todo);
+ mIdx = widx;
+ }
+ unlock();
+
+ return 0;
+}
+
+
+ALCenum WinMMPlayback::open(const ALCchar *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()) return ALC_INVALID_VALUE;
+ auto DeviceID = static_cast<UINT>(std::distance(PlaybackDevices.cbegin(), iter));
+
+retry_open:
+ mFormat = WAVEFORMATEX{};
+ if(mDevice->FmtType == DevFmtFloat)
+ {
+ mFormat.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
+ mFormat.wBitsPerSample = 32;
+ }
+ else
+ {
+ mFormat.wFormatTag = WAVE_FORMAT_PCM;
+ if(mDevice->FmtType == DevFmtUByte || mDevice->FmtType == DevFmtByte)
+ mFormat.wBitsPerSample = 8;
+ else
+ mFormat.wBitsPerSample = 16;
+ }
+ mFormat.nChannels = ((mDevice->FmtChans == DevFmtMono) ? 1 : 2);
+ mFormat.nBlockAlign = mFormat.wBitsPerSample * mFormat.nChannels / 8;
+ mFormat.nSamplesPerSec = mDevice->Frequency;
+ mFormat.nAvgBytesPerSec = mFormat.nSamplesPerSec * mFormat.nBlockAlign;
+ mFormat.cbSize = 0;
+
+ MMRESULT res{waveOutOpen(&mOutHdl, DeviceID, &mFormat, (DWORD_PTR)&WinMMPlayback::waveOutProcC,
+ reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION)};
+ if(res != MMSYSERR_NOERROR)
+ {
+ if(mDevice->FmtType == DevFmtFloat)
+ {
+ mDevice->FmtType = DevFmtShort;
+ goto retry_open;
+ }
+ ERR("waveOutOpen failed: %u\n", res);
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = PlaybackDevices[DeviceID];
+ return ALC_NO_ERROR;
+}
+
+ALCboolean WinMMPlayback::reset()
+{
+ mDevice->BufferSize = static_cast<ALuint>(uint64_t{mDevice->BufferSize} *
+ mFormat.nSamplesPerSec / mDevice->Frequency);
+ mDevice->BufferSize = (mDevice->BufferSize+3) & ~0x3;
+ 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 ALC_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 ALC_FALSE;
+ }
+ }
+ else
+ {
+ ERR("Unhandled format tag: 0x%04x\n", mFormat.wFormatTag);
+ return ALC_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 ALC_FALSE;
+ }
+ SetDefaultWFXChannelOrder(mDevice);
+
+ ALuint BufferSize{mDevice->UpdateSize * mDevice->frameSizeFromFmt()};
+
+ 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 ALC_TRUE;
+}
+
+ALCboolean WinMMPlayback::start()
+{
+ try {
+ std::for_each(mWaveBuffer.begin(), mWaveBuffer.end(),
+ [this](WAVEHDR &waveHdr) -> void
+ { waveOutPrepareHeader(mOutHdl, &waveHdr, static_cast<UINT>(sizeof(WAVEHDR))); }
+ );
+ mWritable.store(static_cast<ALuint>(mWaveBuffer.size()), std::memory_order_release);
+
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&WinMMPlayback::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Failed to start mixing thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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();
+ std::for_each(mWaveBuffer.begin(), mWaveBuffer.end(),
+ [this](WAVEHDR &waveHdr) -> void
+ { waveOutUnprepareHeader(mOutHdl, &waveHdr, sizeof(WAVEHDR)); }
+ );
+ mWritable.store(0, std::memory_order_release);
+}
+
+
+struct WinMMCapture final : public BackendBase {
+ WinMMCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~WinMMCapture() override;
+
+ static void CALLBACK waveInProcC(HWAVEIN device, UINT msg, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2);
+ void CALLBACK waveInProc(HWAVEIN device, UINT msg, DWORD_PTR param1, DWORD_PTR param2);
+
+ int captureProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(void *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+
+ std::atomic<ALuint> mReadable{0u};
+ al::semaphore mSem;
+ int 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{});
+}
+
+void CALLBACK WinMMCapture::waveInProcC(HWAVEIN device, UINT msg, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2)
+{ reinterpret_cast<WinMMCapture*>(instance)->waveInProc(device, msg, param1, param2); }
+
+/* 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)
+{
+ if(msg != WIM_DATA) return;
+ mReadable.fetch_add(1, std::memory_order_acq_rel);
+ mSem.post();
+}
+
+int WinMMCapture::captureProc()
+{
+ althrd_setname(RECORD_THREAD_NAME);
+
+ lock();
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ ALuint todo{mReadable.load(std::memory_order_acquire)};
+ if(todo < 1)
+ {
+ unlock();
+ mSem.wait();
+ lock();
+ continue;
+ }
+
+ int 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 = widx;
+ }
+ unlock();
+
+ return 0;
+}
+
+
+ALCenum WinMMCapture::open(const ALCchar *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()) return ALC_INVALID_VALUE;
+ auto DeviceID = static_cast<UINT>(std::distance(CaptureDevices.cbegin(), iter));
+
+ switch(mDevice->FmtChans)
+ {
+ case DevFmtMono:
+ case DevFmtStereo:
+ break;
+
+ case DevFmtQuad:
+ case DevFmtX51:
+ case DevFmtX51Rear:
+ case DevFmtX61:
+ case DevFmtX71:
+ case DevFmtAmbi3D:
+ return ALC_INVALID_ENUM;
+ }
+
+ switch(mDevice->FmtType)
+ {
+ case DevFmtUByte:
+ case DevFmtShort:
+ case DevFmtInt:
+ case DevFmtFloat:
+ break;
+
+ case DevFmtByte:
+ case DevFmtUShort:
+ case DevFmtUInt:
+ return ALC_INVALID_ENUM;
+ }
+
+ mFormat = WAVEFORMATEX{};
+ mFormat.wFormatTag = (mDevice->FmtType == DevFmtFloat) ?
+ WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM;
+ mFormat.nChannels = mDevice->channelsFromFmt();
+ mFormat.wBitsPerSample = mDevice->bytesFromFmt() * 8;
+ mFormat.nBlockAlign = mFormat.wBitsPerSample * mFormat.nChannels / 8;
+ mFormat.nSamplesPerSec = mDevice->Frequency;
+ mFormat.nAvgBytesPerSec = mFormat.nSamplesPerSec * mFormat.nBlockAlign;
+ mFormat.cbSize = 0;
+
+ MMRESULT res{waveInOpen(&mInHdl, DeviceID, &mFormat, (DWORD_PTR)&WinMMCapture::waveInProcC,
+ reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION)};
+ if(res != MMSYSERR_NOERROR)
+ {
+ ERR("waveInOpen failed: %u\n", res);
+ return ALC_INVALID_VALUE;
+ }
+
+ // 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
+ ALuint CapturedDataSize{mDevice->BufferSize};
+ CapturedDataSize = static_cast<ALuint>(maxz(CapturedDataSize, BufferSize*mWaveBuffer.size()));
+
+ mRing = CreateRingBuffer(CapturedDataSize, mFormat.nBlockAlign, false);
+ if(!mRing) return ALC_INVALID_VALUE;
+
+ al_free(mWaveBuffer[0].lpData);
+ mWaveBuffer[0] = WAVEHDR{};
+ mWaveBuffer[0].lpData = static_cast<char*>(al_calloc(16, BufferSize*4));
+ 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];
+ return ALC_NO_ERROR;
+}
+
+ALCboolean 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);
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Failed to start mixing thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+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;
+}
+
+ALCenum WinMMCapture::captureSamples(void *buffer, ALCuint samples)
+{
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+}
+
+ALCuint WinMMCapture::availableSamples()
+{ return (ALCuint)mRing->readSpace(); }
+
+} // namespace
+
+
+bool WinMMBackendFactory::init()
+{ return true; }
+
+bool WinMMBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+void WinMMBackendFactory::probe(DevProbe 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 DevProbe::Playback:
+ ProbePlaybackDevices();
+ std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+ break;
+
+ case DevProbe::Capture:
+ ProbeCaptureDevices();
+ std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+ break;
+ }
+}
+
+BackendPtr WinMMBackendFactory::createBackend(ALCdevice *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
index 00000000..e357ec19
--- /dev/null
+++ b/alc/backends/winmm.h
@@ -0,0 +1,19 @@
+#ifndef BACKENDS_WINMM_H
+#define BACKENDS_WINMM_H
+
+#include "backends/base.h"
+
+struct WinMMBackendFactory final : public BackendFactory {
+public:
+ bool init() override;
+
+ bool querySupport(BackendType type) override;
+
+ void probe(DevProbe type, std::string *outnames) override;
+
+ BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+
+ static BackendFactory &getFactory();
+};
+
+#endif /* BACKENDS_WINMM_H */