diff options
Diffstat (limited to 'alc/backends')
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, ¶m); + param.sched_priority=param.sched_curpriority+1; + SchedSet(0, 0, SCHED_NOCHANGE, ¶m); + + 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 */ |