diff options
author | Chris Robinson <[email protected]> | 2019-07-28 18:56:04 -0700 |
---|---|---|
committer | Chris Robinson <[email protected]> | 2019-07-28 18:56:04 -0700 |
commit | cb3e96e75640730b9391f0d2d922eecd9ee2ce79 (patch) | |
tree | 23520551bddb2a80354e44da47f54201fdc084f0 /alc | |
parent | 93e60919c8f387c36c267ca9faa1ac653254aea6 (diff) |
Rename Alc to alc
Diffstat (limited to 'alc')
97 files changed, 35900 insertions, 0 deletions
diff --git a/alc/alc.cpp b/alc/alc.cpp new file mode 100644 index 00000000..00f90d91 --- /dev/null +++ b/alc/alc.cpp @@ -0,0 +1,4342 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 1999-2007 by authors. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include "version.h" + +#include <exception> +#include <algorithm> +#include <array> +#include <atomic> +#include <cctype> +#include <chrono> +#include <climits> +#include <cmath> +#include <csignal> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <functional> +#include <iterator> +#include <limits> +#include <memory> +#include <mutex> +#include <new> +#include <numeric> +#include <string> +#include <thread> +#include <utility> + +#include "AL/al.h" +#include "AL/alc.h" +#include "AL/alext.h" +#include "AL/efx.h" + +#include "alAuxEffectSlot.h" +#include "alcmain.h" +#include "alEffect.h" +#include "alError.h" +#include "alFilter.h" +#include "alListener.h" +#include "alSource.h" +#include "albyte.h" +#include "alconfig.h" +#include "alcontext.h" +#include "alexcpt.h" +#include "almalloc.h" +#include "alnumeric.h" +#include "aloptional.h" +#include "alspan.h" +#include "alu.h" +#include "ambidefs.h" +#include "atomic.h" +#include "bformatdec.h" +#include "bs2b.h" +#include "compat.h" +#include "cpu_caps.h" +#include "effects/base.h" +#include "filters/nfc.h" +#include "filters/splitter.h" +#include "fpu_modes.h" +#include "hrtf.h" +#include "inprogext.h" +#include "logging.h" +#include "mastering.h" +#include "opthelpers.h" +#include "ringbuffer.h" +#include "threads.h" +#include "uhjfilter.h" +#include "vecmat.h" +#include "vector.h" + +#include "backends/base.h" +#include "backends/null.h" +#include "backends/loopback.h" +#ifdef HAVE_JACK +#include "backends/jack.h" +#endif +#ifdef HAVE_PULSEAUDIO +#include "backends/pulseaudio.h" +#endif +#ifdef HAVE_ALSA +#include "backends/alsa.h" +#endif +#ifdef HAVE_WASAPI +#include "backends/wasapi.h" +#endif +#ifdef HAVE_COREAUDIO +#include "backends/coreaudio.h" +#endif +#ifdef HAVE_OPENSL +#include "backends/opensl.h" +#endif +#ifdef HAVE_SOLARIS +#include "backends/solaris.h" +#endif +#ifdef HAVE_SNDIO +#include "backends/sndio.h" +#endif +#ifdef HAVE_OSS +#include "backends/oss.h" +#endif +#ifdef HAVE_QSA +#include "backends/qsa.h" +#endif +#ifdef HAVE_DSOUND +#include "backends/dsound.h" +#endif +#ifdef HAVE_WINMM +#include "backends/winmm.h" +#endif +#ifdef HAVE_PORTAUDIO +#include "backends/portaudio.h" +#endif +#ifdef HAVE_SDL2 +#include "backends/sdl2.h" +#endif +#ifdef HAVE_WAVE +#include "backends/wave.h" +#endif + + +namespace { + +using namespace std::placeholders; +using std::chrono::seconds; +using std::chrono::nanoseconds; + + +/************************************************ + * Backends + ************************************************/ +struct BackendInfo { + const char *name; + BackendFactory& (*getFactory)(void); +}; + +BackendInfo BackendList[] = { +#ifdef HAVE_JACK + { "jack", JackBackendFactory::getFactory }, +#endif +#ifdef HAVE_PULSEAUDIO + { "pulse", PulseBackendFactory::getFactory }, +#endif +#ifdef HAVE_ALSA + { "alsa", AlsaBackendFactory::getFactory }, +#endif +#ifdef HAVE_WASAPI + { "wasapi", WasapiBackendFactory::getFactory }, +#endif +#ifdef HAVE_COREAUDIO + { "core", CoreAudioBackendFactory::getFactory }, +#endif +#ifdef HAVE_OPENSL + { "opensl", OSLBackendFactory::getFactory }, +#endif +#ifdef HAVE_SOLARIS + { "solaris", SolarisBackendFactory::getFactory }, +#endif +#ifdef HAVE_SNDIO + { "sndio", SndIOBackendFactory::getFactory }, +#endif +#ifdef HAVE_OSS + { "oss", OSSBackendFactory::getFactory }, +#endif +#ifdef HAVE_QSA + { "qsa", QSABackendFactory::getFactory }, +#endif +#ifdef HAVE_DSOUND + { "dsound", DSoundBackendFactory::getFactory }, +#endif +#ifdef HAVE_WINMM + { "winmm", WinMMBackendFactory::getFactory }, +#endif +#ifdef HAVE_PORTAUDIO + { "port", PortBackendFactory::getFactory }, +#endif +#ifdef HAVE_SDL2 + { "sdl2", SDL2BackendFactory::getFactory }, +#endif + + { "null", NullBackendFactory::getFactory }, +#ifdef HAVE_WAVE + { "wave", WaveBackendFactory::getFactory }, +#endif +}; +auto BackendListEnd = std::end(BackendList); + +BackendFactory *PlaybackFactory{}; +BackendFactory *CaptureFactory{}; + + +/************************************************ + * Functions, enums, and errors + ************************************************/ +#define DECL(x) { #x, (ALCvoid*)(x) } +const struct { + const ALCchar *funcName; + ALCvoid *address; +} alcFunctions[] = { + DECL(alcCreateContext), + DECL(alcMakeContextCurrent), + DECL(alcProcessContext), + DECL(alcSuspendContext), + DECL(alcDestroyContext), + DECL(alcGetCurrentContext), + DECL(alcGetContextsDevice), + DECL(alcOpenDevice), + DECL(alcCloseDevice), + DECL(alcGetError), + DECL(alcIsExtensionPresent), + DECL(alcGetProcAddress), + DECL(alcGetEnumValue), + DECL(alcGetString), + DECL(alcGetIntegerv), + DECL(alcCaptureOpenDevice), + DECL(alcCaptureCloseDevice), + DECL(alcCaptureStart), + DECL(alcCaptureStop), + DECL(alcCaptureSamples), + + DECL(alcSetThreadContext), + DECL(alcGetThreadContext), + + DECL(alcLoopbackOpenDeviceSOFT), + DECL(alcIsRenderFormatSupportedSOFT), + DECL(alcRenderSamplesSOFT), + + DECL(alcDevicePauseSOFT), + DECL(alcDeviceResumeSOFT), + + DECL(alcGetStringiSOFT), + DECL(alcResetDeviceSOFT), + + DECL(alcGetInteger64vSOFT), + + DECL(alEnable), + DECL(alDisable), + DECL(alIsEnabled), + DECL(alGetString), + DECL(alGetBooleanv), + DECL(alGetIntegerv), + DECL(alGetFloatv), + DECL(alGetDoublev), + DECL(alGetBoolean), + DECL(alGetInteger), + DECL(alGetFloat), + DECL(alGetDouble), + DECL(alGetError), + DECL(alIsExtensionPresent), + DECL(alGetProcAddress), + DECL(alGetEnumValue), + DECL(alListenerf), + DECL(alListener3f), + DECL(alListenerfv), + DECL(alListeneri), + DECL(alListener3i), + DECL(alListeneriv), + DECL(alGetListenerf), + DECL(alGetListener3f), + DECL(alGetListenerfv), + DECL(alGetListeneri), + DECL(alGetListener3i), + DECL(alGetListeneriv), + DECL(alGenSources), + DECL(alDeleteSources), + DECL(alIsSource), + DECL(alSourcef), + DECL(alSource3f), + DECL(alSourcefv), + DECL(alSourcei), + DECL(alSource3i), + DECL(alSourceiv), + DECL(alGetSourcef), + DECL(alGetSource3f), + DECL(alGetSourcefv), + DECL(alGetSourcei), + DECL(alGetSource3i), + DECL(alGetSourceiv), + DECL(alSourcePlayv), + DECL(alSourceStopv), + DECL(alSourceRewindv), + DECL(alSourcePausev), + DECL(alSourcePlay), + DECL(alSourceStop), + DECL(alSourceRewind), + DECL(alSourcePause), + DECL(alSourceQueueBuffers), + DECL(alSourceUnqueueBuffers), + DECL(alGenBuffers), + DECL(alDeleteBuffers), + DECL(alIsBuffer), + DECL(alBufferData), + DECL(alBufferf), + DECL(alBuffer3f), + DECL(alBufferfv), + DECL(alBufferi), + DECL(alBuffer3i), + DECL(alBufferiv), + DECL(alGetBufferf), + DECL(alGetBuffer3f), + DECL(alGetBufferfv), + DECL(alGetBufferi), + DECL(alGetBuffer3i), + DECL(alGetBufferiv), + DECL(alDopplerFactor), + DECL(alDopplerVelocity), + DECL(alSpeedOfSound), + DECL(alDistanceModel), + + DECL(alGenFilters), + DECL(alDeleteFilters), + DECL(alIsFilter), + DECL(alFilteri), + DECL(alFilteriv), + DECL(alFilterf), + DECL(alFilterfv), + DECL(alGetFilteri), + DECL(alGetFilteriv), + DECL(alGetFilterf), + DECL(alGetFilterfv), + DECL(alGenEffects), + DECL(alDeleteEffects), + DECL(alIsEffect), + DECL(alEffecti), + DECL(alEffectiv), + DECL(alEffectf), + DECL(alEffectfv), + DECL(alGetEffecti), + DECL(alGetEffectiv), + DECL(alGetEffectf), + DECL(alGetEffectfv), + DECL(alGenAuxiliaryEffectSlots), + DECL(alDeleteAuxiliaryEffectSlots), + DECL(alIsAuxiliaryEffectSlot), + DECL(alAuxiliaryEffectSloti), + DECL(alAuxiliaryEffectSlotiv), + DECL(alAuxiliaryEffectSlotf), + DECL(alAuxiliaryEffectSlotfv), + DECL(alGetAuxiliaryEffectSloti), + DECL(alGetAuxiliaryEffectSlotiv), + DECL(alGetAuxiliaryEffectSlotf), + DECL(alGetAuxiliaryEffectSlotfv), + + DECL(alDeferUpdatesSOFT), + DECL(alProcessUpdatesSOFT), + + DECL(alSourcedSOFT), + DECL(alSource3dSOFT), + DECL(alSourcedvSOFT), + DECL(alGetSourcedSOFT), + DECL(alGetSource3dSOFT), + DECL(alGetSourcedvSOFT), + DECL(alSourcei64SOFT), + DECL(alSource3i64SOFT), + DECL(alSourcei64vSOFT), + DECL(alGetSourcei64SOFT), + DECL(alGetSource3i64SOFT), + DECL(alGetSourcei64vSOFT), + + DECL(alGetStringiSOFT), + + DECL(alBufferStorageSOFT), + DECL(alMapBufferSOFT), + DECL(alUnmapBufferSOFT), + DECL(alFlushMappedBufferSOFT), + + DECL(alEventControlSOFT), + DECL(alEventCallbackSOFT), + DECL(alGetPointerSOFT), + DECL(alGetPointervSOFT), +}; +#undef DECL + +#define DECL(x) { #x, (x) } +constexpr struct { + const ALCchar *enumName; + ALCenum value; +} alcEnumerations[] = { + DECL(ALC_INVALID), + DECL(ALC_FALSE), + DECL(ALC_TRUE), + + DECL(ALC_MAJOR_VERSION), + DECL(ALC_MINOR_VERSION), + DECL(ALC_ATTRIBUTES_SIZE), + DECL(ALC_ALL_ATTRIBUTES), + DECL(ALC_DEFAULT_DEVICE_SPECIFIER), + DECL(ALC_DEVICE_SPECIFIER), + DECL(ALC_ALL_DEVICES_SPECIFIER), + DECL(ALC_DEFAULT_ALL_DEVICES_SPECIFIER), + DECL(ALC_EXTENSIONS), + DECL(ALC_FREQUENCY), + DECL(ALC_REFRESH), + DECL(ALC_SYNC), + DECL(ALC_MONO_SOURCES), + DECL(ALC_STEREO_SOURCES), + DECL(ALC_CAPTURE_DEVICE_SPECIFIER), + DECL(ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER), + DECL(ALC_CAPTURE_SAMPLES), + DECL(ALC_CONNECTED), + + DECL(ALC_EFX_MAJOR_VERSION), + DECL(ALC_EFX_MINOR_VERSION), + DECL(ALC_MAX_AUXILIARY_SENDS), + + DECL(ALC_FORMAT_CHANNELS_SOFT), + DECL(ALC_FORMAT_TYPE_SOFT), + + DECL(ALC_MONO_SOFT), + DECL(ALC_STEREO_SOFT), + DECL(ALC_QUAD_SOFT), + DECL(ALC_5POINT1_SOFT), + DECL(ALC_6POINT1_SOFT), + DECL(ALC_7POINT1_SOFT), + DECL(ALC_BFORMAT3D_SOFT), + + DECL(ALC_BYTE_SOFT), + DECL(ALC_UNSIGNED_BYTE_SOFT), + DECL(ALC_SHORT_SOFT), + DECL(ALC_UNSIGNED_SHORT_SOFT), + DECL(ALC_INT_SOFT), + DECL(ALC_UNSIGNED_INT_SOFT), + DECL(ALC_FLOAT_SOFT), + + DECL(ALC_HRTF_SOFT), + DECL(ALC_DONT_CARE_SOFT), + DECL(ALC_HRTF_STATUS_SOFT), + DECL(ALC_HRTF_DISABLED_SOFT), + DECL(ALC_HRTF_ENABLED_SOFT), + DECL(ALC_HRTF_DENIED_SOFT), + DECL(ALC_HRTF_REQUIRED_SOFT), + DECL(ALC_HRTF_HEADPHONES_DETECTED_SOFT), + DECL(ALC_HRTF_UNSUPPORTED_FORMAT_SOFT), + DECL(ALC_NUM_HRTF_SPECIFIERS_SOFT), + DECL(ALC_HRTF_SPECIFIER_SOFT), + DECL(ALC_HRTF_ID_SOFT), + + DECL(ALC_AMBISONIC_LAYOUT_SOFT), + DECL(ALC_AMBISONIC_SCALING_SOFT), + DECL(ALC_AMBISONIC_ORDER_SOFT), + DECL(ALC_ACN_SOFT), + DECL(ALC_FUMA_SOFT), + DECL(ALC_N3D_SOFT), + DECL(ALC_SN3D_SOFT), + + DECL(ALC_OUTPUT_LIMITER_SOFT), + + DECL(ALC_NO_ERROR), + DECL(ALC_INVALID_DEVICE), + DECL(ALC_INVALID_CONTEXT), + DECL(ALC_INVALID_ENUM), + DECL(ALC_INVALID_VALUE), + DECL(ALC_OUT_OF_MEMORY), + + + DECL(AL_INVALID), + DECL(AL_NONE), + DECL(AL_FALSE), + DECL(AL_TRUE), + + DECL(AL_SOURCE_RELATIVE), + DECL(AL_CONE_INNER_ANGLE), + DECL(AL_CONE_OUTER_ANGLE), + DECL(AL_PITCH), + DECL(AL_POSITION), + DECL(AL_DIRECTION), + DECL(AL_VELOCITY), + DECL(AL_LOOPING), + DECL(AL_BUFFER), + DECL(AL_GAIN), + DECL(AL_MIN_GAIN), + DECL(AL_MAX_GAIN), + DECL(AL_ORIENTATION), + DECL(AL_REFERENCE_DISTANCE), + DECL(AL_ROLLOFF_FACTOR), + DECL(AL_CONE_OUTER_GAIN), + DECL(AL_MAX_DISTANCE), + DECL(AL_SEC_OFFSET), + DECL(AL_SAMPLE_OFFSET), + DECL(AL_BYTE_OFFSET), + DECL(AL_SOURCE_TYPE), + DECL(AL_STATIC), + DECL(AL_STREAMING), + DECL(AL_UNDETERMINED), + DECL(AL_METERS_PER_UNIT), + DECL(AL_LOOP_POINTS_SOFT), + DECL(AL_DIRECT_CHANNELS_SOFT), + + DECL(AL_DIRECT_FILTER), + DECL(AL_AUXILIARY_SEND_FILTER), + DECL(AL_AIR_ABSORPTION_FACTOR), + DECL(AL_ROOM_ROLLOFF_FACTOR), + DECL(AL_CONE_OUTER_GAINHF), + DECL(AL_DIRECT_FILTER_GAINHF_AUTO), + DECL(AL_AUXILIARY_SEND_FILTER_GAIN_AUTO), + DECL(AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO), + + DECL(AL_SOURCE_STATE), + DECL(AL_INITIAL), + DECL(AL_PLAYING), + DECL(AL_PAUSED), + DECL(AL_STOPPED), + + DECL(AL_BUFFERS_QUEUED), + DECL(AL_BUFFERS_PROCESSED), + + DECL(AL_FORMAT_MONO8), + DECL(AL_FORMAT_MONO16), + DECL(AL_FORMAT_MONO_FLOAT32), + DECL(AL_FORMAT_MONO_DOUBLE_EXT), + DECL(AL_FORMAT_STEREO8), + DECL(AL_FORMAT_STEREO16), + DECL(AL_FORMAT_STEREO_FLOAT32), + DECL(AL_FORMAT_STEREO_DOUBLE_EXT), + DECL(AL_FORMAT_MONO_IMA4), + DECL(AL_FORMAT_STEREO_IMA4), + DECL(AL_FORMAT_MONO_MSADPCM_SOFT), + DECL(AL_FORMAT_STEREO_MSADPCM_SOFT), + DECL(AL_FORMAT_QUAD8_LOKI), + DECL(AL_FORMAT_QUAD16_LOKI), + DECL(AL_FORMAT_QUAD8), + DECL(AL_FORMAT_QUAD16), + DECL(AL_FORMAT_QUAD32), + DECL(AL_FORMAT_51CHN8), + DECL(AL_FORMAT_51CHN16), + DECL(AL_FORMAT_51CHN32), + DECL(AL_FORMAT_61CHN8), + DECL(AL_FORMAT_61CHN16), + DECL(AL_FORMAT_61CHN32), + DECL(AL_FORMAT_71CHN8), + DECL(AL_FORMAT_71CHN16), + DECL(AL_FORMAT_71CHN32), + DECL(AL_FORMAT_REAR8), + DECL(AL_FORMAT_REAR16), + DECL(AL_FORMAT_REAR32), + DECL(AL_FORMAT_MONO_MULAW), + DECL(AL_FORMAT_MONO_MULAW_EXT), + DECL(AL_FORMAT_STEREO_MULAW), + DECL(AL_FORMAT_STEREO_MULAW_EXT), + DECL(AL_FORMAT_QUAD_MULAW), + DECL(AL_FORMAT_51CHN_MULAW), + DECL(AL_FORMAT_61CHN_MULAW), + DECL(AL_FORMAT_71CHN_MULAW), + DECL(AL_FORMAT_REAR_MULAW), + DECL(AL_FORMAT_MONO_ALAW_EXT), + DECL(AL_FORMAT_STEREO_ALAW_EXT), + + DECL(AL_FORMAT_BFORMAT2D_8), + DECL(AL_FORMAT_BFORMAT2D_16), + DECL(AL_FORMAT_BFORMAT2D_FLOAT32), + DECL(AL_FORMAT_BFORMAT2D_MULAW), + DECL(AL_FORMAT_BFORMAT3D_8), + DECL(AL_FORMAT_BFORMAT3D_16), + DECL(AL_FORMAT_BFORMAT3D_FLOAT32), + DECL(AL_FORMAT_BFORMAT3D_MULAW), + + DECL(AL_FREQUENCY), + DECL(AL_BITS), + DECL(AL_CHANNELS), + DECL(AL_SIZE), + DECL(AL_UNPACK_BLOCK_ALIGNMENT_SOFT), + DECL(AL_PACK_BLOCK_ALIGNMENT_SOFT), + + DECL(AL_SOURCE_RADIUS), + + DECL(AL_STEREO_ANGLES), + + DECL(AL_UNUSED), + DECL(AL_PENDING), + DECL(AL_PROCESSED), + + DECL(AL_NO_ERROR), + DECL(AL_INVALID_NAME), + DECL(AL_INVALID_ENUM), + DECL(AL_INVALID_VALUE), + DECL(AL_INVALID_OPERATION), + DECL(AL_OUT_OF_MEMORY), + + DECL(AL_VENDOR), + DECL(AL_VERSION), + DECL(AL_RENDERER), + DECL(AL_EXTENSIONS), + + DECL(AL_DOPPLER_FACTOR), + DECL(AL_DOPPLER_VELOCITY), + DECL(AL_DISTANCE_MODEL), + DECL(AL_SPEED_OF_SOUND), + DECL(AL_SOURCE_DISTANCE_MODEL), + DECL(AL_DEFERRED_UPDATES_SOFT), + DECL(AL_GAIN_LIMIT_SOFT), + + DECL(AL_INVERSE_DISTANCE), + DECL(AL_INVERSE_DISTANCE_CLAMPED), + DECL(AL_LINEAR_DISTANCE), + DECL(AL_LINEAR_DISTANCE_CLAMPED), + DECL(AL_EXPONENT_DISTANCE), + DECL(AL_EXPONENT_DISTANCE_CLAMPED), + + DECL(AL_FILTER_TYPE), + DECL(AL_FILTER_NULL), + DECL(AL_FILTER_LOWPASS), + DECL(AL_FILTER_HIGHPASS), + DECL(AL_FILTER_BANDPASS), + + DECL(AL_LOWPASS_GAIN), + DECL(AL_LOWPASS_GAINHF), + + DECL(AL_HIGHPASS_GAIN), + DECL(AL_HIGHPASS_GAINLF), + + DECL(AL_BANDPASS_GAIN), + DECL(AL_BANDPASS_GAINHF), + DECL(AL_BANDPASS_GAINLF), + + DECL(AL_EFFECT_TYPE), + DECL(AL_EFFECT_NULL), + DECL(AL_EFFECT_REVERB), + DECL(AL_EFFECT_EAXREVERB), + DECL(AL_EFFECT_CHORUS), + DECL(AL_EFFECT_DISTORTION), + DECL(AL_EFFECT_ECHO), + DECL(AL_EFFECT_FLANGER), + DECL(AL_EFFECT_PITCH_SHIFTER), + DECL(AL_EFFECT_FREQUENCY_SHIFTER), + DECL(AL_EFFECT_VOCAL_MORPHER), + DECL(AL_EFFECT_RING_MODULATOR), + DECL(AL_EFFECT_AUTOWAH), + DECL(AL_EFFECT_COMPRESSOR), + DECL(AL_EFFECT_EQUALIZER), + DECL(AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT), + DECL(AL_EFFECT_DEDICATED_DIALOGUE), + + DECL(AL_EFFECTSLOT_EFFECT), + DECL(AL_EFFECTSLOT_GAIN), + DECL(AL_EFFECTSLOT_AUXILIARY_SEND_AUTO), + DECL(AL_EFFECTSLOT_NULL), + + DECL(AL_EAXREVERB_DENSITY), + DECL(AL_EAXREVERB_DIFFUSION), + DECL(AL_EAXREVERB_GAIN), + DECL(AL_EAXREVERB_GAINHF), + DECL(AL_EAXREVERB_GAINLF), + DECL(AL_EAXREVERB_DECAY_TIME), + DECL(AL_EAXREVERB_DECAY_HFRATIO), + DECL(AL_EAXREVERB_DECAY_LFRATIO), + DECL(AL_EAXREVERB_REFLECTIONS_GAIN), + DECL(AL_EAXREVERB_REFLECTIONS_DELAY), + DECL(AL_EAXREVERB_REFLECTIONS_PAN), + DECL(AL_EAXREVERB_LATE_REVERB_GAIN), + DECL(AL_EAXREVERB_LATE_REVERB_DELAY), + DECL(AL_EAXREVERB_LATE_REVERB_PAN), + DECL(AL_EAXREVERB_ECHO_TIME), + DECL(AL_EAXREVERB_ECHO_DEPTH), + DECL(AL_EAXREVERB_MODULATION_TIME), + DECL(AL_EAXREVERB_MODULATION_DEPTH), + DECL(AL_EAXREVERB_AIR_ABSORPTION_GAINHF), + DECL(AL_EAXREVERB_HFREFERENCE), + DECL(AL_EAXREVERB_LFREFERENCE), + DECL(AL_EAXREVERB_ROOM_ROLLOFF_FACTOR), + DECL(AL_EAXREVERB_DECAY_HFLIMIT), + + DECL(AL_REVERB_DENSITY), + DECL(AL_REVERB_DIFFUSION), + DECL(AL_REVERB_GAIN), + DECL(AL_REVERB_GAINHF), + DECL(AL_REVERB_DECAY_TIME), + DECL(AL_REVERB_DECAY_HFRATIO), + DECL(AL_REVERB_REFLECTIONS_GAIN), + DECL(AL_REVERB_REFLECTIONS_DELAY), + DECL(AL_REVERB_LATE_REVERB_GAIN), + DECL(AL_REVERB_LATE_REVERB_DELAY), + DECL(AL_REVERB_AIR_ABSORPTION_GAINHF), + DECL(AL_REVERB_ROOM_ROLLOFF_FACTOR), + DECL(AL_REVERB_DECAY_HFLIMIT), + + DECL(AL_CHORUS_WAVEFORM), + DECL(AL_CHORUS_PHASE), + DECL(AL_CHORUS_RATE), + DECL(AL_CHORUS_DEPTH), + DECL(AL_CHORUS_FEEDBACK), + DECL(AL_CHORUS_DELAY), + + DECL(AL_DISTORTION_EDGE), + DECL(AL_DISTORTION_GAIN), + DECL(AL_DISTORTION_LOWPASS_CUTOFF), + DECL(AL_DISTORTION_EQCENTER), + DECL(AL_DISTORTION_EQBANDWIDTH), + + DECL(AL_ECHO_DELAY), + DECL(AL_ECHO_LRDELAY), + DECL(AL_ECHO_DAMPING), + DECL(AL_ECHO_FEEDBACK), + DECL(AL_ECHO_SPREAD), + + DECL(AL_FLANGER_WAVEFORM), + DECL(AL_FLANGER_PHASE), + DECL(AL_FLANGER_RATE), + DECL(AL_FLANGER_DEPTH), + DECL(AL_FLANGER_FEEDBACK), + DECL(AL_FLANGER_DELAY), + + DECL(AL_FREQUENCY_SHIFTER_FREQUENCY), + DECL(AL_FREQUENCY_SHIFTER_LEFT_DIRECTION), + DECL(AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION), + + DECL(AL_RING_MODULATOR_FREQUENCY), + DECL(AL_RING_MODULATOR_HIGHPASS_CUTOFF), + DECL(AL_RING_MODULATOR_WAVEFORM), + + DECL(AL_PITCH_SHIFTER_COARSE_TUNE), + DECL(AL_PITCH_SHIFTER_FINE_TUNE), + + DECL(AL_COMPRESSOR_ONOFF), + + DECL(AL_EQUALIZER_LOW_GAIN), + DECL(AL_EQUALIZER_LOW_CUTOFF), + DECL(AL_EQUALIZER_MID1_GAIN), + DECL(AL_EQUALIZER_MID1_CENTER), + DECL(AL_EQUALIZER_MID1_WIDTH), + DECL(AL_EQUALIZER_MID2_GAIN), + DECL(AL_EQUALIZER_MID2_CENTER), + DECL(AL_EQUALIZER_MID2_WIDTH), + DECL(AL_EQUALIZER_HIGH_GAIN), + DECL(AL_EQUALIZER_HIGH_CUTOFF), + + DECL(AL_DEDICATED_GAIN), + + DECL(AL_AUTOWAH_ATTACK_TIME), + DECL(AL_AUTOWAH_RELEASE_TIME), + DECL(AL_AUTOWAH_RESONANCE), + DECL(AL_AUTOWAH_PEAK_GAIN), + + DECL(AL_NUM_RESAMPLERS_SOFT), + DECL(AL_DEFAULT_RESAMPLER_SOFT), + DECL(AL_SOURCE_RESAMPLER_SOFT), + DECL(AL_RESAMPLER_NAME_SOFT), + + DECL(AL_SOURCE_SPATIALIZE_SOFT), + DECL(AL_AUTO_SOFT), + + DECL(AL_MAP_READ_BIT_SOFT), + DECL(AL_MAP_WRITE_BIT_SOFT), + DECL(AL_MAP_PERSISTENT_BIT_SOFT), + DECL(AL_PRESERVE_DATA_BIT_SOFT), + + DECL(AL_EVENT_CALLBACK_FUNCTION_SOFT), + DECL(AL_EVENT_CALLBACK_USER_PARAM_SOFT), + DECL(AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT), + DECL(AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT), + DECL(AL_EVENT_TYPE_ERROR_SOFT), + DECL(AL_EVENT_TYPE_PERFORMANCE_SOFT), + DECL(AL_EVENT_TYPE_DEPRECATED_SOFT), +}; +#undef DECL + +constexpr ALCchar alcNoError[] = "No Error"; +constexpr ALCchar alcErrInvalidDevice[] = "Invalid Device"; +constexpr ALCchar alcErrInvalidContext[] = "Invalid Context"; +constexpr ALCchar alcErrInvalidEnum[] = "Invalid Enum"; +constexpr ALCchar alcErrInvalidValue[] = "Invalid Value"; +constexpr ALCchar alcErrOutOfMemory[] = "Out of Memory"; + + +/************************************************ + * Global variables + ************************************************/ + +/* Enumerated device names */ +constexpr ALCchar alcDefaultName[] = "OpenAL Soft\0"; + +std::string alcAllDevicesList; +std::string alcCaptureDeviceList; + +/* Default is always the first in the list */ +std::string alcDefaultAllDevicesSpecifier; +std::string alcCaptureDefaultDeviceSpecifier; + +/* Default context extensions */ +constexpr ALchar alExtList[] = + "AL_EXT_ALAW " + "AL_EXT_BFORMAT " + "AL_EXT_DOUBLE " + "AL_EXT_EXPONENT_DISTANCE " + "AL_EXT_FLOAT32 " + "AL_EXT_IMA4 " + "AL_EXT_LINEAR_DISTANCE " + "AL_EXT_MCFORMATS " + "AL_EXT_MULAW " + "AL_EXT_MULAW_BFORMAT " + "AL_EXT_MULAW_MCFORMATS " + "AL_EXT_OFFSET " + "AL_EXT_source_distance_model " + "AL_EXT_SOURCE_RADIUS " + "AL_EXT_STEREO_ANGLES " + "AL_LOKI_quadriphonic " + "AL_SOFT_block_alignment " + "AL_SOFT_deferred_updates " + "AL_SOFT_direct_channels " + "AL_SOFTX_effect_chain " + "AL_SOFTX_events " + "AL_SOFTX_filter_gain_ex " + "AL_SOFT_gain_clamp_ex " + "AL_SOFT_loop_points " + "AL_SOFTX_map_buffer " + "AL_SOFT_MSADPCM " + "AL_SOFT_source_latency " + "AL_SOFT_source_length " + "AL_SOFT_source_resampler " + "AL_SOFT_source_spatialize"; + +std::atomic<ALCenum> LastNullDeviceError{ALC_NO_ERROR}; + +/* Thread-local current context */ +void ReleaseThreadCtx(ALCcontext *context) +{ + auto ref = DecrementRef(&context->ref); + TRACEREF("ALCcontext %p decreasing refcount to %u\n", context, ref); + ERR("Context %p current for thread being destroyed, possible leak!\n", context); +} + +std::atomic<void(*)(ALCcontext*)> ThreadCtxProc{ReleaseThreadCtx}; +class ThreadCtx { + ALCcontext *ctx{nullptr}; + +public: + ~ThreadCtx() + { + auto destruct = ThreadCtxProc.load(); + if(destruct && ctx) + destruct(ctx); + ctx = nullptr; + } + + ALCcontext *get() const noexcept { return ctx; } + void set(ALCcontext *ctx_) noexcept { ctx = ctx_; } +}; +thread_local ThreadCtx LocalContext; +/* Process-wide current context */ +std::atomic<ALCcontext*> GlobalContext{nullptr}; + +/* Flag to trap ALC device errors */ +bool TrapALCError{false}; + +/* One-time configuration init control */ +std::once_flag alc_config_once{}; + +/* Default effect that applies to sources that don't have an effect on send 0 */ +ALeffect DefaultEffect; + +/* Flag to specify if alcSuspendContext/alcProcessContext should defer/process + * updates. + */ +bool SuspendDefers{true}; + + +/************************************************ + * ALC information + ************************************************/ +constexpr ALCchar alcNoDeviceExtList[] = + "ALC_ENUMERATE_ALL_EXT " + "ALC_ENUMERATION_EXT " + "ALC_EXT_CAPTURE " + "ALC_EXT_thread_local_context " + "ALC_SOFT_loopback"; +constexpr ALCchar alcExtensionList[] = + "ALC_ENUMERATE_ALL_EXT " + "ALC_ENUMERATION_EXT " + "ALC_EXT_CAPTURE " + "ALC_EXT_DEDICATED " + "ALC_EXT_disconnect " + "ALC_EXT_EFX " + "ALC_EXT_thread_local_context " + "ALC_SOFT_device_clock " + "ALC_SOFT_HRTF " + "ALC_SOFT_loopback " + "ALC_SOFT_output_limiter " + "ALC_SOFT_pause_device"; +constexpr ALCint alcMajorVersion = 1; +constexpr ALCint alcMinorVersion = 1; + +constexpr ALCint alcEFXMajorVersion = 1; +constexpr ALCint alcEFXMinorVersion = 0; + + +/* To avoid extraneous allocations, a 0-sized FlexArray<ALCcontext*> is defined + * globally as a sharable object. + */ +al::FlexArray<ALCcontext*> EmptyContextArray{0u}; + + +void ALCdevice_IncRef(ALCdevice *device) +{ + auto ref = IncrementRef(&device->ref); + TRACEREF("ALCdevice %p increasing refcount to %u\n", device, ref); +} + +void ALCdevice_DecRef(ALCdevice *device) +{ + auto ref = DecrementRef(&device->ref); + TRACEREF("ALCdevice %p decreasing refcount to %u\n", device, ref); + if(UNLIKELY(ref == 0)) delete device; +} + +/* Simple RAII device reference. Takes the reference of the provided ALCdevice, + * and decrements it when leaving scope. Movable (transfer reference) but not + * copyable (no new references). + */ +class DeviceRef { + ALCdevice *mDev{nullptr}; + + void reset() noexcept + { + if(mDev) + ALCdevice_DecRef(mDev); + mDev = nullptr; + } + +public: + DeviceRef() noexcept = default; + DeviceRef(DeviceRef&& rhs) noexcept : mDev{rhs.mDev} + { rhs.mDev = nullptr; } + explicit DeviceRef(ALCdevice *dev) noexcept : mDev(dev) { } + ~DeviceRef() { reset(); } + + DeviceRef& operator=(const DeviceRef&) = delete; + DeviceRef& operator=(DeviceRef&& rhs) noexcept + { + std::swap(mDev, rhs.mDev); + return *this; + } + + operator bool() const noexcept { return mDev != nullptr; } + + ALCdevice* operator->() const noexcept { return mDev; } + ALCdevice* get() const noexcept { return mDev; } + + ALCdevice* release() noexcept + { + ALCdevice *ret{mDev}; + mDev = nullptr; + return ret; + } +}; + +inline bool operator==(const DeviceRef &lhs, const ALCdevice *rhs) noexcept +{ return lhs.get() == rhs; } +inline bool operator!=(const DeviceRef &lhs, const ALCdevice *rhs) noexcept +{ return !(lhs == rhs); } +inline bool operator<(const DeviceRef &lhs, const ALCdevice *rhs) noexcept +{ return lhs.get() < rhs; } + + +/************************************************ + * Device lists + ************************************************/ +al::vector<DeviceRef> DeviceList; +al::vector<ContextRef> ContextList; + +std::recursive_mutex ListLock; + + +void alc_initconfig(void) +{ + const char *str{getenv("ALSOFT_LOGLEVEL")}; + if(str) + { + long lvl = strtol(str, nullptr, 0); + if(lvl >= NoLog && lvl <= LogRef) + gLogLevel = static_cast<LogLevel>(lvl); + } + + str = getenv("ALSOFT_LOGFILE"); + if(str && str[0]) + { +#ifdef _WIN32 + std::wstring wname{utf8_to_wstr(str)}; + FILE *logfile = _wfopen(wname.c_str(), L"wt"); +#else + FILE *logfile = fopen(str, "wt"); +#endif + if(logfile) gLogFile = logfile; + else ERR("Failed to open log file '%s'\n", str); + } + + TRACE("Initializing library v%s-%s %s\n", ALSOFT_VERSION, ALSOFT_GIT_COMMIT_HASH, + ALSOFT_GIT_BRANCH); + { + std::string names; + if(std::begin(BackendList) == BackendListEnd) + names += "(none)"; + else + { + const al::span<const BackendInfo> infos{std::begin(BackendList), BackendListEnd}; + names += infos[0].name; + for(const auto &backend : infos.subspan(1)) + { + names += ", "; + names += backend.name; + } + } + TRACE("Supported backends: %s\n", names.c_str()); + } + ReadALConfig(); + + str = getenv("__ALSOFT_SUSPEND_CONTEXT"); + if(str && *str) + { + if(strcasecmp(str, "ignore") == 0) + { + SuspendDefers = false; + TRACE("Selected context suspend behavior, \"ignore\"\n"); + } + else + ERR("Unhandled context suspend behavior setting: \"%s\"\n", str); + } + + int capfilter{0}; +#if defined(HAVE_SSE4_1) + capfilter |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3 | CPU_CAP_SSE4_1; +#elif defined(HAVE_SSE3) + capfilter |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3; +#elif defined(HAVE_SSE2) + capfilter |= CPU_CAP_SSE | CPU_CAP_SSE2; +#elif defined(HAVE_SSE) + capfilter |= CPU_CAP_SSE; +#endif +#ifdef HAVE_NEON + capfilter |= CPU_CAP_NEON; +#endif + if(auto cpuopt = ConfigValueStr(nullptr, nullptr, "disable-cpu-exts")) + { + str = cpuopt->c_str(); + if(strcasecmp(str, "all") == 0) + capfilter = 0; + else + { + const char *next = str; + do { + str = next; + while(isspace(str[0])) + str++; + next = strchr(str, ','); + + if(!str[0] || str[0] == ',') + continue; + + size_t len{next ? static_cast<size_t>(next-str) : strlen(str)}; + while(len > 0 && isspace(str[len-1])) + len--; + if(len == 3 && strncasecmp(str, "sse", len) == 0) + capfilter &= ~CPU_CAP_SSE; + else if(len == 4 && strncasecmp(str, "sse2", len) == 0) + capfilter &= ~CPU_CAP_SSE2; + else if(len == 4 && strncasecmp(str, "sse3", len) == 0) + capfilter &= ~CPU_CAP_SSE3; + else if(len == 6 && strncasecmp(str, "sse4.1", len) == 0) + capfilter &= ~CPU_CAP_SSE4_1; + else if(len == 4 && strncasecmp(str, "neon", len) == 0) + capfilter &= ~CPU_CAP_NEON; + else + WARN("Invalid CPU extension \"%s\"\n", str); + } while(next++); + } + } + FillCPUCaps(capfilter); + +#ifdef _WIN32 +#define DEF_MIXER_PRIO 1 +#else +#define DEF_MIXER_PRIO 0 +#endif + RTPrioLevel = ConfigValueInt(nullptr, nullptr, "rt-prio").value_or(DEF_MIXER_PRIO); +#undef DEF_MIXER_PRIO + + aluInit(); + aluInitMixer(); + + str = getenv("ALSOFT_TRAP_ERROR"); + if(str && (strcasecmp(str, "true") == 0 || strtol(str, nullptr, 0) == 1)) + { + TrapALError = true; + TrapALCError = true; + } + else + { + str = getenv("ALSOFT_TRAP_AL_ERROR"); + if(str && (strcasecmp(str, "true") == 0 || strtol(str, nullptr, 0) == 1)) + TrapALError = true; + TrapALError = !!GetConfigValueBool(nullptr, nullptr, "trap-al-error", TrapALError); + + str = getenv("ALSOFT_TRAP_ALC_ERROR"); + if(str && (strcasecmp(str, "true") == 0 || strtol(str, nullptr, 0) == 1)) + TrapALCError = true; + TrapALCError = !!GetConfigValueBool(nullptr, nullptr, "trap-alc-error", TrapALCError); + } + + if(auto boostopt = ConfigValueFloat(nullptr, "reverb", "boost")) + { + const float valf{std::isfinite(*boostopt) ? clampf(*boostopt, -24.0f, 24.0f) : 0.0f}; + ReverbBoost *= std::pow(10.0f, valf / 20.0f); + } + + auto devopt = ConfigValueStr(nullptr, nullptr, "drivers"); + if(const char *devs{getenv("ALSOFT_DRIVERS")}) + { + if(devs[0]) + devopt = devs; + } + if(devopt) + { + auto backendlist_cur = std::begin(BackendList); + + bool endlist{true}; + const char *next{devopt->c_str()}; + do { + const char *devs{next}; + while(isspace(devs[0])) + devs++; + next = strchr(devs, ','); + + const bool delitem{devs[0] == '-'}; + if(devs[0] == '-') devs++; + + if(!devs[0] || devs[0] == ',') + { + endlist = false; + continue; + } + endlist = true; + + size_t len{next ? (static_cast<size_t>(next-devs)) : strlen(devs)}; + while(len > 0 && isspace(devs[len-1])) --len; +#ifdef HAVE_WASAPI + /* HACK: For backwards compatibility, convert backend references of + * mmdevapi to wasapi. This should eventually be removed. + */ + if(len == 8 && strncmp(devs, "mmdevapi", len) == 0) + { + devs = "wasapi"; + len = 6; + } +#endif + + auto find_backend = [devs,len](const BackendInfo &backend) -> bool + { return len == strlen(backend.name) && strncmp(backend.name, devs, len) == 0; }; + auto this_backend = std::find_if(std::begin(BackendList), BackendListEnd, + find_backend); + + if(this_backend == BackendListEnd) + continue; + + if(delitem) + BackendListEnd = std::move(this_backend+1, BackendListEnd, this_backend); + else + backendlist_cur = std::rotate(backendlist_cur, this_backend, this_backend+1); + } while(next++); + + if(endlist) + BackendListEnd = backendlist_cur; + } + + auto init_backend = [](BackendInfo &backend) -> bool + { + if(PlaybackFactory && CaptureFactory) + return true; + + BackendFactory &factory = backend.getFactory(); + if(!factory.init()) + { + WARN("Failed to initialize backend \"%s\"\n", backend.name); + return true; + } + + TRACE("Initialized backend \"%s\"\n", backend.name); + if(!PlaybackFactory && factory.querySupport(BackendType::Playback)) + { + PlaybackFactory = &factory; + TRACE("Added \"%s\" for playback\n", backend.name); + } + if(!CaptureFactory && factory.querySupport(BackendType::Capture)) + { + CaptureFactory = &factory; + TRACE("Added \"%s\" for capture\n", backend.name); + } + return false; + }; + BackendListEnd = std::remove_if(std::begin(BackendList), BackendListEnd, init_backend); + + LoopbackBackendFactory::getFactory().init(); + + if(!PlaybackFactory) + WARN("No playback backend available!\n"); + if(!CaptureFactory) + WARN("No capture backend available!\n"); + + if(auto exclopt = ConfigValueStr(nullptr, nullptr, "excludefx")) + { + const char *next{exclopt->c_str()}; + do { + str = next; + next = strchr(str, ','); + + if(!str[0] || next == str) + continue; + + size_t len{next ? static_cast<size_t>(next-str) : strlen(str)}; + for(const EffectList &effectitem : gEffectList) + { + if(len == strlen(effectitem.name) && + strncmp(effectitem.name, str, len) == 0) + DisabledEffects[effectitem.type] = AL_TRUE; + } + } while(next++); + } + + InitEffect(&DefaultEffect); + auto defrevopt = ConfigValueStr(nullptr, nullptr, "default-reverb"); + if((str=getenv("ALSOFT_DEFAULT_REVERB")) && str[0]) + defrevopt = str; + if(defrevopt) LoadReverbPreset(defrevopt->c_str(), &DefaultEffect); +} +#define DO_INITCONFIG() std::call_once(alc_config_once, [](){alc_initconfig();}) + + +/************************************************ + * Device enumeration + ************************************************/ +void ProbeAllDevicesList() +{ + DO_INITCONFIG(); + + std::lock_guard<std::recursive_mutex> _{ListLock}; + alcAllDevicesList.clear(); + if(PlaybackFactory) + PlaybackFactory->probe(DevProbe::Playback, &alcAllDevicesList); +} +void ProbeCaptureDeviceList() +{ + DO_INITCONFIG(); + + std::lock_guard<std::recursive_mutex> _{ListLock}; + alcCaptureDeviceList.clear(); + if(CaptureFactory) + CaptureFactory->probe(DevProbe::Capture, &alcCaptureDeviceList); +} + +} // namespace + +/* Mixing thread piority level */ +ALint RTPrioLevel; + +FILE *gLogFile{stderr}; +#ifdef _DEBUG +LogLevel gLogLevel{LogWarning}; +#else +LogLevel gLogLevel{LogError}; +#endif + +/************************************************ + * Library initialization + ************************************************/ +#if defined(_WIN32) && !defined(AL_LIBTYPE_STATIC) +BOOL APIENTRY DllMain(HINSTANCE module, DWORD reason, LPVOID /*reserved*/) +{ + switch(reason) + { + case DLL_PROCESS_ATTACH: + /* Pin the DLL so we won't get unloaded until the process terminates */ + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (WCHAR*)module, &module); + break; + + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} +#endif + +/************************************************ + * Device format information + ************************************************/ +const ALCchar *DevFmtTypeString(DevFmtType type) noexcept +{ + switch(type) + { + case DevFmtByte: return "Signed Byte"; + case DevFmtUByte: return "Unsigned Byte"; + case DevFmtShort: return "Signed Short"; + case DevFmtUShort: return "Unsigned Short"; + case DevFmtInt: return "Signed Int"; + case DevFmtUInt: return "Unsigned Int"; + case DevFmtFloat: return "Float"; + } + return "(unknown type)"; +} +const ALCchar *DevFmtChannelsString(DevFmtChannels chans) noexcept +{ + switch(chans) + { + case DevFmtMono: return "Mono"; + case DevFmtStereo: return "Stereo"; + case DevFmtQuad: return "Quadraphonic"; + case DevFmtX51: return "5.1 Surround"; + case DevFmtX51Rear: return "5.1 Surround (Rear)"; + case DevFmtX61: return "6.1 Surround"; + case DevFmtX71: return "7.1 Surround"; + case DevFmtAmbi3D: return "Ambisonic 3D"; + } + return "(unknown channels)"; +} + +ALsizei BytesFromDevFmt(DevFmtType type) noexcept +{ + switch(type) + { + case DevFmtByte: return sizeof(ALbyte); + case DevFmtUByte: return sizeof(ALubyte); + case DevFmtShort: return sizeof(ALshort); + case DevFmtUShort: return sizeof(ALushort); + case DevFmtInt: return sizeof(ALint); + case DevFmtUInt: return sizeof(ALuint); + case DevFmtFloat: return sizeof(ALfloat); + } + return 0; +} +ALsizei ChannelsFromDevFmt(DevFmtChannels chans, ALsizei ambiorder) noexcept +{ + switch(chans) + { + case DevFmtMono: return 1; + case DevFmtStereo: return 2; + case DevFmtQuad: return 4; + case DevFmtX51: return 6; + case DevFmtX51Rear: return 6; + case DevFmtX61: return 7; + case DevFmtX71: return 8; + case DevFmtAmbi3D: return (ambiorder+1) * (ambiorder+1); + } + return 0; +} + +struct DevFmtPair { DevFmtChannels chans; DevFmtType type; }; +static al::optional<DevFmtPair> DecomposeDevFormat(ALenum format) +{ + static const struct { + ALenum format; + DevFmtChannels channels; + DevFmtType type; + } list[] = { + { AL_FORMAT_MONO8, DevFmtMono, DevFmtUByte }, + { AL_FORMAT_MONO16, DevFmtMono, DevFmtShort }, + { AL_FORMAT_MONO_FLOAT32, DevFmtMono, DevFmtFloat }, + + { AL_FORMAT_STEREO8, DevFmtStereo, DevFmtUByte }, + { AL_FORMAT_STEREO16, DevFmtStereo, DevFmtShort }, + { AL_FORMAT_STEREO_FLOAT32, DevFmtStereo, DevFmtFloat }, + + { AL_FORMAT_QUAD8, DevFmtQuad, DevFmtUByte }, + { AL_FORMAT_QUAD16, DevFmtQuad, DevFmtShort }, + { AL_FORMAT_QUAD32, DevFmtQuad, DevFmtFloat }, + + { AL_FORMAT_51CHN8, DevFmtX51, DevFmtUByte }, + { AL_FORMAT_51CHN16, DevFmtX51, DevFmtShort }, + { AL_FORMAT_51CHN32, DevFmtX51, DevFmtFloat }, + + { AL_FORMAT_61CHN8, DevFmtX61, DevFmtUByte }, + { AL_FORMAT_61CHN16, DevFmtX61, DevFmtShort }, + { AL_FORMAT_61CHN32, DevFmtX61, DevFmtFloat }, + + { AL_FORMAT_71CHN8, DevFmtX71, DevFmtUByte }, + { AL_FORMAT_71CHN16, DevFmtX71, DevFmtShort }, + { AL_FORMAT_71CHN32, DevFmtX71, DevFmtFloat }, + }; + + for(const auto &item : list) + { + if(item.format == format) + return al::make_optional(DevFmtPair{item.channels, item.type}); + } + + return al::nullopt; +} + +static ALCboolean IsValidALCType(ALCenum type) +{ + switch(type) + { + case ALC_BYTE_SOFT: + case ALC_UNSIGNED_BYTE_SOFT: + case ALC_SHORT_SOFT: + case ALC_UNSIGNED_SHORT_SOFT: + case ALC_INT_SOFT: + case ALC_UNSIGNED_INT_SOFT: + case ALC_FLOAT_SOFT: + return ALC_TRUE; + } + return ALC_FALSE; +} + +static ALCboolean IsValidALCChannels(ALCenum channels) +{ + switch(channels) + { + case ALC_MONO_SOFT: + case ALC_STEREO_SOFT: + case ALC_QUAD_SOFT: + case ALC_5POINT1_SOFT: + case ALC_6POINT1_SOFT: + case ALC_7POINT1_SOFT: + case ALC_BFORMAT3D_SOFT: + return ALC_TRUE; + } + return ALC_FALSE; +} + +static ALCboolean IsValidAmbiLayout(ALCenum layout) +{ + switch(layout) + { + case ALC_ACN_SOFT: + case ALC_FUMA_SOFT: + return ALC_TRUE; + } + return ALC_FALSE; +} + +static ALCboolean IsValidAmbiScaling(ALCenum scaling) +{ + switch(scaling) + { + case ALC_N3D_SOFT: + case ALC_SN3D_SOFT: + case ALC_FUMA_SOFT: + return ALC_TRUE; + } + return ALC_FALSE; +} + +/************************************************ + * Miscellaneous ALC helpers + ************************************************/ + +/* SetDefaultWFXChannelOrder + * + * Sets the default channel order used by WaveFormatEx. + */ +void SetDefaultWFXChannelOrder(ALCdevice *device) +{ + device->RealOut.ChannelIndex.fill(-1); + + switch(device->FmtChans) + { + case DevFmtMono: + device->RealOut.ChannelIndex[FrontCenter] = 0; + break; + case DevFmtStereo: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + break; + case DevFmtQuad: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + device->RealOut.ChannelIndex[BackLeft] = 2; + device->RealOut.ChannelIndex[BackRight] = 3; + break; + case DevFmtX51: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + device->RealOut.ChannelIndex[FrontCenter] = 2; + device->RealOut.ChannelIndex[LFE] = 3; + device->RealOut.ChannelIndex[SideLeft] = 4; + device->RealOut.ChannelIndex[SideRight] = 5; + break; + case DevFmtX51Rear: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + device->RealOut.ChannelIndex[FrontCenter] = 2; + device->RealOut.ChannelIndex[LFE] = 3; + device->RealOut.ChannelIndex[BackLeft] = 4; + device->RealOut.ChannelIndex[BackRight] = 5; + break; + case DevFmtX61: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + device->RealOut.ChannelIndex[FrontCenter] = 2; + device->RealOut.ChannelIndex[LFE] = 3; + device->RealOut.ChannelIndex[BackCenter] = 4; + device->RealOut.ChannelIndex[SideLeft] = 5; + device->RealOut.ChannelIndex[SideRight] = 6; + break; + case DevFmtX71: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + device->RealOut.ChannelIndex[FrontCenter] = 2; + device->RealOut.ChannelIndex[LFE] = 3; + device->RealOut.ChannelIndex[BackLeft] = 4; + device->RealOut.ChannelIndex[BackRight] = 5; + device->RealOut.ChannelIndex[SideLeft] = 6; + device->RealOut.ChannelIndex[SideRight] = 7; + break; + case DevFmtAmbi3D: + device->RealOut.ChannelIndex[Aux0] = 0; + if(device->mAmbiOrder > 0) + { + device->RealOut.ChannelIndex[Aux1] = 1; + device->RealOut.ChannelIndex[Aux2] = 2; + device->RealOut.ChannelIndex[Aux3] = 3; + } + if(device->mAmbiOrder > 1) + { + device->RealOut.ChannelIndex[Aux4] = 4; + device->RealOut.ChannelIndex[Aux5] = 5; + device->RealOut.ChannelIndex[Aux6] = 6; + device->RealOut.ChannelIndex[Aux7] = 7; + device->RealOut.ChannelIndex[Aux8] = 8; + } + if(device->mAmbiOrder > 2) + { + device->RealOut.ChannelIndex[Aux9] = 9; + device->RealOut.ChannelIndex[Aux10] = 10; + device->RealOut.ChannelIndex[Aux11] = 11; + device->RealOut.ChannelIndex[Aux12] = 12; + device->RealOut.ChannelIndex[Aux13] = 13; + device->RealOut.ChannelIndex[Aux14] = 14; + device->RealOut.ChannelIndex[Aux15] = 15; + } + break; + } +} + +/* SetDefaultChannelOrder + * + * Sets the default channel order used by most non-WaveFormatEx-based APIs. + */ +void SetDefaultChannelOrder(ALCdevice *device) +{ + device->RealOut.ChannelIndex.fill(-1); + + switch(device->FmtChans) + { + case DevFmtX51Rear: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + device->RealOut.ChannelIndex[BackLeft] = 2; + device->RealOut.ChannelIndex[BackRight] = 3; + device->RealOut.ChannelIndex[FrontCenter] = 4; + device->RealOut.ChannelIndex[LFE] = 5; + return; + case DevFmtX71: + device->RealOut.ChannelIndex[FrontLeft] = 0; + device->RealOut.ChannelIndex[FrontRight] = 1; + device->RealOut.ChannelIndex[BackLeft] = 2; + device->RealOut.ChannelIndex[BackRight] = 3; + device->RealOut.ChannelIndex[FrontCenter] = 4; + device->RealOut.ChannelIndex[LFE] = 5; + device->RealOut.ChannelIndex[SideLeft] = 6; + device->RealOut.ChannelIndex[SideRight] = 7; + return; + + /* Same as WFX order */ + case DevFmtMono: + case DevFmtStereo: + case DevFmtQuad: + case DevFmtX51: + case DevFmtX61: + case DevFmtAmbi3D: + SetDefaultWFXChannelOrder(device); + break; + } +} + + +/* ALCcontext_DeferUpdates + * + * Defers/suspends updates for the given context's listener and sources. This + * does *NOT* stop mixing, but rather prevents certain property changes from + * taking effect. + */ +void ALCcontext_DeferUpdates(ALCcontext *context) +{ + context->DeferUpdates.store(true); +} + +/* ALCcontext_ProcessUpdates + * + * Resumes update processing after being deferred. + */ +void ALCcontext_ProcessUpdates(ALCcontext *context) +{ + std::lock_guard<std::mutex> _{context->PropLock}; + if(context->DeferUpdates.exchange(false)) + { + /* Tell the mixer to stop applying updates, then wait for any active + * updating to finish, before providing updates. + */ + context->HoldUpdates.store(true, std::memory_order_release); + while((context->UpdateCount.load(std::memory_order_acquire)&1) != 0) + std::this_thread::yield(); + + if(!context->PropsClean.test_and_set(std::memory_order_acq_rel)) + UpdateContextProps(context); + if(!context->Listener.PropsClean.test_and_set(std::memory_order_acq_rel)) + UpdateListenerProps(context); + UpdateAllEffectSlotProps(context); + UpdateAllSourceProps(context); + + /* Now with all updates declared, let the mixer continue applying them + * so they all happen at once. + */ + context->HoldUpdates.store(false, std::memory_order_release); + } +} + + +/* alcSetError + * + * Stores the latest ALC device error + */ +static void alcSetError(ALCdevice *device, ALCenum errorCode) +{ + WARN("Error generated on device %p, code 0x%04x\n", device, errorCode); + if(TrapALCError) + { +#ifdef _WIN32 + /* DebugBreak() will cause an exception if there is no debugger */ + if(IsDebuggerPresent()) + DebugBreak(); +#elif defined(SIGTRAP) + raise(SIGTRAP); +#endif + } + + if(device) + device->LastError.store(errorCode); + else + LastNullDeviceError.store(errorCode); +} + + +static std::unique_ptr<Compressor> CreateDeviceLimiter(const ALCdevice *device, const ALfloat threshold) +{ + return CompressorInit(static_cast<ALuint>(device->RealOut.Buffer.size()), device->Frequency, + AL_TRUE, AL_TRUE, AL_TRUE, AL_TRUE, AL_TRUE, 0.001f, 0.002f, 0.0f, 0.0f, threshold, + INFINITY, 0.0f, 0.020f, 0.200f); +} + +/* UpdateClockBase + * + * Updates the device's base clock time with however many samples have been + * done. This is used so frequency changes on the device don't cause the time + * to jump forward or back. Must not be called while the device is running/ + * mixing. + */ +static inline void UpdateClockBase(ALCdevice *device) +{ + IncrementRef(&device->MixCount); + device->ClockBase += nanoseconds{seconds{device->SamplesDone}} / device->Frequency; + device->SamplesDone = 0; + IncrementRef(&device->MixCount); +} + +/* UpdateDeviceParams + * + * Updates device parameters according to the attribute list (caller is + * responsible for holding the list lock). + */ +static ALCenum UpdateDeviceParams(ALCdevice *device, const ALCint *attrList) +{ + HrtfRequestMode hrtf_userreq = Hrtf_Default; + HrtfRequestMode hrtf_appreq = Hrtf_Default; + ALCenum gainLimiter = device->LimiterState; + const ALsizei old_sends = device->NumAuxSends; + ALsizei new_sends = device->NumAuxSends; + DevFmtChannels oldChans; + DevFmtType oldType; + ALboolean update_failed; + ALCsizei hrtf_id = -1; + ALCuint oldFreq; + + if((!attrList || !attrList[0]) && device->Type == Loopback) + { + WARN("Missing attributes for loopback device\n"); + return ALC_INVALID_VALUE; + } + + // Check for attributes + if(attrList && attrList[0]) + { + ALCenum alayout{AL_NONE}; + ALCenum ascale{AL_NONE}; + ALCenum schans{AL_NONE}; + ALCenum stype{AL_NONE}; + ALCsizei attrIdx{0}; + ALCsizei aorder{0}; + ALCuint freq{0u}; + + ALuint numMono{device->NumMonoSources}; + ALuint numStereo{device->NumStereoSources}; + ALsizei numSends{old_sends}; + +#define TRACE_ATTR(a, v) TRACE("%s = %d\n", #a, v) + while(attrList[attrIdx]) + { + switch(attrList[attrIdx]) + { + case ALC_FORMAT_CHANNELS_SOFT: + schans = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_FORMAT_CHANNELS_SOFT, schans); + break; + + case ALC_FORMAT_TYPE_SOFT: + stype = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_FORMAT_TYPE_SOFT, stype); + break; + + case ALC_FREQUENCY: + freq = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_FREQUENCY, freq); + break; + + case ALC_AMBISONIC_LAYOUT_SOFT: + alayout = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_AMBISONIC_LAYOUT_SOFT, alayout); + break; + + case ALC_AMBISONIC_SCALING_SOFT: + ascale = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_AMBISONIC_SCALING_SOFT, ascale); + break; + + case ALC_AMBISONIC_ORDER_SOFT: + aorder = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_AMBISONIC_ORDER_SOFT, aorder); + break; + + case ALC_MONO_SOURCES: + numMono = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_MONO_SOURCES, numMono); + if(numMono > INT_MAX) numMono = 0; + break; + + case ALC_STEREO_SOURCES: + numStereo = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_STEREO_SOURCES, numStereo); + if(numStereo > INT_MAX) numStereo = 0; + break; + + case ALC_MAX_AUXILIARY_SENDS: + numSends = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_MAX_AUXILIARY_SENDS, numSends); + numSends = clampi(numSends, 0, MAX_SENDS); + break; + + case ALC_HRTF_SOFT: + TRACE_ATTR(ALC_HRTF_SOFT, attrList[attrIdx + 1]); + if(attrList[attrIdx + 1] == ALC_FALSE) + hrtf_appreq = Hrtf_Disable; + else if(attrList[attrIdx + 1] == ALC_TRUE) + hrtf_appreq = Hrtf_Enable; + else + hrtf_appreq = Hrtf_Default; + break; + + case ALC_HRTF_ID_SOFT: + hrtf_id = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_HRTF_ID_SOFT, hrtf_id); + break; + + case ALC_OUTPUT_LIMITER_SOFT: + gainLimiter = attrList[attrIdx + 1]; + TRACE_ATTR(ALC_OUTPUT_LIMITER_SOFT, gainLimiter); + break; + + default: + TRACE("0x%04X = %d (0x%x)\n", attrList[attrIdx], + attrList[attrIdx + 1], attrList[attrIdx + 1]); + break; + } + + attrIdx += 2; + } +#undef TRACE_ATTR + + const bool loopback{device->Type == Loopback}; + if(loopback) + { + if(!schans || !stype || !freq) + { + WARN("Missing format for loopback device\n"); + return ALC_INVALID_VALUE; + } + if(!IsValidALCChannels(schans) || !IsValidALCType(stype) || freq < MIN_OUTPUT_RATE) + return ALC_INVALID_VALUE; + if(schans == ALC_BFORMAT3D_SOFT) + { + if(!alayout || !ascale || !aorder) + { + WARN("Missing ambisonic info for loopback device\n"); + return ALC_INVALID_VALUE; + } + if(!IsValidAmbiLayout(alayout) || !IsValidAmbiScaling(ascale)) + return ALC_INVALID_VALUE; + if(aorder < 1 || aorder > MAX_AMBI_ORDER) + return ALC_INVALID_VALUE; + if((alayout == ALC_FUMA_SOFT || ascale == ALC_FUMA_SOFT) && aorder > 3) + return ALC_INVALID_VALUE; + } + } + + /* If a context is already running on the device, stop playback so the + * device attributes can be updated. + */ + if(device->Flags.get<DeviceRunning>()) + device->Backend->stop(); + device->Flags.unset<DeviceRunning>(); + + UpdateClockBase(device); + + const char *devname{nullptr}; + if(!loopback) + { + devname = device->DeviceName.c_str(); + + device->BufferSize = DEFAULT_UPDATE_SIZE * DEFAULT_NUM_UPDATES; + device->UpdateSize = DEFAULT_UPDATE_SIZE; + device->Frequency = DEFAULT_OUTPUT_RATE; + + freq = ConfigValueUInt(devname, nullptr, "frequency").value_or(freq); + if(freq < 1) + device->Flags.unset<FrequencyRequest>(); + else + { + freq = maxi(freq, MIN_OUTPUT_RATE); + + device->UpdateSize = (device->UpdateSize*freq + device->Frequency/2) / + device->Frequency; + device->BufferSize = (device->BufferSize*freq + device->Frequency/2) / + device->Frequency; + + device->Frequency = freq; + device->Flags.set<FrequencyRequest>(); + } + + if(auto persizeopt = ConfigValueUInt(devname, nullptr, "period_size")) + device->UpdateSize = clampu(*persizeopt, 64, 8192); + + if(auto peropt = ConfigValueUInt(devname, nullptr, "periods")) + device->BufferSize = device->UpdateSize * clampu(*peropt, 2, 16); + else + device->BufferSize = maxu(device->BufferSize, device->UpdateSize*2); + } + else + { + device->Frequency = freq; + device->FmtChans = static_cast<DevFmtChannels>(schans); + device->FmtType = static_cast<DevFmtType>(stype); + if(schans == ALC_BFORMAT3D_SOFT) + { + device->mAmbiOrder = aorder; + device->mAmbiLayout = static_cast<AmbiLayout>(alayout); + device->mAmbiScale = static_cast<AmbiNorm>(ascale); + } + } + + if(numMono > INT_MAX-numStereo) + numMono = INT_MAX-numStereo; + numMono += numStereo; + if(auto srcsopt = ConfigValueUInt(devname, nullptr, "sources")) + { + if(*srcsopt <= 0) numMono = 256; + else numMono = *srcsopt; + } + else + numMono = maxu(numMono, 256); + numStereo = minu(numStereo, numMono); + numMono -= numStereo; + device->SourcesMax = numMono + numStereo; + + device->NumMonoSources = numMono; + device->NumStereoSources = numStereo; + + if(auto sendsopt = ConfigValueInt(devname, nullptr, "sends")) + new_sends = mini(numSends, clampi(*sendsopt, 0, MAX_SENDS)); + else + new_sends = numSends; + } + + if(device->Flags.get<DeviceRunning>()) + return ALC_NO_ERROR; + + device->AvgSpeakerDist = 0.0f; + device->Uhj_Encoder = nullptr; + device->AmbiDecoder = nullptr; + device->Bs2b = nullptr; + device->PostProcess = nullptr; + + device->Stablizer = nullptr; + device->Limiter = nullptr; + device->ChannelDelay.clear(); + + device->Dry.AmbiMap.fill(BFChannelConfig{}); + device->Dry.Buffer = {}; + std::fill(std::begin(device->NumChannelsPerOrder), std::end(device->NumChannelsPerOrder), 0u); + device->RealOut.ChannelIndex.fill(-1); + device->RealOut.Buffer = {}; + device->MixBuffer.clear(); + device->MixBuffer.shrink_to_fit(); + + UpdateClockBase(device); + device->FixedLatency = nanoseconds::zero(); + + device->DitherDepth = 0.0f; + device->DitherSeed = DITHER_RNG_SEED; + + /************************************************************************* + * Update device format request if HRTF is requested + */ + device->HrtfStatus = ALC_HRTF_DISABLED_SOFT; + if(device->Type != Loopback) + { + if(auto hrtfopt = ConfigValueStr(device->DeviceName.c_str(), nullptr, "hrtf")) + { + const char *hrtf{hrtfopt->c_str()}; + if(strcasecmp(hrtf, "true") == 0) + hrtf_userreq = Hrtf_Enable; + else if(strcasecmp(hrtf, "false") == 0) + hrtf_userreq = Hrtf_Disable; + else if(strcasecmp(hrtf, "auto") != 0) + ERR("Unexpected hrtf value: %s\n", hrtf); + } + + if(hrtf_userreq == Hrtf_Enable || (hrtf_userreq != Hrtf_Disable && hrtf_appreq == Hrtf_Enable)) + { + HrtfEntry *hrtf{nullptr}; + if(device->HrtfList.empty()) + device->HrtfList = EnumerateHrtf(device->DeviceName.c_str()); + if(!device->HrtfList.empty()) + { + if(hrtf_id >= 0 && static_cast<size_t>(hrtf_id) < device->HrtfList.size()) + hrtf = GetLoadedHrtf(device->HrtfList[hrtf_id].hrtf); + else + hrtf = GetLoadedHrtf(device->HrtfList.front().hrtf); + } + + if(hrtf) + { + device->FmtChans = DevFmtStereo; + device->Frequency = hrtf->sampleRate; + device->Flags.set<ChannelsRequest, FrequencyRequest>(); + if(HrtfEntry *oldhrtf{device->mHrtf}) + oldhrtf->DecRef(); + device->mHrtf = hrtf; + } + else + { + hrtf_userreq = Hrtf_Default; + hrtf_appreq = Hrtf_Disable; + device->HrtfStatus = ALC_HRTF_UNSUPPORTED_FORMAT_SOFT; + } + } + } + + oldFreq = device->Frequency; + oldChans = device->FmtChans; + oldType = device->FmtType; + + TRACE("Pre-reset: %s%s, %s%s, %s%uhz, %u / %u buffer\n", + device->Flags.get<ChannelsRequest>()?"*":"", DevFmtChannelsString(device->FmtChans), + device->Flags.get<SampleTypeRequest>()?"*":"", DevFmtTypeString(device->FmtType), + device->Flags.get<FrequencyRequest>()?"*":"", device->Frequency, + device->UpdateSize, device->BufferSize); + + try { + if(device->Backend->reset() == ALC_FALSE) + return ALC_INVALID_DEVICE; + } + catch(std::exception &e) { + ERR("Device reset failed: %s\n", e.what()); + return ALC_INVALID_DEVICE; + } + + if(device->FmtChans != oldChans && device->Flags.get<ChannelsRequest>()) + { + ERR("Failed to set %s, got %s instead\n", DevFmtChannelsString(oldChans), + DevFmtChannelsString(device->FmtChans)); + device->Flags.unset<ChannelsRequest>(); + } + if(device->FmtType != oldType && device->Flags.get<SampleTypeRequest>()) + { + ERR("Failed to set %s, got %s instead\n", DevFmtTypeString(oldType), + DevFmtTypeString(device->FmtType)); + device->Flags.unset<SampleTypeRequest>(); + } + if(device->Frequency != oldFreq && device->Flags.get<FrequencyRequest>()) + { + WARN("Failed to set %uhz, got %uhz instead\n", oldFreq, device->Frequency); + device->Flags.unset<FrequencyRequest>(); + } + + TRACE("Post-reset: %s, %s, %uhz, %u / %u buffer\n", + DevFmtChannelsString(device->FmtChans), DevFmtTypeString(device->FmtType), + device->Frequency, device->UpdateSize, device->BufferSize); + + aluInitRenderer(device, hrtf_id, hrtf_appreq, hrtf_userreq); + + device->NumAuxSends = new_sends; + TRACE("Max sources: %d (%d + %d), effect slots: %d, sends: %d\n", + device->SourcesMax, device->NumMonoSources, device->NumStereoSources, + device->AuxiliaryEffectSlotMax, device->NumAuxSends); + + /* Enable the stablizer only for formats that have front-left, front-right, + * and front-center outputs. + */ + switch(device->FmtChans) + { + case DevFmtX51: + case DevFmtX51Rear: + case DevFmtX61: + case DevFmtX71: + if(GetConfigValueBool(device->DeviceName.c_str(), nullptr, "front-stablizer", 0)) + { + auto stablizer = al::make_unique<FrontStablizer>(); + /* Initialize band-splitting filters for the front-left and front- + * right channels, with a crossover at 5khz (could be higher). + */ + const ALfloat scale{5000.0f / static_cast<ALfloat>(device->Frequency)}; + + stablizer->LFilter.init(scale); + stablizer->RFilter = stablizer->LFilter; + + device->Stablizer = std::move(stablizer); + /* NOTE: Don't know why this has to be "copied" into a local static + * constexpr variable to avoid a reference on + * FrontStablizer::DelayLength... + */ + static constexpr size_t StablizerDelay{FrontStablizer::DelayLength}; + device->FixedLatency += nanoseconds{seconds{StablizerDelay}} / device->Frequency; + } + break; + case DevFmtMono: + case DevFmtStereo: + case DevFmtQuad: + case DevFmtAmbi3D: + break; + } + TRACE("Front stablizer %s\n", device->Stablizer ? "enabled" : "disabled"); + + if(GetConfigValueBool(device->DeviceName.c_str(), nullptr, "dither", 1)) + { + ALint depth{ + ConfigValueInt(device->DeviceName.c_str(), nullptr, "dither-depth").value_or(0)}; + if(depth <= 0) + { + switch(device->FmtType) + { + case DevFmtByte: + case DevFmtUByte: + depth = 8; + break; + case DevFmtShort: + case DevFmtUShort: + depth = 16; + break; + case DevFmtInt: + case DevFmtUInt: + case DevFmtFloat: + break; + } + } + + if(depth > 0) + { + depth = clampi(depth, 2, 24); + device->DitherDepth = std::pow(2.0f, static_cast<ALfloat>(depth-1)); + } + } + if(!(device->DitherDepth > 0.0f)) + TRACE("Dithering disabled\n"); + else + TRACE("Dithering enabled (%d-bit, %g)\n", float2int(std::log2(device->DitherDepth)+0.5f)+1, + device->DitherDepth); + + device->LimiterState = gainLimiter; + if(auto limopt = ConfigValueBool(device->DeviceName.c_str(), nullptr, "output-limiter")) + gainLimiter = *limopt ? ALC_TRUE : ALC_FALSE; + + /* Valid values for gainLimiter are ALC_DONT_CARE_SOFT, ALC_TRUE, and + * ALC_FALSE. For ALC_DONT_CARE_SOFT, use the limiter for integer-based + * output (where samples must be clamped), and don't for floating-point + * (which can take unclamped samples). + */ + if(gainLimiter == ALC_DONT_CARE_SOFT) + { + switch(device->FmtType) + { + case DevFmtByte: + case DevFmtUByte: + case DevFmtShort: + case DevFmtUShort: + case DevFmtInt: + case DevFmtUInt: + gainLimiter = ALC_TRUE; + break; + case DevFmtFloat: + gainLimiter = ALC_FALSE; + break; + } + } + if(gainLimiter == ALC_FALSE) + TRACE("Output limiter disabled\n"); + else + { + ALfloat thrshld = 1.0f; + switch(device->FmtType) + { + case DevFmtByte: + case DevFmtUByte: + thrshld = 127.0f / 128.0f; + break; + case DevFmtShort: + case DevFmtUShort: + thrshld = 32767.0f / 32768.0f; + break; + case DevFmtInt: + case DevFmtUInt: + case DevFmtFloat: + break; + } + if(device->DitherDepth > 0.0f) + thrshld -= 1.0f / device->DitherDepth; + + const float thrshld_dB{std::log10(thrshld) * 20.0f}; + auto limiter = CreateDeviceLimiter(device, thrshld_dB); + /* Convert the lookahead from samples to nanosamples to nanoseconds. */ + device->FixedLatency += nanoseconds{seconds{limiter->getLookAhead()}} / device->Frequency; + device->Limiter = std::move(limiter); + TRACE("Output limiter enabled, %.4fdB limit\n", thrshld_dB); + } + + TRACE("Fixed device latency: %ldns\n", (long)device->FixedLatency.count()); + + /* Need to delay returning failure until replacement Send arrays have been + * allocated with the appropriate size. + */ + update_failed = AL_FALSE; + FPUCtl mixer_mode{}; + for(ALCcontext *context : *device->mContexts.load()) + { + if(context->DefaultSlot) + { + ALeffectslot *slot = context->DefaultSlot.get(); + aluInitEffectPanning(slot, device); + + EffectState *state{slot->Effect.State}; + state->mOutTarget = device->Dry.Buffer; + if(state->deviceUpdate(device) == AL_FALSE) + update_failed = AL_TRUE; + else + UpdateEffectSlotProps(slot, context); + } + + std::unique_lock<std::mutex> proplock{context->PropLock}; + std::unique_lock<std::mutex> slotlock{context->EffectSlotLock}; + for(auto &sublist : context->EffectSlotList) + { + uint64_t usemask = ~sublist.FreeMask; + while(usemask) + { + ALsizei idx = CTZ64(usemask); + ALeffectslot *slot = sublist.EffectSlots + idx; + + usemask &= ~(1_u64 << idx); + + aluInitEffectPanning(slot, device); + + EffectState *state{slot->Effect.State}; + state->mOutTarget = device->Dry.Buffer; + if(state->deviceUpdate(device) == AL_FALSE) + update_failed = AL_TRUE; + else + UpdateEffectSlotProps(slot, context); + } + } + slotlock.unlock(); + + std::unique_lock<std::mutex> srclock{context->SourceLock}; + for(auto &sublist : context->SourceList) + { + uint64_t usemask = ~sublist.FreeMask; + while(usemask) + { + ALsizei idx = CTZ64(usemask); + ALsource *source = sublist.Sources + idx; + + usemask &= ~(1_u64 << idx); + + if(old_sends != device->NumAuxSends) + { + ALsizei s; + for(s = device->NumAuxSends;s < old_sends;s++) + { + if(source->Send[s].Slot) + DecrementRef(&source->Send[s].Slot->ref); + source->Send[s].Slot = nullptr; + } + source->Send.resize(device->NumAuxSends); + source->Send.shrink_to_fit(); + for(s = old_sends;s < device->NumAuxSends;s++) + { + source->Send[s].Slot = nullptr; + source->Send[s].Gain = 1.0f; + source->Send[s].GainHF = 1.0f; + source->Send[s].HFReference = LOWPASSFREQREF; + source->Send[s].GainLF = 1.0f; + source->Send[s].LFReference = HIGHPASSFREQREF; + } + } + + source->PropsClean.clear(std::memory_order_release); + } + } + + /* Clear any pre-existing voice property structs, in case the number of + * auxiliary sends is changing. Active sources will have updates + * respecified in UpdateAllSourceProps. + */ + ALvoiceProps *vprops{context->FreeVoiceProps.exchange(nullptr, std::memory_order_acq_rel)}; + while(vprops) + { + ALvoiceProps *next = vprops->next.load(std::memory_order_relaxed); + delete vprops; + vprops = next; + } + + auto voices = context->Voices.get(); + auto voices_end = voices->begin() + context->VoiceCount.load(std::memory_order_relaxed); + if(device->NumAuxSends < old_sends) + { + const ALsizei num_sends{device->NumAuxSends}; + /* Clear extraneous property set sends. */ + auto clear_sends = [num_sends](ALvoice &voice) -> void + { + std::fill(std::begin(voice.mProps.Send)+num_sends, std::end(voice.mProps.Send), + ALvoiceProps::SendData{}); + + std::fill(voice.mSend.begin()+num_sends, voice.mSend.end(), ALvoice::SendData{}); + auto clear_chan_sends = [num_sends](ALvoice::ChannelData &chandata) -> void + { + std::fill(chandata.mWetParams.begin()+num_sends, chandata.mWetParams.end(), + SendParams{}); + }; + std::for_each(voice.mChans.begin(), voice.mChans.end(), clear_chan_sends); + }; + std::for_each(voices->begin(), voices_end, clear_sends); + } + std::for_each(voices->begin(), voices_end, + [device](ALvoice &voice) -> void + { + delete voice.mUpdate.exchange(nullptr, std::memory_order_acq_rel); + + /* Force the voice to stopped if it was stopping. */ + ALvoice::State vstate{ALvoice::Stopping}; + voice.mPlayState.compare_exchange_strong(vstate, ALvoice::Stopped, + std::memory_order_acquire, std::memory_order_acquire); + if(voice.mSourceID.load(std::memory_order_relaxed) == 0u) + return; + + if(device->AvgSpeakerDist > 0.0f) + { + /* Reinitialize the NFC filters for new parameters. */ + const ALfloat w1{SPEEDOFSOUNDMETRESPERSEC / + (device->AvgSpeakerDist * device->Frequency)}; + auto init_nfc = [w1](ALvoice::ChannelData &chandata) -> void + { chandata.mDryParams.NFCtrlFilter.init(w1); }; + std::for_each(voice.mChans.begin(), voice.mChans.begin()+voice.mNumChannels, + init_nfc); + } + } + ); + srclock.unlock(); + + context->PropsClean.test_and_set(std::memory_order_release); + UpdateContextProps(context); + context->Listener.PropsClean.test_and_set(std::memory_order_release); + UpdateListenerProps(context); + UpdateAllSourceProps(context); + } + mixer_mode.leave(); + if(update_failed) + return ALC_INVALID_DEVICE; + + if(!device->Flags.get<DevicePaused>()) + { + if(device->Backend->start() == ALC_FALSE) + return ALC_INVALID_DEVICE; + device->Flags.set<DeviceRunning>(); + } + + return ALC_NO_ERROR; +} + + +ALCdevice::ALCdevice(DeviceType type) : Type{type}, mContexts{&EmptyContextArray} +{ +} + +/* ALCdevice::~ALCdevice + * + * Frees the device structure, and destroys any objects the app failed to + * delete. Called once there's no more references on the device. + */ +ALCdevice::~ALCdevice() +{ + TRACE("Freeing device %p\n", this); + + Backend = nullptr; + + size_t count{std::accumulate(BufferList.cbegin(), BufferList.cend(), size_t{0u}, + [](size_t cur, const BufferSubList &sublist) noexcept -> size_t + { return cur + POPCNT64(~sublist.FreeMask); } + )}; + if(count > 0) + WARN("%zu Buffer%s not deleted\n", count, (count==1)?"":"s"); + + count = std::accumulate(EffectList.cbegin(), EffectList.cend(), size_t{0u}, + [](size_t cur, const EffectSubList &sublist) noexcept -> size_t + { return cur + POPCNT64(~sublist.FreeMask); } + ); + if(count > 0) + WARN("%zu Effect%s not deleted\n", count, (count==1)?"":"s"); + + count = std::accumulate(FilterList.cbegin(), FilterList.cend(), size_t{0u}, + [](size_t cur, const FilterSubList &sublist) noexcept -> size_t + { return cur + POPCNT64(~sublist.FreeMask); } + ); + if(count > 0) + WARN("%zu Filter%s not deleted\n", count, (count==1)?"":"s"); + + if(mHrtf) + mHrtf->DecRef(); + mHrtf = nullptr; + + auto *oldarray = mContexts.exchange(nullptr, std::memory_order_relaxed); + if(oldarray != &EmptyContextArray) delete oldarray; +} + + +/* VerifyDevice + * + * Checks if the device handle is valid, and returns a new reference if so. + */ +static DeviceRef VerifyDevice(ALCdevice *device) +{ + std::lock_guard<std::recursive_mutex> _{ListLock}; + auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device); + if(iter != DeviceList.cend() && *iter == device) + { + ALCdevice_IncRef(iter->get()); + return DeviceRef{iter->get()}; + } + return DeviceRef{}; +} + + +ALCcontext::ALCcontext(ALCdevice *device) : Device{device} +{ + PropsClean.test_and_set(std::memory_order_relaxed); +} + +/* InitContext + * + * Initializes context fields + */ +static ALvoid InitContext(ALCcontext *Context) +{ + ALlistener &listener = Context->Listener; + ALeffectslotArray *auxslots; + + //Validate Context + if(!Context->DefaultSlot) + auxslots = ALeffectslot::CreatePtrArray(0); + else + { + auxslots = ALeffectslot::CreatePtrArray(1); + (*auxslots)[0] = Context->DefaultSlot.get(); + } + Context->ActiveAuxSlots.store(auxslots, std::memory_order_relaxed); + + //Set globals + Context->mDistanceModel = DistanceModel::Default; + Context->SourceDistanceModel = AL_FALSE; + Context->DopplerFactor = 1.0f; + Context->DopplerVelocity = 1.0f; + Context->SpeedOfSound = SPEEDOFSOUNDMETRESPERSEC; + Context->MetersPerUnit = AL_DEFAULT_METERS_PER_UNIT; + + Context->ExtensionList = alExtList; + + + listener.Params.Matrix = alu::Matrix::Identity(); + listener.Params.Velocity = alu::Vector{}; + listener.Params.Gain = listener.Gain; + listener.Params.MetersPerUnit = Context->MetersPerUnit; + listener.Params.DopplerFactor = Context->DopplerFactor; + listener.Params.SpeedOfSound = Context->SpeedOfSound * Context->DopplerVelocity; + listener.Params.ReverbSpeedOfSound = listener.Params.SpeedOfSound * + listener.Params.MetersPerUnit; + listener.Params.SourceDistanceModel = Context->SourceDistanceModel; + listener.Params.mDistanceModel = Context->mDistanceModel; + + + Context->AsyncEvents = CreateRingBuffer(511, sizeof(AsyncEvent), false); + StartEventThrd(Context); +} + + +/* ALCcontext::~ALCcontext() + * + * Cleans up the context, and destroys any remaining objects the app failed to + * delete. Called once there's no more references on the context. + */ +ALCcontext::~ALCcontext() +{ + TRACE("Freeing context %p\n", this); + + ALcontextProps *cprops{Update.exchange(nullptr, std::memory_order_relaxed)}; + if(cprops) + { + TRACE("Freed unapplied context update %p\n", cprops); + al_free(cprops); + } + size_t count{0}; + cprops = FreeContextProps.exchange(nullptr, std::memory_order_acquire); + while(cprops) + { + ALcontextProps *next{cprops->next.load(std::memory_order_relaxed)}; + al_free(cprops); + cprops = next; + ++count; + } + TRACE("Freed %zu context property object%s\n", count, (count==1)?"":"s"); + + count = std::accumulate(SourceList.cbegin(), SourceList.cend(), size_t{0u}, + [](size_t cur, const SourceSubList &sublist) noexcept -> size_t + { return cur + POPCNT64(~sublist.FreeMask); } + ); + if(count > 0) + WARN("%zu Source%s not deleted\n", count, (count==1)?"":"s"); + SourceList.clear(); + NumSources = 0; + + count = 0; + ALeffectslotProps *eprops{FreeEffectslotProps.exchange(nullptr, std::memory_order_acquire)}; + while(eprops) + { + ALeffectslotProps *next{eprops->next.load(std::memory_order_relaxed)}; + if(eprops->State) eprops->State->DecRef(); + al_free(eprops); + eprops = next; + ++count; + } + TRACE("Freed %zu AuxiliaryEffectSlot property object%s\n", count, (count==1)?"":"s"); + + delete ActiveAuxSlots.exchange(nullptr, std::memory_order_relaxed); + DefaultSlot = nullptr; + + count = std::accumulate(EffectSlotList.cbegin(), EffectSlotList.cend(), size_t{0u}, + [](size_t cur, const EffectSlotSubList &sublist) noexcept -> size_t + { return cur + POPCNT64(~sublist.FreeMask); } + ); + if(count > 0) + WARN("%zu AuxiliaryEffectSlot%s not deleted\n", count, (count==1)?"":"s"); + EffectSlotList.clear(); + NumEffectSlots = 0; + + count = 0; + ALvoiceProps *vprops{FreeVoiceProps.exchange(nullptr, std::memory_order_acquire)}; + while(vprops) + { + ALvoiceProps *next{vprops->next.load(std::memory_order_relaxed)}; + delete vprops; + vprops = next; + ++count; + } + TRACE("Freed %zu voice property object%s\n", count, (count==1)?"":"s"); + + Voices = nullptr; + VoiceCount.store(0, std::memory_order_relaxed); + + ALlistenerProps *lprops{Listener.Update.exchange(nullptr, std::memory_order_relaxed)}; + if(lprops) + { + TRACE("Freed unapplied listener update %p\n", lprops); + al_free(lprops); + } + count = 0; + lprops = FreeListenerProps.exchange(nullptr, std::memory_order_acquire); + while(lprops) + { + ALlistenerProps *next{lprops->next.load(std::memory_order_relaxed)}; + al_free(lprops); + lprops = next; + ++count; + } + TRACE("Freed %zu listener property object%s\n", count, (count==1)?"":"s"); + + if(AsyncEvents) + { + count = 0; + auto evt_vec = AsyncEvents->getReadVector(); + if(evt_vec.first.len > 0) + { + al::destroy_n(reinterpret_cast<AsyncEvent*>(evt_vec.first.buf), evt_vec.first.len); + count += evt_vec.first.len; + } + if(evt_vec.second.len > 0) + { + al::destroy_n(reinterpret_cast<AsyncEvent*>(evt_vec.second.buf), evt_vec.second.len); + count += evt_vec.second.len; + } + if(count > 0) + TRACE("Destructed %zu orphaned event%s\n", count, (count==1)?"":"s"); + AsyncEvents->readAdvance(count); + } + + ALCdevice_DecRef(Device); +} + +/* ReleaseContext + * + * Removes the context reference from the given device and removes it from + * being current on the running thread or globally. Returns true if other + * contexts still exist on the device. + */ +static bool ReleaseContext(ALCcontext *context, ALCdevice *device) +{ + if(LocalContext.get() == context) + { + WARN("%p released while current on thread\n", context); + LocalContext.set(nullptr); + ALCcontext_DecRef(context); + } + + ALCcontext *origctx{context}; + if(GlobalContext.compare_exchange_strong(origctx, nullptr)) + ALCcontext_DecRef(context); + + bool ret{}; + { + using ContextArray = al::FlexArray<ALCcontext*>; + + /* First make sure this context exists in the device's list. */ + auto *oldarray = device->mContexts.load(std::memory_order_acquire); + if(auto toremove = std::count(oldarray->begin(), oldarray->end(), context)) + { + auto alloc_ctx_array = [](const size_t count) -> ContextArray* + { + if(count == 0) return &EmptyContextArray; + void *ptr{al_calloc(alignof(ContextArray), ContextArray::Sizeof(count))}; + return new (ptr) ContextArray{count}; + }; + auto *newarray = alloc_ctx_array(oldarray->size() - toremove); + + /* Copy the current/old context handles to the new array, excluding + * the given context. + */ + std::copy_if(oldarray->begin(), oldarray->end(), newarray->begin(), + std::bind(std::not_equal_to<ALCcontext*>{}, _1, context)); + + /* Store the new context array in the device. Wait for any current + * mix to finish before deleting the old array. + */ + device->mContexts.store(newarray); + if(oldarray != &EmptyContextArray) + { + while((device->MixCount.load(std::memory_order_acquire)&1)) + std::this_thread::yield(); + delete oldarray; + } + + ret = !newarray->empty(); + } + else + ret = !oldarray->empty(); + } + + StopEventThrd(context); + + return ret; +} + +static void ALCcontext_IncRef(ALCcontext *context) +{ + auto ref = IncrementRef(&context->ref); + TRACEREF("ALCcontext %p increasing refcount to %u\n", context, ref); +} + +void ALCcontext_DecRef(ALCcontext *context) +{ + auto ref = DecrementRef(&context->ref); + TRACEREF("ALCcontext %p decreasing refcount to %u\n", context, ref); + if(UNLIKELY(ref == 0)) delete context; +} + +/* VerifyContext + * + * Checks if the given context is valid, returning a new reference to it if so. + */ +static ContextRef VerifyContext(ALCcontext *context) +{ + std::lock_guard<std::recursive_mutex> _{ListLock}; + auto iter = std::lower_bound(ContextList.cbegin(), ContextList.cend(), context); + if(iter != ContextList.cend() && *iter == context) + { + ALCcontext_IncRef(iter->get()); + return ContextRef{iter->get()}; + } + return ContextRef{}; +} + + +/* GetContextRef + * + * Returns a new reference to the currently active context for this thread. + */ +ContextRef GetContextRef(void) +{ + ALCcontext *context{LocalContext.get()}; + if(context) + ALCcontext_IncRef(context); + else + { + std::lock_guard<std::recursive_mutex> _{ListLock}; + context = GlobalContext.load(std::memory_order_acquire); + if(context) ALCcontext_IncRef(context); + } + return ContextRef{context}; +} + + +void AllocateVoices(ALCcontext *context, size_t num_voices) +{ + ALCdevice *device{context->Device}; + const ALsizei num_sends{device->NumAuxSends}; + + if(context->Voices && num_voices == context->Voices->size()) + return; + + std::unique_ptr<al::FlexArray<ALvoice>> voices; + { + void *ptr{al_calloc(16, al::FlexArray<ALvoice>::Sizeof(num_voices))}; + voices.reset(new (ptr) al::FlexArray<ALvoice>{num_voices}); + } + + const size_t v_count{minz(context->VoiceCount.load(std::memory_order_relaxed), num_voices)}; + if(context->Voices) + { + /* Copy the old voice data to the new storage. */ + auto viter = std::move(context->Voices->begin(), context->Voices->begin()+v_count, + voices->begin()); + + /* Clear extraneous property set sends. */ + auto clear_sends = [num_sends](ALvoice &voice) -> void + { + std::fill(std::begin(voice.mProps.Send)+num_sends, std::end(voice.mProps.Send), + ALvoiceProps::SendData{}); + + std::fill(voice.mSend.begin()+num_sends, voice.mSend.end(), ALvoice::SendData{}); + auto clear_chan_sends = [num_sends](ALvoice::ChannelData &chandata) -> void + { + std::fill(chandata.mWetParams.begin()+num_sends, chandata.mWetParams.end(), + SendParams{}); + }; + std::for_each(voice.mChans.begin(), voice.mChans.end(), clear_chan_sends); + }; + std::for_each(voices->begin(), viter, clear_sends); + } + + context->Voices = std::move(voices); + context->VoiceCount.store(static_cast<ALuint>(v_count), std::memory_order_relaxed); +} + + +/************************************************ + * Standard ALC functions + ************************************************/ + +/* alcGetError + * + * Return last ALC generated error code for the given device + */ +ALC_API ALCenum ALC_APIENTRY alcGetError(ALCdevice *device) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(dev) return dev->LastError.exchange(ALC_NO_ERROR); + return LastNullDeviceError.exchange(ALC_NO_ERROR); +} +END_API_FUNC + + +/* alcSuspendContext + * + * Suspends updates for the given context + */ +ALC_API ALCvoid ALC_APIENTRY alcSuspendContext(ALCcontext *context) +START_API_FUNC +{ + if(!SuspendDefers) + return; + + ContextRef ctx{VerifyContext(context)}; + if(!ctx) + alcSetError(nullptr, ALC_INVALID_CONTEXT); + else + ALCcontext_DeferUpdates(ctx.get()); +} +END_API_FUNC + +/* alcProcessContext + * + * Resumes processing updates for the given context + */ +ALC_API ALCvoid ALC_APIENTRY alcProcessContext(ALCcontext *context) +START_API_FUNC +{ + if(!SuspendDefers) + return; + + ContextRef ctx{VerifyContext(context)}; + if(!ctx) + alcSetError(nullptr, ALC_INVALID_CONTEXT); + else + ALCcontext_ProcessUpdates(ctx.get()); +} +END_API_FUNC + + +/* alcGetString + * + * Returns information about the device, and error strings + */ +ALC_API const ALCchar* ALC_APIENTRY alcGetString(ALCdevice *Device, ALCenum param) +START_API_FUNC +{ + const ALCchar *value = nullptr; + + switch(param) + { + case ALC_NO_ERROR: + value = alcNoError; + break; + + case ALC_INVALID_ENUM: + value = alcErrInvalidEnum; + break; + + case ALC_INVALID_VALUE: + value = alcErrInvalidValue; + break; + + case ALC_INVALID_DEVICE: + value = alcErrInvalidDevice; + break; + + case ALC_INVALID_CONTEXT: + value = alcErrInvalidContext; + break; + + case ALC_OUT_OF_MEMORY: + value = alcErrOutOfMemory; + break; + + case ALC_DEVICE_SPECIFIER: + value = alcDefaultName; + break; + + case ALC_ALL_DEVICES_SPECIFIER: + if(DeviceRef dev{VerifyDevice(Device)}) + value = dev->DeviceName.c_str(); + else + { + ProbeAllDevicesList(); + value = alcAllDevicesList.c_str(); + } + break; + + case ALC_CAPTURE_DEVICE_SPECIFIER: + if(DeviceRef dev{VerifyDevice(Device)}) + value = dev->DeviceName.c_str(); + else + { + ProbeCaptureDeviceList(); + value = alcCaptureDeviceList.c_str(); + } + break; + + /* Default devices are always first in the list */ + case ALC_DEFAULT_DEVICE_SPECIFIER: + value = alcDefaultName; + break; + + case ALC_DEFAULT_ALL_DEVICES_SPECIFIER: + if(alcAllDevicesList.empty()) + ProbeAllDevicesList(); + + /* Copy first entry as default. */ + alcDefaultAllDevicesSpecifier = alcAllDevicesList.c_str(); + value = alcDefaultAllDevicesSpecifier.c_str(); + break; + + case ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER: + if(alcCaptureDeviceList.empty()) + ProbeCaptureDeviceList(); + + /* Copy first entry as default. */ + alcCaptureDefaultDeviceSpecifier = alcCaptureDeviceList.c_str(); + value = alcCaptureDefaultDeviceSpecifier.c_str(); + break; + + case ALC_EXTENSIONS: + if(VerifyDevice(Device)) + value = alcExtensionList; + else + value = alcNoDeviceExtList; + break; + + case ALC_HRTF_SPECIFIER_SOFT: + if(DeviceRef dev{VerifyDevice(Device)}) + { + std::lock_guard<std::mutex> _{dev->StateLock}; + value = (dev->mHrtf ? dev->HrtfName.c_str() : ""); + } + else + alcSetError(nullptr, ALC_INVALID_DEVICE); + break; + + default: + alcSetError(VerifyDevice(Device).get(), ALC_INVALID_ENUM); + break; + } + + return value; +} +END_API_FUNC + + +static inline ALCsizei NumAttrsForDevice(ALCdevice *device) +{ + if(device->Type == Capture) return 9; + if(device->Type != Loopback) return 29; + if(device->FmtChans == DevFmtAmbi3D) + return 35; + return 29; +} + +static ALCsizei GetIntegerv(ALCdevice *device, ALCenum param, const al::span<ALCint> values) +{ + ALCsizei i; + + if(values.empty()) + { + alcSetError(device, ALC_INVALID_VALUE); + return 0; + } + + if(!device) + { + switch(param) + { + case ALC_MAJOR_VERSION: + values[0] = alcMajorVersion; + return 1; + case ALC_MINOR_VERSION: + values[0] = alcMinorVersion; + return 1; + + case ALC_ATTRIBUTES_SIZE: + case ALC_ALL_ATTRIBUTES: + case ALC_FREQUENCY: + case ALC_REFRESH: + case ALC_SYNC: + case ALC_MONO_SOURCES: + case ALC_STEREO_SOURCES: + case ALC_CAPTURE_SAMPLES: + case ALC_FORMAT_CHANNELS_SOFT: + case ALC_FORMAT_TYPE_SOFT: + case ALC_AMBISONIC_LAYOUT_SOFT: + case ALC_AMBISONIC_SCALING_SOFT: + case ALC_AMBISONIC_ORDER_SOFT: + case ALC_MAX_AMBISONIC_ORDER_SOFT: + alcSetError(nullptr, ALC_INVALID_DEVICE); + return 0; + + default: + alcSetError(nullptr, ALC_INVALID_ENUM); + return 0; + } + return 0; + } + + if(device->Type == Capture) + { + switch(param) + { + case ALC_ATTRIBUTES_SIZE: + values[0] = NumAttrsForDevice(device); + return 1; + + case ALC_ALL_ATTRIBUTES: + i = 0; + if(values.size() < static_cast<size_t>(NumAttrsForDevice(device))) + alcSetError(device, ALC_INVALID_VALUE); + else + { + std::lock_guard<std::mutex> _{device->StateLock}; + values[i++] = ALC_MAJOR_VERSION; + values[i++] = alcMajorVersion; + values[i++] = ALC_MINOR_VERSION; + values[i++] = alcMinorVersion; + values[i++] = ALC_CAPTURE_SAMPLES; + values[i++] = device->Backend->availableSamples(); + values[i++] = ALC_CONNECTED; + values[i++] = device->Connected.load(std::memory_order_relaxed); + values[i++] = 0; + } + return i; + + case ALC_MAJOR_VERSION: + values[0] = alcMajorVersion; + return 1; + case ALC_MINOR_VERSION: + values[0] = alcMinorVersion; + return 1; + + case ALC_CAPTURE_SAMPLES: + { std::lock_guard<std::mutex> _{device->StateLock}; + values[0] = device->Backend->availableSamples(); + } + return 1; + + case ALC_CONNECTED: + { std::lock_guard<std::mutex> _{device->StateLock}; + values[0] = device->Connected.load(std::memory_order_acquire); + } + return 1; + + default: + alcSetError(device, ALC_INVALID_ENUM); + return 0; + } + return 0; + } + + /* render device */ + switch(param) + { + case ALC_ATTRIBUTES_SIZE: + values[0] = NumAttrsForDevice(device); + return 1; + + case ALC_ALL_ATTRIBUTES: + i = 0; + if(values.size() < static_cast<size_t>(NumAttrsForDevice(device))) + alcSetError(device, ALC_INVALID_VALUE); + else + { + std::lock_guard<std::mutex> _{device->StateLock}; + values[i++] = ALC_MAJOR_VERSION; + values[i++] = alcMajorVersion; + values[i++] = ALC_MINOR_VERSION; + values[i++] = alcMinorVersion; + values[i++] = ALC_EFX_MAJOR_VERSION; + values[i++] = alcEFXMajorVersion; + values[i++] = ALC_EFX_MINOR_VERSION; + values[i++] = alcEFXMinorVersion; + + values[i++] = ALC_FREQUENCY; + values[i++] = device->Frequency; + if(device->Type != Loopback) + { + values[i++] = ALC_REFRESH; + values[i++] = device->Frequency / device->UpdateSize; + + values[i++] = ALC_SYNC; + values[i++] = ALC_FALSE; + } + else + { + if(device->FmtChans == DevFmtAmbi3D) + { + values[i++] = ALC_AMBISONIC_LAYOUT_SOFT; + values[i++] = static_cast<ALCint>(device->mAmbiLayout); + + values[i++] = ALC_AMBISONIC_SCALING_SOFT; + values[i++] = static_cast<ALCint>(device->mAmbiScale); + + values[i++] = ALC_AMBISONIC_ORDER_SOFT; + values[i++] = device->mAmbiOrder; + } + + values[i++] = ALC_FORMAT_CHANNELS_SOFT; + values[i++] = device->FmtChans; + + values[i++] = ALC_FORMAT_TYPE_SOFT; + values[i++] = device->FmtType; + } + + values[i++] = ALC_MONO_SOURCES; + values[i++] = device->NumMonoSources; + + values[i++] = ALC_STEREO_SOURCES; + values[i++] = device->NumStereoSources; + + values[i++] = ALC_MAX_AUXILIARY_SENDS; + values[i++] = device->NumAuxSends; + + values[i++] = ALC_HRTF_SOFT; + values[i++] = (device->mHrtf ? ALC_TRUE : ALC_FALSE); + + values[i++] = ALC_HRTF_STATUS_SOFT; + values[i++] = device->HrtfStatus; + + values[i++] = ALC_OUTPUT_LIMITER_SOFT; + values[i++] = device->Limiter ? ALC_TRUE : ALC_FALSE; + + values[i++] = ALC_MAX_AMBISONIC_ORDER_SOFT; + values[i++] = MAX_AMBI_ORDER; + + values[i++] = 0; + } + return i; + + case ALC_MAJOR_VERSION: + values[0] = alcMajorVersion; + return 1; + + case ALC_MINOR_VERSION: + values[0] = alcMinorVersion; + return 1; + + case ALC_EFX_MAJOR_VERSION: + values[0] = alcEFXMajorVersion; + return 1; + + case ALC_EFX_MINOR_VERSION: + values[0] = alcEFXMinorVersion; + return 1; + + case ALC_FREQUENCY: + values[0] = device->Frequency; + return 1; + + case ALC_REFRESH: + if(device->Type == Loopback) + { + alcSetError(device, ALC_INVALID_DEVICE); + return 0; + } + { std::lock_guard<std::mutex> _{device->StateLock}; + values[0] = device->Frequency / device->UpdateSize; + } + return 1; + + case ALC_SYNC: + if(device->Type == Loopback) + { + alcSetError(device, ALC_INVALID_DEVICE); + return 0; + } + values[0] = ALC_FALSE; + return 1; + + case ALC_FORMAT_CHANNELS_SOFT: + if(device->Type != Loopback) + { + alcSetError(device, ALC_INVALID_DEVICE); + return 0; + } + values[0] = device->FmtChans; + return 1; + + case ALC_FORMAT_TYPE_SOFT: + if(device->Type != Loopback) + { + alcSetError(device, ALC_INVALID_DEVICE); + return 0; + } + values[0] = device->FmtType; + return 1; + + case ALC_AMBISONIC_LAYOUT_SOFT: + if(device->Type != Loopback || device->FmtChans != DevFmtAmbi3D) + { + alcSetError(device, ALC_INVALID_DEVICE); + return 0; + } + values[0] = static_cast<ALCint>(device->mAmbiLayout); + return 1; + + case ALC_AMBISONIC_SCALING_SOFT: + if(device->Type != Loopback || device->FmtChans != DevFmtAmbi3D) + { + alcSetError(device, ALC_INVALID_DEVICE); + return 0; + } + values[0] = static_cast<ALCint>(device->mAmbiScale); + return 1; + + case ALC_AMBISONIC_ORDER_SOFT: + if(device->Type != Loopback || device->FmtChans != DevFmtAmbi3D) + { + alcSetError(device, ALC_INVALID_DEVICE); + return 0; + } + values[0] = device->mAmbiOrder; + return 1; + + case ALC_MONO_SOURCES: + values[0] = device->NumMonoSources; + return 1; + + case ALC_STEREO_SOURCES: + values[0] = device->NumStereoSources; + return 1; + + case ALC_MAX_AUXILIARY_SENDS: + values[0] = device->NumAuxSends; + return 1; + + case ALC_CONNECTED: + { std::lock_guard<std::mutex> _{device->StateLock}; + values[0] = device->Connected.load(std::memory_order_acquire); + } + return 1; + + case ALC_HRTF_SOFT: + values[0] = (device->mHrtf ? ALC_TRUE : ALC_FALSE); + return 1; + + case ALC_HRTF_STATUS_SOFT: + values[0] = device->HrtfStatus; + return 1; + + case ALC_NUM_HRTF_SPECIFIERS_SOFT: + { std::lock_guard<std::mutex> _{device->StateLock}; + device->HrtfList.clear(); + device->HrtfList = EnumerateHrtf(device->DeviceName.c_str()); + values[0] = static_cast<ALCint>(minz(device->HrtfList.size(), + std::numeric_limits<ALCint>::max())); + } + return 1; + + case ALC_OUTPUT_LIMITER_SOFT: + values[0] = device->Limiter ? ALC_TRUE : ALC_FALSE; + return 1; + + case ALC_MAX_AMBISONIC_ORDER_SOFT: + values[0] = MAX_AMBI_ORDER; + return 1; + + default: + alcSetError(device, ALC_INVALID_ENUM); + return 0; + } + return 0; +} + +/* alcGetIntegerv + * + * Returns information about the device and the version of OpenAL + */ +ALC_API void ALC_APIENTRY alcGetIntegerv(ALCdevice *device, ALCenum param, ALCsizei size, ALCint *values) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(size <= 0 || values == nullptr) + alcSetError(dev.get(), ALC_INVALID_VALUE); + else + GetIntegerv(dev.get(), param, {values, values+size}); +} +END_API_FUNC + +ALC_API void ALC_APIENTRY alcGetInteger64vSOFT(ALCdevice *device, ALCenum pname, ALCsizei size, ALCint64SOFT *values) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(size <= 0 || values == nullptr) + alcSetError(dev.get(), ALC_INVALID_VALUE); + else if(!dev || dev->Type == Capture) + { + auto ivals = al::vector<ALCint>(size); + size = GetIntegerv(dev.get(), pname, {ivals.data(), ivals.size()}); + std::copy(ivals.begin(), ivals.begin()+size, values); + } + else /* render device */ + { + switch(pname) + { + case ALC_ATTRIBUTES_SIZE: + *values = NumAttrsForDevice(dev.get())+4; + break; + + case ALC_ALL_ATTRIBUTES: + if(size < NumAttrsForDevice(dev.get())+4) + alcSetError(dev.get(), ALC_INVALID_VALUE); + else + { + ALsizei i{0}; + std::lock_guard<std::mutex> _{dev->StateLock}; + values[i++] = ALC_FREQUENCY; + values[i++] = dev->Frequency; + + if(dev->Type != Loopback) + { + values[i++] = ALC_REFRESH; + values[i++] = dev->Frequency / dev->UpdateSize; + + values[i++] = ALC_SYNC; + values[i++] = ALC_FALSE; + } + else + { + if(dev->FmtChans == DevFmtAmbi3D) + { + values[i++] = ALC_AMBISONIC_LAYOUT_SOFT; + values[i++] = static_cast<ALCint64SOFT>(dev->mAmbiLayout); + + values[i++] = ALC_AMBISONIC_SCALING_SOFT; + values[i++] = static_cast<ALCint64SOFT>(dev->mAmbiScale); + + values[i++] = ALC_AMBISONIC_ORDER_SOFT; + values[i++] = dev->mAmbiOrder; + } + + values[i++] = ALC_FORMAT_CHANNELS_SOFT; + values[i++] = dev->FmtChans; + + values[i++] = ALC_FORMAT_TYPE_SOFT; + values[i++] = dev->FmtType; + } + + values[i++] = ALC_MONO_SOURCES; + values[i++] = dev->NumMonoSources; + + values[i++] = ALC_STEREO_SOURCES; + values[i++] = dev->NumStereoSources; + + values[i++] = ALC_MAX_AUXILIARY_SENDS; + values[i++] = dev->NumAuxSends; + + values[i++] = ALC_HRTF_SOFT; + values[i++] = (dev->mHrtf ? ALC_TRUE : ALC_FALSE); + + values[i++] = ALC_HRTF_STATUS_SOFT; + values[i++] = dev->HrtfStatus; + + values[i++] = ALC_OUTPUT_LIMITER_SOFT; + values[i++] = dev->Limiter ? ALC_TRUE : ALC_FALSE; + + ClockLatency clock{GetClockLatency(dev.get())}; + values[i++] = ALC_DEVICE_CLOCK_SOFT; + values[i++] = clock.ClockTime.count(); + + values[i++] = ALC_DEVICE_LATENCY_SOFT; + values[i++] = clock.Latency.count(); + + values[i++] = 0; + } + break; + + case ALC_DEVICE_CLOCK_SOFT: + { std::lock_guard<std::mutex> _{dev->StateLock}; + nanoseconds basecount; + ALuint samplecount; + ALuint refcount; + do { + while(((refcount=ReadRef(&dev->MixCount))&1) != 0) + std::this_thread::yield(); + basecount = dev->ClockBase; + samplecount = dev->SamplesDone; + } while(refcount != ReadRef(&dev->MixCount)); + basecount += nanoseconds{seconds{samplecount}} / dev->Frequency; + *values = basecount.count(); + } + break; + + case ALC_DEVICE_LATENCY_SOFT: + { std::lock_guard<std::mutex> _{dev->StateLock}; + ClockLatency clock{GetClockLatency(dev.get())}; + *values = clock.Latency.count(); + } + break; + + case ALC_DEVICE_CLOCK_LATENCY_SOFT: + if(size < 2) + alcSetError(dev.get(), ALC_INVALID_VALUE); + else + { + std::lock_guard<std::mutex> _{dev->StateLock}; + ClockLatency clock{GetClockLatency(dev.get())}; + values[0] = clock.ClockTime.count(); + values[1] = clock.Latency.count(); + } + break; + + default: + auto ivals = al::vector<ALCint>(size); + size = GetIntegerv(dev.get(), pname, {ivals.data(), ivals.size()}); + std::copy(ivals.begin(), ivals.begin()+size, values); + break; + } + } +} +END_API_FUNC + + +/* alcIsExtensionPresent + * + * Determines if there is support for a particular extension + */ +ALC_API ALCboolean ALC_APIENTRY alcIsExtensionPresent(ALCdevice *device, const ALCchar *extName) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!extName) + alcSetError(dev.get(), ALC_INVALID_VALUE); + else + { + size_t len = strlen(extName); + const char *ptr = (dev ? alcExtensionList : alcNoDeviceExtList); + while(ptr && *ptr) + { + if(strncasecmp(ptr, extName, len) == 0 && + (ptr[len] == '\0' || isspace(ptr[len]))) + return ALC_TRUE; + + if((ptr=strchr(ptr, ' ')) != nullptr) + { + do { + ++ptr; + } while(isspace(*ptr)); + } + } + } + return ALC_FALSE; +} +END_API_FUNC + + +/* alcGetProcAddress + * + * Retrieves the function address for a particular extension function + */ +ALC_API ALCvoid* ALC_APIENTRY alcGetProcAddress(ALCdevice *device, const ALCchar *funcName) +START_API_FUNC +{ + if(!funcName) + { + DeviceRef dev{VerifyDevice(device)}; + alcSetError(dev.get(), ALC_INVALID_VALUE); + } + else + { + for(const auto &func : alcFunctions) + { + if(strcmp(func.funcName, funcName) == 0) + return func.address; + } + } + return nullptr; +} +END_API_FUNC + + +/* alcGetEnumValue + * + * Get the value for a particular ALC enumeration name + */ +ALC_API ALCenum ALC_APIENTRY alcGetEnumValue(ALCdevice *device, const ALCchar *enumName) +START_API_FUNC +{ + if(!enumName) + { + DeviceRef dev{VerifyDevice(device)}; + alcSetError(dev.get(), ALC_INVALID_VALUE); + } + else + { + for(const auto &enm : alcEnumerations) + { + if(strcmp(enm.enumName, enumName) == 0) + return enm.value; + } + } + return 0; +} +END_API_FUNC + + +/* alcCreateContext + * + * Create and attach a context to the given device. + */ +ALC_API ALCcontext* ALC_APIENTRY alcCreateContext(ALCdevice *device, const ALCint *attrList) +START_API_FUNC +{ + /* Explicitly hold the list lock while taking the StateLock in case the + * device is asynchronously destroyed, to ensure this new context is + * properly cleaned up after being made. + */ + std::unique_lock<std::recursive_mutex> listlock{ListLock}; + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type == Capture || !dev->Connected.load(std::memory_order_relaxed)) + { + listlock.unlock(); + alcSetError(dev.get(), ALC_INVALID_DEVICE); + return nullptr; + } + std::unique_lock<std::mutex> statelock{dev->StateLock}; + listlock.unlock(); + + dev->LastError.store(ALC_NO_ERROR); + + ContextRef context{new ALCcontext{dev.get()}}; + ALCdevice_IncRef(context->Device); + + ALCenum err{UpdateDeviceParams(dev.get(), attrList)}; + if(err != ALC_NO_ERROR) + { + alcSetError(dev.get(), err); + if(err == ALC_INVALID_DEVICE) + aluHandleDisconnect(dev.get(), "Device update failure"); + statelock.unlock(); + + return nullptr; + } + AllocateVoices(context.get(), 256); + + if(DefaultEffect.type != AL_EFFECT_NULL && dev->Type == Playback) + { + void *ptr{al_calloc(16, sizeof(ALeffectslot))}; + context->DefaultSlot = std::unique_ptr<ALeffectslot>{new (ptr) ALeffectslot{}}; + if(InitEffectSlot(context->DefaultSlot.get()) == AL_NO_ERROR) + aluInitEffectPanning(context->DefaultSlot.get(), dev.get()); + else + { + context->DefaultSlot = nullptr; + ERR("Failed to initialize the default effect slot\n"); + } + } + + InitContext(context.get()); + + if(auto volopt = ConfigValueFloat(dev->DeviceName.c_str(), nullptr, "volume-adjust")) + { + const ALfloat valf{*volopt}; + if(!std::isfinite(valf)) + ERR("volume-adjust must be finite: %f\n", valf); + else + { + const ALfloat db{clampf(valf, -24.0f, 24.0f)}; + if(db != valf) + WARN("volume-adjust clamped: %f, range: +/-%f\n", valf, 24.0f); + context->GainBoost = std::pow(10.0f, db/20.0f); + TRACE("volume-adjust gain: %f\n", context->GainBoost); + } + } + UpdateListenerProps(context.get()); + + { + using ContextArray = al::FlexArray<ALCcontext*>; + + /* Allocate a new context array, which holds 1 more than the current/ + * old array. + */ + auto *oldarray = device->mContexts.load(); + const size_t newcount{oldarray->size()+1}; + void *ptr{al_calloc(alignof(ContextArray), ContextArray::Sizeof(newcount))}; + auto *newarray = new (ptr) ContextArray{newcount}; + + /* Copy the current/old context handles to the new array, appending the + * new context. + */ + auto iter = std::copy(oldarray->begin(), oldarray->end(), newarray->begin()); + *iter = context.get(); + + /* Store the new context array in the device. Wait for any current mix + * to finish before deleting the old array. + */ + dev->mContexts.store(newarray); + if(oldarray != &EmptyContextArray) + { + while((dev->MixCount.load(std::memory_order_acquire)&1)) + std::this_thread::yield(); + delete oldarray; + } + } + statelock.unlock(); + + { + std::lock_guard<std::recursive_mutex> _{ListLock}; + auto iter = std::lower_bound(ContextList.cbegin(), ContextList.cend(), context.get()); + ALCcontext_IncRef(context.get()); + ContextList.insert(iter, ContextRef{context.get()}); + } + + if(context->DefaultSlot) + { + if(InitializeEffect(context.get(), context->DefaultSlot.get(), &DefaultEffect) == AL_NO_ERROR) + UpdateEffectSlotProps(context->DefaultSlot.get(), context.get()); + else + ERR("Failed to initialize the default effect\n"); + } + + TRACE("Created context %p\n", context.get()); + return context.get(); +} +END_API_FUNC + +/* alcDestroyContext + * + * Remove a context from its device + */ +ALC_API ALCvoid ALC_APIENTRY alcDestroyContext(ALCcontext *context) +START_API_FUNC +{ + std::unique_lock<std::recursive_mutex> listlock{ListLock}; + auto iter = std::lower_bound(ContextList.begin(), ContextList.end(), context); + if(iter == ContextList.end() || *iter != context) + { + listlock.unlock(); + alcSetError(nullptr, ALC_INVALID_CONTEXT); + return; + } + /* Hold an extra reference to this context so it remains valid until the + * ListLock is released. + */ + ContextRef ctx{std::move(*iter)}; + ContextList.erase(iter); + + ALCdevice *Device{ctx->Device}; + + std::lock_guard<std::mutex> _{Device->StateLock}; + if(!ReleaseContext(ctx.get(), Device) && Device->Flags.get<DeviceRunning>()) + { + Device->Backend->stop(); + Device->Flags.unset<DeviceRunning>(); + } +} +END_API_FUNC + + +/* alcGetCurrentContext + * + * Returns the currently active context on the calling thread + */ +ALC_API ALCcontext* ALC_APIENTRY alcGetCurrentContext(void) +START_API_FUNC +{ + ALCcontext *Context{LocalContext.get()}; + if(!Context) Context = GlobalContext.load(); + return Context; +} +END_API_FUNC + +/* alcGetThreadContext + * + * Returns the currently active thread-local context + */ +ALC_API ALCcontext* ALC_APIENTRY alcGetThreadContext(void) +START_API_FUNC +{ return LocalContext.get(); } +END_API_FUNC + +/* alcMakeContextCurrent + * + * Makes the given context the active process-wide context, and removes the + * thread-local context for the calling thread. + */ +ALC_API ALCboolean ALC_APIENTRY alcMakeContextCurrent(ALCcontext *context) +START_API_FUNC +{ + /* context must be valid or nullptr */ + ContextRef ctx; + if(context) + { + ctx = VerifyContext(context); + if(!ctx) + { + alcSetError(nullptr, ALC_INVALID_CONTEXT); + return ALC_FALSE; + } + } + /* Release this reference (if any) to store it in the GlobalContext + * pointer. Take ownership of the reference (if any) that was previously + * stored there. + */ + ctx = ContextRef{GlobalContext.exchange(ctx.release())}; + + /* Reset (decrement) the previous global reference by replacing it with the + * thread-local context. Take ownership of the thread-local context + * reference (if any), clearing the storage to null. + */ + ctx = ContextRef{LocalContext.get()}; + if(ctx) LocalContext.set(nullptr); + /* Reset (decrement) the previous thread-local reference. */ + + return ALC_TRUE; +} +END_API_FUNC + +/* alcSetThreadContext + * + * Makes the given context the active context for the current thread + */ +ALC_API ALCboolean ALC_APIENTRY alcSetThreadContext(ALCcontext *context) +START_API_FUNC +{ + /* context must be valid or nullptr */ + ContextRef ctx; + if(context) + { + ctx = VerifyContext(context); + if(!ctx) + { + alcSetError(nullptr, ALC_INVALID_CONTEXT); + return ALC_FALSE; + } + } + /* context's reference count is already incremented */ + ContextRef old{LocalContext.get()}; + LocalContext.set(ctx.release()); + + return ALC_TRUE; +} +END_API_FUNC + + +/* alcGetContextsDevice + * + * Returns the device that a particular context is attached to + */ +ALC_API ALCdevice* ALC_APIENTRY alcGetContextsDevice(ALCcontext *Context) +START_API_FUNC +{ + ContextRef ctx{VerifyContext(Context)}; + if(!ctx) + { + alcSetError(nullptr, ALC_INVALID_CONTEXT); + return nullptr; + } + return ctx->Device; +} +END_API_FUNC + + +/* alcOpenDevice + * + * Opens the named device. + */ +ALC_API ALCdevice* ALC_APIENTRY alcOpenDevice(const ALCchar *deviceName) +START_API_FUNC +{ + DO_INITCONFIG(); + + if(!PlaybackFactory) + { + alcSetError(nullptr, ALC_INVALID_VALUE); + return nullptr; + } + + if(deviceName && (!deviceName[0] || strcasecmp(deviceName, alcDefaultName) == 0 || strcasecmp(deviceName, "openal-soft") == 0 +#ifdef _WIN32 + /* Some old Windows apps hardcode these expecting OpenAL to use a + * specific audio API, even when they're not enumerated. Creative's + * router effectively ignores them too. + */ + || strcasecmp(deviceName, "DirectSound3D") == 0 || strcasecmp(deviceName, "DirectSound") == 0 + || strcasecmp(deviceName, "MMSYSTEM") == 0 +#endif + )) + deviceName = nullptr; + + DeviceRef device{new ALCdevice{Playback}}; + + /* Set output format */ + device->FmtChans = DevFmtChannelsDefault; + device->FmtType = DevFmtTypeDefault; + device->Frequency = DEFAULT_OUTPUT_RATE; + device->UpdateSize = DEFAULT_UPDATE_SIZE; + device->BufferSize = DEFAULT_UPDATE_SIZE * DEFAULT_NUM_UPDATES; + + device->SourcesMax = 256; + device->AuxiliaryEffectSlotMax = 64; + device->NumAuxSends = DEFAULT_SENDS; + + try { + /* Create the device backend. */ + device->Backend = PlaybackFactory->createBackend(device.get(), BackendType::Playback); + + /* Find a playback device to open */ + ALCenum err{device->Backend->open(deviceName)}; + if(err != ALC_NO_ERROR) + { + alcSetError(nullptr, err); + return nullptr; + } + } + catch(al::backend_exception &e) { + WARN("Failed to open playback device: %s\n", e.what()); + alcSetError(nullptr, e.errorCode()); + return nullptr; + } + + deviceName = device->DeviceName.c_str(); + if(auto chanopt = ConfigValueStr(deviceName, nullptr, "channels")) + { + static constexpr struct ChannelMap { + const char name[16]; + DevFmtChannels chans; + ALsizei order; + } chanlist[] = { + { "mono", DevFmtMono, 0 }, + { "stereo", DevFmtStereo, 0 }, + { "quad", DevFmtQuad, 0 }, + { "surround51", DevFmtX51, 0 }, + { "surround61", DevFmtX61, 0 }, + { "surround71", DevFmtX71, 0 }, + { "surround51rear", DevFmtX51Rear, 0 }, + { "ambi1", DevFmtAmbi3D, 1 }, + { "ambi2", DevFmtAmbi3D, 2 }, + { "ambi3", DevFmtAmbi3D, 3 }, + }; + + const ALCchar *fmt{chanopt->c_str()}; + auto iter = std::find_if(std::begin(chanlist), std::end(chanlist), + [fmt](const ChannelMap &entry) -> bool + { return strcasecmp(entry.name, fmt) == 0; } + ); + if(iter == std::end(chanlist)) + ERR("Unsupported channels: %s\n", fmt); + else + { + device->FmtChans = iter->chans; + device->mAmbiOrder = iter->order; + device->Flags.set<ChannelsRequest>(); + } + } + if(auto typeopt = ConfigValueStr(deviceName, nullptr, "sample-type")) + { + static constexpr struct TypeMap { + const char name[16]; + DevFmtType type; + } typelist[] = { + { "int8", DevFmtByte }, + { "uint8", DevFmtUByte }, + { "int16", DevFmtShort }, + { "uint16", DevFmtUShort }, + { "int32", DevFmtInt }, + { "uint32", DevFmtUInt }, + { "float32", DevFmtFloat }, + }; + + const ALCchar *fmt{typeopt->c_str()}; + auto iter = std::find_if(std::begin(typelist), std::end(typelist), + [fmt](const TypeMap &entry) -> bool + { return strcasecmp(entry.name, fmt) == 0; } + ); + if(iter == std::end(typelist)) + ERR("Unsupported sample-type: %s\n", fmt); + else + { + device->FmtType = iter->type; + device->Flags.set<SampleTypeRequest>(); + } + } + + if(ALuint freq{ConfigValueUInt(deviceName, nullptr, "frequency").value_or(0)}) + { + if(freq < MIN_OUTPUT_RATE) + { + ERR("%uhz request clamped to %uhz minimum\n", freq, MIN_OUTPUT_RATE); + freq = MIN_OUTPUT_RATE; + } + device->UpdateSize = (device->UpdateSize*freq + device->Frequency/2) / device->Frequency; + device->BufferSize = (device->BufferSize*freq + device->Frequency/2) / device->Frequency; + device->Frequency = freq; + device->Flags.set<FrequencyRequest>(); + } + + if(auto persizeopt = ConfigValueUInt(deviceName, nullptr, "period_size")) + device->UpdateSize = clampu(*persizeopt, 64, 8192); + + if(auto peropt = ConfigValueUInt(deviceName, nullptr, "periods")) + device->BufferSize = device->UpdateSize * clampu(*peropt, 2, 16); + else + device->BufferSize = maxu(device->BufferSize, device->UpdateSize*2); + + if(auto srcsopt = ConfigValueUInt(deviceName, nullptr, "sources")) + { + if(*srcsopt > 0) device->SourcesMax = *srcsopt; + } + + if(auto slotsopt = ConfigValueUInt(deviceName, nullptr, "slots")) + { + if(*slotsopt > 0) + device->AuxiliaryEffectSlotMax = minu(*slotsopt, INT_MAX); + } + + if(auto sendsopt = ConfigValueInt(deviceName, nullptr, "sends")) + device->NumAuxSends = clampi(DEFAULT_SENDS, 0, clampi(*sendsopt, 0, MAX_SENDS)); + + device->NumStereoSources = 1; + device->NumMonoSources = device->SourcesMax - device->NumStereoSources; + + if(auto ambiopt = ConfigValueStr(deviceName, nullptr, "ambi-format")) + { + const ALCchar *fmt{ambiopt->c_str()}; + if(strcasecmp(fmt, "fuma") == 0) + { + if(device->mAmbiOrder > 3) + ERR("FuMa is incompatible with %d%s order ambisonics (up to third-order only)\n", + device->mAmbiOrder, + (((device->mAmbiOrder%100)/10) == 1) ? "th" : + ((device->mAmbiOrder%10) == 1) ? "st" : + ((device->mAmbiOrder%10) == 2) ? "nd" : + ((device->mAmbiOrder%10) == 3) ? "rd" : "th"); + else + { + device->mAmbiLayout = AmbiLayout::FuMa; + device->mAmbiScale = AmbiNorm::FuMa; + } + } + else if(strcasecmp(fmt, "ambix") == 0 || strcasecmp(fmt, "acn+sn3d") == 0) + { + device->mAmbiLayout = AmbiLayout::ACN; + device->mAmbiScale = AmbiNorm::SN3D; + } + else if(strcasecmp(fmt, "acn+n3d") == 0) + { + device->mAmbiLayout = AmbiLayout::ACN; + device->mAmbiScale = AmbiNorm::N3D; + } + else + ERR("Unsupported ambi-format: %s\n", fmt); + } + + { + std::lock_guard<std::recursive_mutex> _{ListLock}; + auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device.get()); + ALCdevice_IncRef(device.get()); + DeviceList.insert(iter, DeviceRef{device.get()}); + } + + TRACE("Created device %p, \"%s\"\n", device.get(), device->DeviceName.c_str()); + return device.get(); +} +END_API_FUNC + +/* alcCloseDevice + * + * Closes the given device. + */ +ALC_API ALCboolean ALC_APIENTRY alcCloseDevice(ALCdevice *device) +START_API_FUNC +{ + std::unique_lock<std::recursive_mutex> listlock{ListLock}; + auto iter = std::lower_bound(DeviceList.begin(), DeviceList.end(), device); + if(iter == DeviceList.end() || *iter != device) + { + alcSetError(nullptr, ALC_INVALID_DEVICE); + return ALC_FALSE; + } + if((*iter)->Type == Capture) + { + alcSetError(iter->get(), ALC_INVALID_DEVICE); + return ALC_FALSE; + } + + /* Erase the device, and any remaining contexts left on it, from their + * respective lists. + */ + DeviceRef dev{std::move(*iter)}; + DeviceList.erase(iter); + + std::unique_lock<std::mutex> statelock{dev->StateLock}; + al::vector<ContextRef> orphanctxs; + for(ALCcontext *ctx : *dev->mContexts.load()) + { + auto iter = std::lower_bound(ContextList.begin(), ContextList.end(), ctx); + if(iter != ContextList.end() && *iter == ctx) + { + orphanctxs.emplace_back(std::move(*iter)); + ContextList.erase(iter); + } + } + listlock.unlock(); + + for(ContextRef &context : orphanctxs) + { + WARN("Releasing context %p\n", context.get()); + ReleaseContext(context.get(), dev.get()); + } + orphanctxs.clear(); + + if(dev->Flags.get<DeviceRunning>()) + dev->Backend->stop(); + dev->Flags.unset<DeviceRunning>(); + + return ALC_TRUE; +} +END_API_FUNC + + +/************************************************ + * ALC capture functions + ************************************************/ +ALC_API ALCdevice* ALC_APIENTRY alcCaptureOpenDevice(const ALCchar *deviceName, ALCuint frequency, ALCenum format, ALCsizei samples) +START_API_FUNC +{ + DO_INITCONFIG(); + + if(!CaptureFactory) + { + alcSetError(nullptr, ALC_INVALID_VALUE); + return nullptr; + } + + if(samples <= 0) + { + alcSetError(nullptr, ALC_INVALID_VALUE); + return nullptr; + } + + if(deviceName && (!deviceName[0] || strcasecmp(deviceName, alcDefaultName) == 0 || strcasecmp(deviceName, "openal-soft") == 0)) + deviceName = nullptr; + + DeviceRef device{new ALCdevice{Capture}}; + + auto decompfmt = DecomposeDevFormat(format); + if(!decompfmt) + { + alcSetError(nullptr, ALC_INVALID_ENUM); + return nullptr; + } + + device->Frequency = frequency; + device->FmtChans = decompfmt->chans; + device->FmtType = decompfmt->type; + device->Flags.set<FrequencyRequest, ChannelsRequest, SampleTypeRequest>(); + + device->UpdateSize = samples; + device->BufferSize = samples; + + try { + device->Backend = CaptureFactory->createBackend(device.get(), BackendType::Capture); + + TRACE("Capture format: %s, %s, %uhz, %u / %u buffer\n", + DevFmtChannelsString(device->FmtChans), DevFmtTypeString(device->FmtType), + device->Frequency, device->UpdateSize, device->BufferSize); + ALCenum err{device->Backend->open(deviceName)}; + if(err != ALC_NO_ERROR) + { + alcSetError(nullptr, err); + return nullptr; + } + } + catch(al::backend_exception &e) { + WARN("Failed to open capture device: %s\n", e.what()); + alcSetError(nullptr, e.errorCode()); + return nullptr; + } + + { + std::lock_guard<std::recursive_mutex> _{ListLock}; + auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device.get()); + ALCdevice_IncRef(device.get()); + DeviceList.insert(iter, DeviceRef{device.get()}); + } + + TRACE("Created device %p, \"%s\"\n", device.get(), device->DeviceName.c_str()); + return device.get(); +} +END_API_FUNC + +ALC_API ALCboolean ALC_APIENTRY alcCaptureCloseDevice(ALCdevice *device) +START_API_FUNC +{ + std::unique_lock<std::recursive_mutex> listlock{ListLock}; + auto iter = std::lower_bound(DeviceList.begin(), DeviceList.end(), device); + if(iter == DeviceList.end() || *iter != device) + { + alcSetError(nullptr, ALC_INVALID_DEVICE); + return ALC_FALSE; + } + if((*iter)->Type != Capture) + { + alcSetError(iter->get(), ALC_INVALID_DEVICE); + return ALC_FALSE; + } + + DeviceRef dev{std::move(*iter)}; + DeviceList.erase(iter); + listlock.unlock(); + + std::lock_guard<std::mutex> _{dev->StateLock}; + if(dev->Flags.get<DeviceRunning>()) + dev->Backend->stop(); + dev->Flags.unset<DeviceRunning>(); + + return ALC_TRUE; +} +END_API_FUNC + +ALC_API void ALC_APIENTRY alcCaptureStart(ALCdevice *device) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type != Capture) + { + alcSetError(dev.get(), ALC_INVALID_DEVICE); + return; + } + + std::lock_guard<std::mutex> _{dev->StateLock}; + if(!dev->Connected.load(std::memory_order_acquire)) + alcSetError(dev.get(), ALC_INVALID_DEVICE); + else if(!dev->Flags.get<DeviceRunning>()) + { + if(dev->Backend->start()) + dev->Flags.set<DeviceRunning>(); + else + { + aluHandleDisconnect(dev.get(), "Device start failure"); + alcSetError(dev.get(), ALC_INVALID_DEVICE); + } + } +} +END_API_FUNC + +ALC_API void ALC_APIENTRY alcCaptureStop(ALCdevice *device) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type != Capture) + alcSetError(dev.get(), ALC_INVALID_DEVICE); + else + { + std::lock_guard<std::mutex> _{dev->StateLock}; + if(dev->Flags.get<DeviceRunning>()) + dev->Backend->stop(); + dev->Flags.unset<DeviceRunning>(); + } +} +END_API_FUNC + +ALC_API void ALC_APIENTRY alcCaptureSamples(ALCdevice *device, ALCvoid *buffer, ALCsizei samples) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type != Capture) + { + alcSetError(dev.get(), ALC_INVALID_DEVICE); + return; + } + + ALCenum err{ALC_INVALID_VALUE}; + { std::lock_guard<std::mutex> _{dev->StateLock}; + BackendBase *backend{dev->Backend.get()}; + if(samples >= 0 && backend->availableSamples() >= static_cast<ALCuint>(samples)) + err = backend->captureSamples(buffer, samples); + } + if(err != ALC_NO_ERROR) + alcSetError(dev.get(), err); +} +END_API_FUNC + + +/************************************************ + * ALC loopback functions + ************************************************/ + +/* alcLoopbackOpenDeviceSOFT + * + * Open a loopback device, for manual rendering. + */ +ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar *deviceName) +START_API_FUNC +{ + DO_INITCONFIG(); + + /* Make sure the device name, if specified, is us. */ + if(deviceName && strcmp(deviceName, alcDefaultName) != 0) + { + alcSetError(nullptr, ALC_INVALID_VALUE); + return nullptr; + } + + DeviceRef device{new ALCdevice{Loopback}}; + + device->SourcesMax = 256; + device->AuxiliaryEffectSlotMax = 64; + device->NumAuxSends = DEFAULT_SENDS; + + //Set output format + device->BufferSize = 0; + device->UpdateSize = 0; + + device->Frequency = DEFAULT_OUTPUT_RATE; + device->FmtChans = DevFmtChannelsDefault; + device->FmtType = DevFmtTypeDefault; + + if(auto srcsopt = ConfigValueUInt(nullptr, nullptr, "sources")) + { + if(*srcsopt > 0) device->SourcesMax = *srcsopt; + } + + if(auto slotsopt = ConfigValueUInt(nullptr, nullptr, "slots")) + { + if(*slotsopt > 0) + device->AuxiliaryEffectSlotMax = minu(*slotsopt, INT_MAX); + } + + if(auto sendsopt = ConfigValueInt(nullptr, nullptr, "sends")) + device->NumAuxSends = clampi(DEFAULT_SENDS, 0, clampi(*sendsopt, 0, MAX_SENDS)); + + device->NumStereoSources = 1; + device->NumMonoSources = device->SourcesMax - device->NumStereoSources; + + try { + device->Backend = LoopbackBackendFactory::getFactory().createBackend(device.get(), + BackendType::Playback); + + // Open the "backend" + device->Backend->open("Loopback"); + } + catch(al::backend_exception &e) { + WARN("Failed to open loopback device: %s\n", e.what()); + alcSetError(nullptr, e.errorCode()); + return nullptr; + } + + { + std::lock_guard<std::recursive_mutex> _{ListLock}; + auto iter = std::lower_bound(DeviceList.cbegin(), DeviceList.cend(), device.get()); + ALCdevice_IncRef(device.get()); + DeviceList.insert(iter, DeviceRef{device.get()}); + } + + TRACE("Created device %p\n", device.get()); + return device.get(); +} +END_API_FUNC + +/* alcIsRenderFormatSupportedSOFT + * + * Determines if the loopback device supports the given format for rendering. + */ +ALC_API ALCboolean ALC_APIENTRY alcIsRenderFormatSupportedSOFT(ALCdevice *device, ALCsizei freq, ALCenum channels, ALCenum type) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type != Loopback) + alcSetError(dev.get(), ALC_INVALID_DEVICE); + else if(freq <= 0) + alcSetError(dev.get(), ALC_INVALID_VALUE); + else + { + if(IsValidALCType(type) && IsValidALCChannels(channels) && freq >= MIN_OUTPUT_RATE) + return ALC_TRUE; + } + + return ALC_FALSE; +} +END_API_FUNC + +/* alcRenderSamplesSOFT + * + * Renders some samples into a buffer, using the format last set by the + * attributes given to alcCreateContext. + */ +FORCE_ALIGN ALC_API void ALC_APIENTRY alcRenderSamplesSOFT(ALCdevice *device, ALCvoid *buffer, ALCsizei samples) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type != Loopback) + alcSetError(dev.get(), ALC_INVALID_DEVICE); + else if(samples < 0 || (samples > 0 && buffer == nullptr)) + alcSetError(dev.get(), ALC_INVALID_VALUE); + else + { + BackendLockGuard _{*device->Backend}; + aluMixData(dev.get(), buffer, samples); + } +} +END_API_FUNC + + +/************************************************ + * ALC DSP pause/resume functions + ************************************************/ + +/* alcDevicePauseSOFT + * + * Pause the DSP to stop audio processing. + */ +ALC_API void ALC_APIENTRY alcDevicePauseSOFT(ALCdevice *device) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type != Playback) + alcSetError(dev.get(), ALC_INVALID_DEVICE); + else + { + std::lock_guard<std::mutex> _{dev->StateLock}; + if(dev->Flags.get<DeviceRunning>()) + dev->Backend->stop(); + dev->Flags.unset<DeviceRunning>(); + dev->Flags.set<DevicePaused>(); + } +} +END_API_FUNC + +/* alcDeviceResumeSOFT + * + * Resume the DSP to restart audio processing. + */ +ALC_API void ALC_APIENTRY alcDeviceResumeSOFT(ALCdevice *device) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type != Playback) + { + alcSetError(dev.get(), ALC_INVALID_DEVICE); + return; + } + + std::lock_guard<std::mutex> _{dev->StateLock}; + if(!dev->Flags.get<DevicePaused>()) + return; + dev->Flags.unset<DevicePaused>(); + if(dev->mContexts.load()->empty()) + return; + + if(dev->Backend->start() == ALC_FALSE) + { + aluHandleDisconnect(dev.get(), "Device start failure"); + alcSetError(dev.get(), ALC_INVALID_DEVICE); + return; + } + dev->Flags.set<DeviceRunning>(); +} +END_API_FUNC + + +/************************************************ + * ALC HRTF functions + ************************************************/ + +/* alcGetStringiSOFT + * + * Gets a string parameter at the given index. + */ +ALC_API const ALCchar* ALC_APIENTRY alcGetStringiSOFT(ALCdevice *device, ALCenum paramName, ALCsizei index) +START_API_FUNC +{ + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type == Capture) + alcSetError(dev.get(), ALC_INVALID_DEVICE); + else switch(paramName) + { + case ALC_HRTF_SPECIFIER_SOFT: + if(index >= 0 && static_cast<size_t>(index) < dev->HrtfList.size()) + return dev->HrtfList[index].name.c_str(); + alcSetError(dev.get(), ALC_INVALID_VALUE); + break; + + default: + alcSetError(dev.get(), ALC_INVALID_ENUM); + break; + } + + return nullptr; +} +END_API_FUNC + +/* alcResetDeviceSOFT + * + * Resets the given device output, using the specified attribute list. + */ +ALC_API ALCboolean ALC_APIENTRY alcResetDeviceSOFT(ALCdevice *device, const ALCint *attribs) +START_API_FUNC +{ + std::unique_lock<std::recursive_mutex> listlock{ListLock}; + DeviceRef dev{VerifyDevice(device)}; + if(!dev || dev->Type == Capture) + { + listlock.unlock(); + alcSetError(dev.get(), ALC_INVALID_DEVICE); + return ALC_FALSE; + } + std::lock_guard<std::mutex> _{dev->StateLock}; + listlock.unlock(); + + /* Force the backend to stop mixing first since we're resetting. Also reset + * the connected state so lost devices can attempt recover. + */ + if(dev->Flags.get<DeviceRunning>()) + dev->Backend->stop(); + dev->Flags.unset<DeviceRunning>(); + device->Connected.store(true); + + ALCenum err{UpdateDeviceParams(dev.get(), attribs)}; + if(LIKELY(err == ALC_NO_ERROR)) return ALC_TRUE; + + alcSetError(dev.get(), err); + if(err == ALC_INVALID_DEVICE) + aluHandleDisconnect(dev.get(), "Device start failure"); + return ALC_FALSE; +} +END_API_FUNC diff --git a/alc/alcmain.h b/alc/alcmain.h new file mode 100644 index 00000000..a22e0e81 --- /dev/null +++ b/alc/alcmain.h @@ -0,0 +1,534 @@ +#ifndef ALC_MAIN_H +#define ALC_MAIN_H + +#include <algorithm> +#include <array> +#include <atomic> +#include <chrono> +#include <cstdint> +#include <cstddef> +#include <memory> +#include <mutex> +#include <string> +#include <utility> + +#include "AL/al.h" +#include "AL/alc.h" +#include "AL/alext.h" + +#include "albyte.h" +#include "almalloc.h" +#include "alnumeric.h" +#include "alspan.h" +#include "ambidefs.h" +#include "atomic.h" +#include "hrtf.h" +#include "inprogext.h" +#include "vector.h" + +class BFormatDec; +struct ALbuffer; +struct ALeffect; +struct ALfilter; +struct BackendBase; +struct Compressor; +struct EffectState; +struct FrontStablizer; +struct Uhj2Encoder; +struct bs2b; + + +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) +#define IS_LITTLE_ENDIAN (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +#else +static const union { + ALuint u; + ALubyte b[sizeof(ALuint)]; +} EndianTest = { 1 }; +#define IS_LITTLE_ENDIAN (EndianTest.b[0] == 1) +#endif + + +#define MIN_OUTPUT_RATE 8000 +#define DEFAULT_OUTPUT_RATE 44100 +#define DEFAULT_UPDATE_SIZE 882 /* 20ms */ +#define DEFAULT_NUM_UPDATES 3 + + +enum Channel { + FrontLeft = 0, + FrontRight, + FrontCenter, + LFE, + BackLeft, + BackRight, + BackCenter, + SideLeft, + SideRight, + + UpperFrontLeft, + UpperFrontRight, + UpperBackLeft, + UpperBackRight, + LowerFrontLeft, + LowerFrontRight, + LowerBackLeft, + LowerBackRight, + + Aux0, + Aux1, + Aux2, + Aux3, + Aux4, + Aux5, + Aux6, + Aux7, + Aux8, + Aux9, + Aux10, + Aux11, + Aux12, + Aux13, + Aux14, + Aux15, + + MaxChannels +}; + + +/* Device formats */ +enum DevFmtType : ALenum { + DevFmtByte = ALC_BYTE_SOFT, + DevFmtUByte = ALC_UNSIGNED_BYTE_SOFT, + DevFmtShort = ALC_SHORT_SOFT, + DevFmtUShort = ALC_UNSIGNED_SHORT_SOFT, + DevFmtInt = ALC_INT_SOFT, + DevFmtUInt = ALC_UNSIGNED_INT_SOFT, + DevFmtFloat = ALC_FLOAT_SOFT, + + DevFmtTypeDefault = DevFmtFloat +}; +enum DevFmtChannels : ALenum { + DevFmtMono = ALC_MONO_SOFT, + DevFmtStereo = ALC_STEREO_SOFT, + DevFmtQuad = ALC_QUAD_SOFT, + DevFmtX51 = ALC_5POINT1_SOFT, + DevFmtX61 = ALC_6POINT1_SOFT, + DevFmtX71 = ALC_7POINT1_SOFT, + DevFmtAmbi3D = ALC_BFORMAT3D_SOFT, + + /* Similar to 5.1, except using rear channels instead of sides */ + DevFmtX51Rear = 0x70000000, + + DevFmtChannelsDefault = DevFmtStereo +}; +#define MAX_OUTPUT_CHANNELS (16) + +/* DevFmtType traits, providing the type, etc given a DevFmtType. */ +template<DevFmtType T> +struct DevFmtTypeTraits { }; + +template<> +struct DevFmtTypeTraits<DevFmtByte> { using Type = ALbyte; }; +template<> +struct DevFmtTypeTraits<DevFmtUByte> { using Type = ALubyte; }; +template<> +struct DevFmtTypeTraits<DevFmtShort> { using Type = ALshort; }; +template<> +struct DevFmtTypeTraits<DevFmtUShort> { using Type = ALushort; }; +template<> +struct DevFmtTypeTraits<DevFmtInt> { using Type = ALint; }; +template<> +struct DevFmtTypeTraits<DevFmtUInt> { using Type = ALuint; }; +template<> +struct DevFmtTypeTraits<DevFmtFloat> { using Type = ALfloat; }; + + +ALsizei BytesFromDevFmt(DevFmtType type) noexcept; +ALsizei ChannelsFromDevFmt(DevFmtChannels chans, ALsizei ambiorder) noexcept; +inline ALsizei FrameSizeFromDevFmt(DevFmtChannels chans, DevFmtType type, ALsizei ambiorder) noexcept +{ return ChannelsFromDevFmt(chans, ambiorder) * BytesFromDevFmt(type); } + +enum class AmbiLayout { + FuMa = ALC_FUMA_SOFT, /* FuMa channel order */ + ACN = ALC_ACN_SOFT, /* ACN channel order */ + + Default = ACN +}; + +enum class AmbiNorm { + FuMa = ALC_FUMA_SOFT, /* FuMa normalization */ + SN3D = ALC_SN3D_SOFT, /* SN3D normalization */ + N3D = ALC_N3D_SOFT, /* N3D normalization */ + + Default = SN3D +}; + + +enum DeviceType { + Playback, + Capture, + Loopback +}; + + +enum RenderMode { + NormalRender, + StereoPair, + HrtfRender +}; + + +struct BufferSubList { + uint64_t FreeMask{~0_u64}; + ALbuffer *Buffers{nullptr}; /* 64 */ + + BufferSubList() noexcept = default; + BufferSubList(const BufferSubList&) = delete; + BufferSubList(BufferSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Buffers{rhs.Buffers} + { rhs.FreeMask = ~0_u64; rhs.Buffers = nullptr; } + ~BufferSubList(); + + BufferSubList& operator=(const BufferSubList&) = delete; + BufferSubList& operator=(BufferSubList&& rhs) noexcept + { std::swap(FreeMask, rhs.FreeMask); std::swap(Buffers, rhs.Buffers); return *this; } +}; + +struct EffectSubList { + uint64_t FreeMask{~0_u64}; + ALeffect *Effects{nullptr}; /* 64 */ + + EffectSubList() noexcept = default; + EffectSubList(const EffectSubList&) = delete; + EffectSubList(EffectSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Effects{rhs.Effects} + { rhs.FreeMask = ~0_u64; rhs.Effects = nullptr; } + ~EffectSubList(); + + EffectSubList& operator=(const EffectSubList&) = delete; + EffectSubList& operator=(EffectSubList&& rhs) noexcept + { std::swap(FreeMask, rhs.FreeMask); std::swap(Effects, rhs.Effects); return *this; } +}; + +struct FilterSubList { + uint64_t FreeMask{~0_u64}; + ALfilter *Filters{nullptr}; /* 64 */ + + FilterSubList() noexcept = default; + FilterSubList(const FilterSubList&) = delete; + FilterSubList(FilterSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Filters{rhs.Filters} + { rhs.FreeMask = ~0_u64; rhs.Filters = nullptr; } + ~FilterSubList(); + + FilterSubList& operator=(const FilterSubList&) = delete; + FilterSubList& operator=(FilterSubList&& rhs) noexcept + { std::swap(FreeMask, rhs.FreeMask); std::swap(Filters, rhs.Filters); return *this; } +}; + + +/* Maximum delay in samples for speaker distance compensation. */ +#define MAX_DELAY_LENGTH 1024 + +class DistanceComp { +public: + struct DistData { + ALfloat Gain{1.0f}; + ALsizei Length{0}; /* Valid range is [0...MAX_DELAY_LENGTH). */ + ALfloat *Buffer{nullptr}; + }; + +private: + std::array<DistData,MAX_OUTPUT_CHANNELS> mChannels; + al::vector<ALfloat,16> mSamples; + +public: + void setSampleCount(size_t new_size) { mSamples.resize(new_size); } + void clear() noexcept + { + for(auto &chan : mChannels) + { + chan.Gain = 1.0f; + chan.Length = 0; + chan.Buffer = nullptr; + } + using SampleVecT = decltype(mSamples); + SampleVecT{}.swap(mSamples); + } + + ALfloat *getSamples() noexcept { return mSamples.data(); } + + al::span<DistData,MAX_OUTPUT_CHANNELS> as_span() { return mChannels; } +}; + +struct BFChannelConfig { + ALfloat Scale; + ALsizei Index; +}; + +/* Size for temporary storage of buffer data, in ALfloats. Larger values need + * more memory, while smaller values may need more iterations. The value needs + * to be a sensible size, however, as it constrains the max stepping value used + * for mixing, as well as the maximum number of samples per mixing iteration. + */ +#define BUFFERSIZE 1024 + +using FloatBufferLine = std::array<float,BUFFERSIZE>; + +/* Maximum number of samples to pad on either end of a buffer for resampling. + * Note that both the beginning and end need padding! + */ +#define MAX_RESAMPLE_PADDING 24 + + +struct MixParams { + /* Coefficient channel mapping for mixing to the buffer. */ + std::array<BFChannelConfig,MAX_OUTPUT_CHANNELS> AmbiMap{}; + + al::span<FloatBufferLine> Buffer; +}; + +struct RealMixParams { + std::array<ALint,MaxChannels> ChannelIndex{}; + + al::span<FloatBufferLine> Buffer; +}; + +using POSTPROCESS = void(*)(ALCdevice *device, const ALsizei SamplesToDo); + +enum { + // Frequency was requested by the app or config file + FrequencyRequest, + // Channel configuration was requested by the config file + ChannelsRequest, + // Sample type was requested by the config file + SampleTypeRequest, + + // Specifies if the DSP is paused at user request + DevicePaused, + // Specifies if the device is currently running + DeviceRunning, + + DeviceFlagsCount +}; + +struct ALCdevice { + RefCount ref{1u}; + + std::atomic<bool> Connected{true}; + const DeviceType Type{}; + + ALuint Frequency{}; + ALuint UpdateSize{}; + ALuint BufferSize{}; + + DevFmtChannels FmtChans{}; + DevFmtType FmtType{}; + ALboolean IsHeadphones{AL_FALSE}; + ALsizei mAmbiOrder{0}; + /* For DevFmtAmbi* output only, specifies the channel order and + * normalization. + */ + AmbiLayout mAmbiLayout{AmbiLayout::Default}; + AmbiNorm mAmbiScale{AmbiNorm::Default}; + + ALCenum LimiterState{ALC_DONT_CARE_SOFT}; + + std::string DeviceName; + + // Device flags + al::bitfield<DeviceFlagsCount> Flags{}; + + std::string HrtfName; + al::vector<EnumeratedHrtf> HrtfList; + ALCenum HrtfStatus{ALC_FALSE}; + + std::atomic<ALCenum> LastError{ALC_NO_ERROR}; + + // Maximum number of sources that can be created + ALuint SourcesMax{}; + // Maximum number of slots that can be created + ALuint AuxiliaryEffectSlotMax{}; + + ALCuint NumMonoSources{}; + ALCuint NumStereoSources{}; + ALsizei NumAuxSends{}; + + // Map of Buffers for this device + std::mutex BufferLock; + al::vector<BufferSubList> BufferList; + + // Map of Effects for this device + std::mutex EffectLock; + al::vector<EffectSubList> EffectList; + + // Map of Filters for this device + std::mutex FilterLock; + al::vector<FilterSubList> FilterList; + + /* Rendering mode. */ + RenderMode mRenderMode{NormalRender}; + + /* The average speaker distance as determined by the ambdec configuration, + * HRTF data set, or the NFC-HOA reference delay. Only used for NFC. + */ + ALfloat AvgSpeakerDist{0.0f}; + + ALuint SamplesDone{0u}; + std::chrono::nanoseconds ClockBase{0}; + std::chrono::nanoseconds FixedLatency{0}; + + /* Temp storage used for mixer processing. */ + alignas(16) ALfloat SourceData[BUFFERSIZE + MAX_RESAMPLE_PADDING*2]; + alignas(16) ALfloat ResampledData[BUFFERSIZE]; + alignas(16) ALfloat FilteredData[BUFFERSIZE]; + union { + alignas(16) ALfloat HrtfSourceData[BUFFERSIZE + HRTF_HISTORY_LENGTH]; + alignas(16) ALfloat NfcSampleData[BUFFERSIZE]; + }; + alignas(16) float2 HrtfAccumData[BUFFERSIZE + HRIR_LENGTH]; + + /* Mixing buffer used by the Dry mix and Real output. */ + al::vector<FloatBufferLine, 16> MixBuffer; + + /* The "dry" path corresponds to the main output. */ + MixParams Dry; + ALuint NumChannelsPerOrder[MAX_AMBI_ORDER+1]{}; + + /* "Real" output, which will be written to the device buffer. May alias the + * dry buffer. + */ + RealMixParams RealOut; + + /* HRTF state and info */ + std::unique_ptr<DirectHrtfState> mHrtfState; + HrtfEntry *mHrtf{nullptr}; + + /* Ambisonic-to-UHJ encoder */ + std::unique_ptr<Uhj2Encoder> Uhj_Encoder; + + /* Ambisonic decoder for speakers */ + std::unique_ptr<BFormatDec> AmbiDecoder; + + /* Stereo-to-binaural filter */ + std::unique_ptr<bs2b> Bs2b; + + POSTPROCESS PostProcess{}; + + std::unique_ptr<FrontStablizer> Stablizer; + + std::unique_ptr<Compressor> Limiter; + + /* Delay buffers used to compensate for speaker distances. */ + DistanceComp ChannelDelay; + + /* Dithering control. */ + ALfloat DitherDepth{0.0f}; + ALuint DitherSeed{0u}; + + /* Running count of the mixer invocations, in 31.1 fixed point. This + * actually increments *twice* when mixing, first at the start and then at + * the end, so the bottom bit indicates if the device is currently mixing + * and the upper bits indicates how many mixes have been done. + */ + RefCount MixCount{0u}; + + // Contexts created on this device + std::atomic<al::FlexArray<ALCcontext*>*> mContexts{nullptr}; + + /* This lock protects the device state (format, update size, etc) from + * being from being changed in multiple threads, or being accessed while + * being changed. It's also used to serialize calls to the backend. + */ + std::mutex StateLock; + std::unique_ptr<BackendBase> Backend; + + + ALCdevice(DeviceType type); + ALCdevice(const ALCdevice&) = delete; + ALCdevice& operator=(const ALCdevice&) = delete; + ~ALCdevice(); + + ALsizei bytesFromFmt() const noexcept { return BytesFromDevFmt(FmtType); } + ALsizei channelsFromFmt() const noexcept { return ChannelsFromDevFmt(FmtChans, mAmbiOrder); } + ALsizei frameSizeFromFmt() const noexcept { return bytesFromFmt() * channelsFromFmt(); } + + DEF_NEWDEL(ALCdevice) +}; + +/* Must be less than 15 characters (16 including terminating null) for + * compatibility with pthread_setname_np limitations. */ +#define MIXER_THREAD_NAME "alsoft-mixer" + +#define RECORD_THREAD_NAME "alsoft-record" + + +enum { + /* End event thread processing. */ + EventType_KillThread = 0, + + /* User event types. */ + EventType_SourceStateChange = 1<<0, + EventType_BufferCompleted = 1<<1, + EventType_Error = 1<<2, + EventType_Performance = 1<<3, + EventType_Deprecated = 1<<4, + EventType_Disconnected = 1<<5, + + /* Internal events. */ + EventType_ReleaseEffectState = 65536, +}; + +struct AsyncEvent { + unsigned int EnumType{0u}; + union { + char dummy; + struct { + ALuint id; + ALenum state; + } srcstate; + struct { + ALuint id; + ALsizei count; + } bufcomp; + struct { + ALenum type; + ALuint id; + ALuint param; + ALchar msg[1008]; + } user; + EffectState *mEffectState; + } u{}; + + AsyncEvent() noexcept = default; + constexpr AsyncEvent(unsigned int type) noexcept : EnumType{type} { } +}; + + +void AllocateVoices(ALCcontext *context, size_t num_voices); + + +extern ALint RTPrioLevel; +void SetRTPriority(void); + +void SetDefaultChannelOrder(ALCdevice *device); +void SetDefaultWFXChannelOrder(ALCdevice *device); + +const ALCchar *DevFmtTypeString(DevFmtType type) noexcept; +const ALCchar *DevFmtChannelsString(DevFmtChannels chans) noexcept; + +/** + * GetChannelIdxByName + * + * Returns the index for the given channel name (e.g. FrontCenter), or -1 if it + * doesn't exist. + */ +inline ALint GetChannelIdxByName(const RealMixParams &real, Channel chan) noexcept +{ return real.ChannelIndex[chan]; } + + +void StartEventThrd(ALCcontext *ctx); +void StopEventThrd(ALCcontext *ctx); + + +al::vector<std::string> SearchDataFiles(const char *match, const char *subdir); + +#endif diff --git a/alc/alconfig.cpp b/alc/alconfig.cpp new file mode 100644 index 00000000..b246a91d --- /dev/null +++ b/alc/alconfig.cpp @@ -0,0 +1,545 @@ +/** + * 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 + */ + +#ifdef _WIN32 +#ifdef __MINGW32__ +#define _WIN32_IE 0x501 +#else +#define _WIN32_IE 0x400 +#endif +#endif + +#include "config.h" + +#include "alconfig.h" + +#include <cstdlib> +#include <cctype> +#include <cstring> +#ifdef _WIN32_IE +#include <windows.h> +#include <shlobj.h> +#endif +#ifdef __APPLE__ +#include <CoreFoundation/CoreFoundation.h> +#endif + +#include <vector> +#include <string> +#include <algorithm> + +#include "alcmain.h" +#include "logging.h" +#include "compat.h" + + +namespace { + +struct ConfigEntry { + std::string key; + std::string value; +}; +al::vector<ConfigEntry> ConfOpts; + + +std::string &lstrip(std::string &line) +{ + size_t pos{0}; + while(pos < line.length() && std::isspace(line[pos])) + ++pos; + line.erase(0, pos); + return line; +} + +bool readline(std::istream &f, std::string &output) +{ + while(f.good() && f.peek() == '\n') + f.ignore(); + + return std::getline(f, output) && !output.empty(); +} + +std:: string expdup(const char *str) +{ + std::string output; + + while(*str != '\0') + { + const char *addstr; + size_t addstrlen; + + if(str[0] != '$') + { + const char *next = std::strchr(str, '$'); + addstr = str; + addstrlen = next ? static_cast<size_t>(next-str) : std::strlen(str); + + str += addstrlen; + } + else + { + str++; + if(*str == '$') + { + const char *next = std::strchr(str+1, '$'); + addstr = str; + addstrlen = next ? static_cast<size_t>(next-str) : std::strlen(str); + + str += addstrlen; + } + else + { + bool hasbraces{(*str == '{')}; + if(hasbraces) str++; + + std::string envname; + while((std::isalnum(*str) || *str == '_')) + envname += *(str++); + + if(hasbraces && *str != '}') + continue; + + if(hasbraces) str++; + if((addstr=std::getenv(envname.c_str())) == nullptr) + continue; + addstrlen = std::strlen(addstr); + } + } + if(addstrlen == 0) + continue; + + output.append(addstr, addstrlen); + } + + return output; +} + +void LoadConfigFromFile(std::istream &f) +{ + std::string curSection; + std::string buffer; + + while(readline(f, buffer)) + { + while(!buffer.empty() && std::isspace(buffer.back())) + buffer.pop_back(); + if(lstrip(buffer).empty()) + continue; + + buffer.push_back(0); + char *line{&buffer[0]}; + + if(line[0] == '[') + { + char *section = line+1; + char *endsection; + + endsection = std::strchr(section, ']'); + if(!endsection || section == endsection) + { + ERR("config parse error: bad line \"%s\"\n", line); + continue; + } + if(endsection[1] != 0) + { + char *end = endsection+1; + while(std::isspace(*end)) + ++end; + if(*end != 0 && *end != '#') + { + ERR("config parse error: bad line \"%s\"\n", line); + continue; + } + } + *endsection = 0; + + curSection.clear(); + if(strcasecmp(section, "general") != 0) + { + do { + char *nextp = std::strchr(section, '%'); + if(!nextp) + { + curSection += section; + break; + } + + curSection.append(section, nextp); + section = nextp; + + if(((section[1] >= '0' && section[1] <= '9') || + (section[1] >= 'a' && section[1] <= 'f') || + (section[1] >= 'A' && section[1] <= 'F')) && + ((section[2] >= '0' && section[2] <= '9') || + (section[2] >= 'a' && section[2] <= 'f') || + (section[2] >= 'A' && section[2] <= 'F'))) + { + unsigned char b = 0; + if(section[1] >= '0' && section[1] <= '9') + b = (section[1]-'0') << 4; + else if(section[1] >= 'a' && section[1] <= 'f') + b = (section[1]-'a'+0xa) << 4; + else if(section[1] >= 'A' && section[1] <= 'F') + b = (section[1]-'A'+0x0a) << 4; + if(section[2] >= '0' && section[2] <= '9') + b |= (section[2]-'0'); + else if(section[2] >= 'a' && section[2] <= 'f') + b |= (section[2]-'a'+0xa); + else if(section[2] >= 'A' && section[2] <= 'F') + b |= (section[2]-'A'+0x0a); + curSection += static_cast<char>(b); + section += 3; + } + else if(section[1] == '%') + { + curSection += '%'; + section += 2; + } + else + { + curSection += '%'; + section += 1; + } + } while(*section != 0); + } + + continue; + } + + char *comment{std::strchr(line, '#')}; + if(comment) *(comment++) = 0; + if(!line[0]) continue; + + char key[256]{}; + char value[256]{}; + if(std::sscanf(line, "%255[^=] = \"%255[^\"]\"", key, value) == 2 || + std::sscanf(line, "%255[^=] = '%255[^\']'", key, value) == 2 || + std::sscanf(line, "%255[^=] = %255[^\n]", key, value) == 2) + { + /* sscanf doesn't handle '' or "" as empty values, so clip it + * manually. */ + if(std::strcmp(value, "\"\"") == 0 || std::strcmp(value, "''") == 0) + value[0] = 0; + } + else if(sscanf(line, "%255[^=] %255[=]", key, value) == 2) + { + /* Special case for 'key =' */ + value[0] = 0; + } + else + { + ERR("config parse error: malformed option line: \"%s\"\n\n", line); + continue; + } + + std::string fullKey; + if(!curSection.empty()) + { + fullKey += curSection; + fullKey += '/'; + } + fullKey += key; + while(!fullKey.empty() && std::isspace(fullKey.back())) + fullKey.pop_back(); + + /* Check if we already have this option set */ + auto ent = std::find_if(ConfOpts.begin(), ConfOpts.end(), + [&fullKey](const ConfigEntry &entry) -> bool + { return entry.key == fullKey; } + ); + if(ent != ConfOpts.end()) + ent->value = expdup(value); + else + { + ConfOpts.emplace_back(ConfigEntry{std::move(fullKey), expdup(value)}); + ent = ConfOpts.end()-1; + } + + TRACE("found '%s' = '%s'\n", ent->key.c_str(), ent->value.c_str()); + } + ConfOpts.shrink_to_fit(); +} + +} // namespace + + +#ifdef _WIN32 +void ReadALConfig() +{ + WCHAR buffer[MAX_PATH]; + if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE) + { + std::string filepath{wstr_to_utf8(buffer)}; + filepath += "\\alsoft.ini"; + + TRACE("Loading config %s...\n", filepath.c_str()); + al::ifstream f{filepath}; + if(f.is_open()) + LoadConfigFromFile(f); + } + + std::string ppath{GetProcBinary().path}; + if(!ppath.empty()) + { + ppath += "\\alsoft.ini"; + TRACE("Loading config %s...\n", ppath.c_str()); + al::ifstream f{ppath}; + if(f.is_open()) + LoadConfigFromFile(f); + } + + const WCHAR *str{_wgetenv(L"ALSOFT_CONF")}; + if(str != nullptr && *str) + { + std::string filepath{wstr_to_utf8(str)}; + + TRACE("Loading config %s...\n", filepath.c_str()); + al::ifstream f{filepath}; + if(f.is_open()) + LoadConfigFromFile(f); + } +} +#else +void ReadALConfig() +{ + const char *str{"/etc/openal/alsoft.conf"}; + + TRACE("Loading config %s...\n", str); + al::ifstream f{str}; + if(f.is_open()) + LoadConfigFromFile(f); + f.close(); + + if(!(str=getenv("XDG_CONFIG_DIRS")) || str[0] == 0) + str = "/etc/xdg"; + std::string confpaths = str; + /* Go through the list in reverse, since "the order of base directories + * denotes their importance; the first directory listed is the most + * important". Ergo, we need to load the settings from the later dirs + * first so that the settings in the earlier dirs override them. + */ + std::string fname; + while(!confpaths.empty()) + { + auto next = confpaths.find_last_of(':'); + if(next < confpaths.length()) + { + fname = confpaths.substr(next+1); + confpaths.erase(next); + } + else + { + fname = confpaths; + confpaths.clear(); + } + + if(fname.empty() || fname.front() != '/') + WARN("Ignoring XDG config dir: %s\n", fname.c_str()); + else + { + if(fname.back() != '/') fname += "/alsoft.conf"; + else fname += "alsoft.conf"; + + TRACE("Loading config %s...\n", fname.c_str()); + al::ifstream f{fname}; + if(f.is_open()) + LoadConfigFromFile(f); + } + fname.clear(); + } + +#ifdef __APPLE__ + CFBundleRef mainBundle = CFBundleGetMainBundle(); + if(mainBundle) + { + unsigned char fileName[PATH_MAX]; + CFURLRef configURL; + + if((configURL=CFBundleCopyResourceURL(mainBundle, CFSTR(".alsoftrc"), CFSTR(""), nullptr)) && + CFURLGetFileSystemRepresentation(configURL, true, fileName, sizeof(fileName))) + { + al::ifstream f{reinterpret_cast<char*>(fileName)}; + if(f.is_open()) + LoadConfigFromFile(f); + } + } +#endif + + if((str=getenv("HOME")) != nullptr && *str) + { + fname = str; + if(fname.back() != '/') fname += "/.alsoftrc"; + else fname += ".alsoftrc"; + + TRACE("Loading config %s...\n", fname.c_str()); + al::ifstream f{fname}; + if(f.is_open()) + LoadConfigFromFile(f); + } + + if((str=getenv("XDG_CONFIG_HOME")) != nullptr && str[0] != 0) + { + fname = str; + if(fname.back() != '/') fname += "/alsoft.conf"; + else fname += "alsoft.conf"; + } + else + { + fname.clear(); + if((str=getenv("HOME")) != nullptr && str[0] != 0) + { + fname = str; + if(fname.back() != '/') fname += "/.config/alsoft.conf"; + else fname += ".config/alsoft.conf"; + } + } + if(!fname.empty()) + { + TRACE("Loading config %s...\n", fname.c_str()); + al::ifstream f{fname}; + if(f.is_open()) + LoadConfigFromFile(f); + } + + std::string ppath{GetProcBinary().path}; + if(!ppath.empty()) + { + if(ppath.back() != '/') ppath += "/alsoft.conf"; + else ppath += "alsoft.conf"; + + TRACE("Loading config %s...\n", ppath.c_str()); + al::ifstream f{ppath}; + if(f.is_open()) + LoadConfigFromFile(f); + } + + if((str=getenv("ALSOFT_CONF")) != nullptr && *str) + { + TRACE("Loading config %s...\n", str); + al::ifstream f{str}; + if(f.is_open()) + LoadConfigFromFile(f); + } +} +#endif + +const char *GetConfigValue(const char *devName, const char *blockName, const char *keyName, const char *def) +{ + if(!keyName) + return def; + + std::string key; + if(blockName && strcasecmp(blockName, "general") != 0) + { + key = blockName; + if(devName) + { + key += '/'; + key += devName; + } + key += '/'; + key += keyName; + } + else + { + if(devName) + { + key = devName; + key += '/'; + } + key += keyName; + } + + auto iter = std::find_if(ConfOpts.cbegin(), ConfOpts.cend(), + [&key](const ConfigEntry &entry) -> bool + { return entry.key == key; } + ); + if(iter != ConfOpts.cend()) + { + TRACE("Found %s = \"%s\"\n", key.c_str(), iter->value.c_str()); + if(!iter->value.empty()) + return iter->value.c_str(); + return def; + } + + if(!devName) + { + TRACE("Key %s not found\n", key.c_str()); + return def; + } + return GetConfigValue(nullptr, blockName, keyName, def); +} + +int ConfigValueExists(const char *devName, const char *blockName, const char *keyName) +{ + const char *val = GetConfigValue(devName, blockName, keyName, ""); + return val[0] != 0; +} + +al::optional<std::string> ConfigValueStr(const char *devName, const char *blockName, const char *keyName) +{ + const char *val = GetConfigValue(devName, blockName, keyName, ""); + if(!val[0]) return al::nullopt; + + return al::make_optional<std::string>(val); +} + +al::optional<int> ConfigValueInt(const char *devName, const char *blockName, const char *keyName) +{ + const char *val = GetConfigValue(devName, blockName, keyName, ""); + if(!val[0]) return al::nullopt; + + return al::make_optional(static_cast<int>(std::strtol(val, nullptr, 0))); +} + +al::optional<unsigned int> ConfigValueUInt(const char *devName, const char *blockName, const char *keyName) +{ + const char *val = GetConfigValue(devName, blockName, keyName, ""); + if(!val[0]) return al::nullopt; + + return al::make_optional(static_cast<unsigned int>(std::strtoul(val, nullptr, 0))); +} + +al::optional<float> ConfigValueFloat(const char *devName, const char *blockName, const char *keyName) +{ + const char *val = GetConfigValue(devName, blockName, keyName, ""); + if(!val[0]) return al::nullopt; + + return al::make_optional(std::strtof(val, nullptr)); +} + +al::optional<bool> ConfigValueBool(const char *devName, const char *blockName, const char *keyName) +{ + const char *val = GetConfigValue(devName, blockName, keyName, ""); + if(!val[0]) return al::nullopt; + + return al::make_optional( + strcasecmp(val, "true") == 0 || strcasecmp(val, "yes") == 0 || + strcasecmp(val, "on") == 0 || atoi(val) != 0); +} + +int GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, int def) +{ + const char *val = GetConfigValue(devName, blockName, keyName, ""); + + if(!val[0]) return def != 0; + return (strcasecmp(val, "true") == 0 || strcasecmp(val, "yes") == 0 || + strcasecmp(val, "on") == 0 || atoi(val) != 0); +} diff --git a/alc/alconfig.h b/alc/alconfig.h new file mode 100644 index 00000000..ffc7adad --- /dev/null +++ b/alc/alconfig.h @@ -0,0 +1,20 @@ +#ifndef ALCONFIG_H +#define ALCONFIG_H + +#include <string> + +#include "aloptional.h" + +void ReadALConfig(); + +int ConfigValueExists(const char *devName, const char *blockName, const char *keyName); +const char *GetConfigValue(const char *devName, const char *blockName, const char *keyName, const char *def); +int GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, int def); + +al::optional<std::string> ConfigValueStr(const char *devName, const char *blockName, const char *keyName); +al::optional<int> ConfigValueInt(const char *devName, const char *blockName, const char *keyName); +al::optional<unsigned int> ConfigValueUInt(const char *devName, const char *blockName, const char *keyName); +al::optional<float> ConfigValueFloat(const char *devName, const char *blockName, const char *keyName); +al::optional<bool> ConfigValueBool(const char *devName, const char *blockName, const char *keyName); + +#endif /* ALCONFIG_H */ diff --git a/alc/alcontext.h b/alc/alcontext.h new file mode 100644 index 00000000..cf956079 --- /dev/null +++ b/alc/alcontext.h @@ -0,0 +1,217 @@ +#ifndef ALCONTEXT_H +#define ALCONTEXT_H + +#include <mutex> +#include <atomic> +#include <memory> +#include <thread> + +#include "AL/al.h" +#include "AL/alc.h" +#include "AL/alext.h" +#include "inprogext.h" + +#include "atomic.h" +#include "vector.h" +#include "threads.h" +#include "almalloc.h" +#include "alnumeric.h" + +#include "alListener.h" +#include "alu.h" + + +struct ALsource; +struct ALeffectslot; +struct ALcontextProps; +struct ALlistenerProps; +struct ALvoiceProps; +struct ALeffectslotProps; +struct RingBuffer; + +enum class DistanceModel { + InverseClamped = AL_INVERSE_DISTANCE_CLAMPED, + LinearClamped = AL_LINEAR_DISTANCE_CLAMPED, + ExponentClamped = AL_EXPONENT_DISTANCE_CLAMPED, + Inverse = AL_INVERSE_DISTANCE, + Linear = AL_LINEAR_DISTANCE, + Exponent = AL_EXPONENT_DISTANCE, + Disable = AL_NONE, + + Default = InverseClamped +}; + +struct SourceSubList { + uint64_t FreeMask{~0_u64}; + ALsource *Sources{nullptr}; /* 64 */ + + SourceSubList() noexcept = default; + SourceSubList(const SourceSubList&) = delete; + SourceSubList(SourceSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Sources{rhs.Sources} + { rhs.FreeMask = ~0_u64; rhs.Sources = nullptr; } + ~SourceSubList(); + + SourceSubList& operator=(const SourceSubList&) = delete; + SourceSubList& operator=(SourceSubList&& rhs) noexcept + { std::swap(FreeMask, rhs.FreeMask); std::swap(Sources, rhs.Sources); return *this; } +}; + +struct EffectSlotSubList { + uint64_t FreeMask{~0_u64}; + ALeffectslot *EffectSlots{nullptr}; /* 64 */ + + EffectSlotSubList() noexcept = default; + EffectSlotSubList(const EffectSlotSubList&) = delete; + EffectSlotSubList(EffectSlotSubList&& rhs) noexcept + : FreeMask{rhs.FreeMask}, EffectSlots{rhs.EffectSlots} + { rhs.FreeMask = ~0_u64; rhs.EffectSlots = nullptr; } + ~EffectSlotSubList(); + + EffectSlotSubList& operator=(const EffectSlotSubList&) = delete; + EffectSlotSubList& operator=(EffectSlotSubList&& rhs) noexcept + { std::swap(FreeMask, rhs.FreeMask); std::swap(EffectSlots, rhs.EffectSlots); return *this; } +}; + +struct ALCcontext { + RefCount ref{1u}; + + al::vector<SourceSubList> SourceList; + ALuint NumSources{0}; + std::mutex SourceLock; + + al::vector<EffectSlotSubList> EffectSlotList; + ALuint NumEffectSlots{0u}; + std::mutex EffectSlotLock; + + std::atomic<ALenum> LastError{AL_NO_ERROR}; + + DistanceModel mDistanceModel{DistanceModel::Default}; + ALboolean SourceDistanceModel{AL_FALSE}; + + ALfloat DopplerFactor{1.0f}; + ALfloat DopplerVelocity{1.0f}; + ALfloat SpeedOfSound{}; + ALfloat MetersPerUnit{1.0f}; + + std::atomic_flag PropsClean; + std::atomic<bool> DeferUpdates{false}; + + std::mutex PropLock; + + /* Counter for the pre-mixing updates, in 31.1 fixed point (lowest bit + * indicates if updates are currently happening). + */ + RefCount UpdateCount{0u}; + std::atomic<bool> HoldUpdates{false}; + + ALfloat GainBoost{1.0f}; + + std::atomic<ALcontextProps*> Update{nullptr}; + + /* Linked lists of unused property containers, free to use for future + * updates. + */ + std::atomic<ALcontextProps*> FreeContextProps{nullptr}; + std::atomic<ALlistenerProps*> FreeListenerProps{nullptr}; + std::atomic<ALvoiceProps*> FreeVoiceProps{nullptr}; + std::atomic<ALeffectslotProps*> FreeEffectslotProps{nullptr}; + + std::unique_ptr<al::FlexArray<ALvoice>> Voices{nullptr}; + std::atomic<ALuint> VoiceCount{0u}; + + using ALeffectslotArray = al::FlexArray<ALeffectslot*>; + std::atomic<ALeffectslotArray*> ActiveAuxSlots{nullptr}; + + std::thread EventThread; + al::semaphore EventSem; + std::unique_ptr<RingBuffer> AsyncEvents; + std::atomic<ALbitfieldSOFT> EnabledEvts{0u}; + std::mutex EventCbLock; + ALEVENTPROCSOFT EventCb{}; + void *EventParam{nullptr}; + + /* Default effect slot */ + std::unique_ptr<ALeffectslot> DefaultSlot; + + ALCdevice *const Device; + const ALCchar *ExtensionList{nullptr}; + + ALlistener Listener{}; + + + ALCcontext(ALCdevice *device); + ALCcontext(const ALCcontext&) = delete; + ALCcontext& operator=(const ALCcontext&) = delete; + ~ALCcontext(); + + DEF_NEWDEL(ALCcontext) +}; + +void ALCcontext_DecRef(ALCcontext *context); + +void UpdateContextProps(ALCcontext *context); + +void ALCcontext_DeferUpdates(ALCcontext *context); +void ALCcontext_ProcessUpdates(ALCcontext *context); + + +/* Simple RAII context reference. Takes the reference of the provided + * ALCcontext, and decrements it when leaving scope. Movable (transfer + * reference) but not copyable (no new references). + */ +class ContextRef { + ALCcontext *mCtx{nullptr}; + + void reset() noexcept + { + if(mCtx) + ALCcontext_DecRef(mCtx); + mCtx = nullptr; + } + +public: + ContextRef() noexcept = default; + ContextRef(ContextRef&& rhs) noexcept : mCtx{rhs.mCtx} + { rhs.mCtx = nullptr; } + explicit ContextRef(ALCcontext *ctx) noexcept : mCtx(ctx) { } + ~ContextRef() { reset(); } + + ContextRef& operator=(const ContextRef&) = delete; + ContextRef& operator=(ContextRef&& rhs) noexcept + { std::swap(mCtx, rhs.mCtx); return *this; } + + operator bool() const noexcept { return mCtx != nullptr; } + + ALCcontext* operator->() const noexcept { return mCtx; } + ALCcontext* get() const noexcept { return mCtx; } + + ALCcontext* release() noexcept + { + ALCcontext *ret{mCtx}; + mCtx = nullptr; + return ret; + } +}; + +inline bool operator==(const ContextRef &lhs, const ALCcontext *rhs) noexcept +{ return lhs.get() == rhs; } +inline bool operator!=(const ContextRef &lhs, const ALCcontext *rhs) noexcept +{ return !(lhs == rhs); } +inline bool operator<(const ContextRef &lhs, const ALCcontext *rhs) noexcept +{ return lhs.get() < rhs; } + +ContextRef GetContextRef(void); + + +struct ALcontextProps { + ALfloat DopplerFactor; + ALfloat DopplerVelocity; + ALfloat SpeedOfSound; + ALboolean SourceDistanceModel; + DistanceModel mDistanceModel; + ALfloat MetersPerUnit; + + std::atomic<ALcontextProps*> next; +}; + +#endif /* ALCONTEXT_H */ diff --git a/alc/alu.cpp b/alc/alu.cpp new file mode 100644 index 00000000..cc1a5a98 --- /dev/null +++ b/alc/alu.cpp @@ -0,0 +1,1798 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 1999-2007 by authors. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include "alu.h" + +#include <algorithm> +#include <array> +#include <atomic> +#include <cassert> +#include <chrono> +#include <climits> +#include <cmath> +#include <cstdarg> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <functional> +#include <iterator> +#include <limits> +#include <memory> +#include <new> +#include <numeric> +#include <utility> + +#include "AL/al.h" +#include "AL/alc.h" +#include "AL/efx.h" + +#include "alAuxEffectSlot.h" +#include "alBuffer.h" +#include "alcmain.h" +#include "alEffect.h" +#include "alListener.h" +#include "alcontext.h" +#include "almalloc.h" +#include "alnumeric.h" +#include "alspan.h" +#include "ambidefs.h" +#include "atomic.h" +#include "bformatdec.h" +#include "bs2b.h" +#include "cpu_caps.h" +#include "effects/base.h" +#include "filters/biquad.h" +#include "filters/nfc.h" +#include "filters/splitter.h" +#include "fpu_modes.h" +#include "hrtf.h" +#include "inprogext.h" +#include "mastering.h" +#include "math_defs.h" +#include "mixer/defs.h" +#include "opthelpers.h" +#include "ringbuffer.h" +#include "threads.h" +#include "uhjfilter.h" +#include "vecmat.h" +#include "vector.h" + +#include "bsinc_inc.h" + + +namespace { + +using namespace std::placeholders; + +ALfloat InitConeScale() +{ + ALfloat ret{1.0f}; + const char *str{getenv("__ALSOFT_HALF_ANGLE_CONES")}; + if(str && (strcasecmp(str, "true") == 0 || strtol(str, nullptr, 0) == 1)) + ret *= 0.5f; + return ret; +} + +ALfloat InitZScale() +{ + ALfloat ret{1.0f}; + const char *str{getenv("__ALSOFT_REVERSE_Z")}; + if(str && (strcasecmp(str, "true") == 0 || strtol(str, nullptr, 0) == 1)) + ret *= -1.0f; + return ret; +} + +ALboolean InitReverbSOS() +{ + ALboolean ret{AL_FALSE}; + const char *str{getenv("__ALSOFT_REVERB_IGNORES_SOUND_SPEED")}; + if(str && (strcasecmp(str, "true") == 0 || strtol(str, nullptr, 0) == 1)) + ret = AL_TRUE; + return ret; +} + +} // namespace + +/* Cone scalar */ +const ALfloat ConeScale{InitConeScale()}; + +/* Localized Z scalar for mono sources */ +const ALfloat ZScale{InitZScale()}; + +/* Force default speed of sound for distance-related reverb decay. */ +const ALboolean OverrideReverbSpeedOfSound{InitReverbSOS()}; + + +namespace { + +void ClearArray(ALfloat (&f)[MAX_OUTPUT_CHANNELS]) +{ + std::fill(std::begin(f), std::end(f), 0.0f); +} + +struct ChanMap { + Channel channel; + ALfloat angle; + ALfloat elevation; +}; + +HrtfDirectMixerFunc MixDirectHrtf = MixDirectHrtf_<CTag>; +inline HrtfDirectMixerFunc SelectHrtfMixer(void) +{ +#ifdef HAVE_NEON + if((CPUCapFlags&CPU_CAP_NEON)) + return MixDirectHrtf_<NEONTag>; +#endif +#ifdef HAVE_SSE + if((CPUCapFlags&CPU_CAP_SSE)) + return MixDirectHrtf_<SSETag>; +#endif + + return MixDirectHrtf_<CTag>; +} + +} // namespace + +void aluInit(void) +{ + MixDirectHrtf = SelectHrtfMixer(); +} + + +void ProcessHrtf(ALCdevice *device, const ALsizei SamplesToDo) +{ + /* HRTF is stereo output only. */ + const int lidx{device->RealOut.ChannelIndex[FrontLeft]}; + const int ridx{device->RealOut.ChannelIndex[FrontRight]}; + ASSUME(lidx >= 0 && ridx >= 0); + + DirectHrtfState *state{device->mHrtfState.get()}; + MixDirectHrtf(device->RealOut.Buffer[lidx], device->RealOut.Buffer[ridx], device->Dry.Buffer, + device->HrtfAccumData, state, SamplesToDo); +} + +void ProcessAmbiDec(ALCdevice *device, const ALsizei SamplesToDo) +{ + BFormatDec *ambidec{device->AmbiDecoder.get()}; + ambidec->process(device->RealOut.Buffer, device->Dry.Buffer.data(), SamplesToDo); +} + +void ProcessUhj(ALCdevice *device, const ALsizei SamplesToDo) +{ + /* UHJ is stereo output only. */ + const int lidx{device->RealOut.ChannelIndex[FrontLeft]}; + const int ridx{device->RealOut.ChannelIndex[FrontRight]}; + ASSUME(lidx >= 0 && ridx >= 0); + + /* Encode to stereo-compatible 2-channel UHJ output. */ + Uhj2Encoder *uhj2enc{device->Uhj_Encoder.get()}; + uhj2enc->encode(device->RealOut.Buffer[lidx], device->RealOut.Buffer[ridx], + device->Dry.Buffer.data(), SamplesToDo); +} + +void ProcessBs2b(ALCdevice *device, const ALsizei SamplesToDo) +{ + /* First, decode the ambisonic mix to the "real" output. */ + BFormatDec *ambidec{device->AmbiDecoder.get()}; + ambidec->process(device->RealOut.Buffer, device->Dry.Buffer.data(), SamplesToDo); + + /* BS2B is stereo output only. */ + const int lidx{device->RealOut.ChannelIndex[FrontLeft]}; + const int ridx{device->RealOut.ChannelIndex[FrontRight]}; + ASSUME(lidx >= 0 && ridx >= 0); + + /* Now apply the BS2B binaural/crossfeed filter. */ + bs2b_cross_feed(device->Bs2b.get(), device->RealOut.Buffer[lidx].data(), + device->RealOut.Buffer[ridx].data(), SamplesToDo); +} + + +/* Prepares the interpolator for a given rate (determined by increment). + * + * With a bit of work, and a trade of memory for CPU cost, this could be + * modified for use with an interpolated increment for buttery-smooth pitch + * changes. + */ +void BsincPrepare(const ALuint increment, BsincState *state, const BSincTable *table) +{ + ALsizei si{BSINC_SCALE_COUNT - 1}; + ALfloat sf{0.0f}; + + if(increment > FRACTIONONE) + { + sf = static_cast<ALfloat>FRACTIONONE / increment; + sf = maxf(0.0f, (BSINC_SCALE_COUNT-1) * (sf-table->scaleBase) * table->scaleRange); + si = float2int(sf); + /* The interpolation factor is fit to this diagonally-symmetric curve + * to reduce the transition ripple caused by interpolating different + * scales of the sinc function. + */ + sf = 1.0f - std::cos(std::asin(sf - si)); + } + + state->sf = sf; + state->m = table->m[si]; + state->l = (state->m/2) - 1; + state->filter = table->Tab + table->filterOffset[si]; +} + + +namespace { + +/* This RNG method was created based on the math found in opusdec. It's quick, + * and starting with a seed value of 22222, is suitable for generating + * whitenoise. + */ +inline ALuint dither_rng(ALuint *seed) noexcept +{ + *seed = (*seed * 96314165) + 907633515; + return *seed; +} + + +inline alu::Vector aluCrossproduct(const alu::Vector &in1, const alu::Vector &in2) +{ + return alu::Vector{ + in1[1]*in2[2] - in1[2]*in2[1], + in1[2]*in2[0] - in1[0]*in2[2], + in1[0]*in2[1] - in1[1]*in2[0], + 0.0f + }; +} + +inline ALfloat aluDotproduct(const alu::Vector &vec1, const alu::Vector &vec2) +{ + return vec1[0]*vec2[0] + vec1[1]*vec2[1] + vec1[2]*vec2[2]; +} + + +alu::Vector operator*(const alu::Matrix &mtx, const alu::Vector &vec) noexcept +{ + return alu::Vector{ + vec[0]*mtx[0][0] + vec[1]*mtx[1][0] + vec[2]*mtx[2][0] + vec[3]*mtx[3][0], + vec[0]*mtx[0][1] + vec[1]*mtx[1][1] + vec[2]*mtx[2][1] + vec[3]*mtx[3][1], + vec[0]*mtx[0][2] + vec[1]*mtx[1][2] + vec[2]*mtx[2][2] + vec[3]*mtx[3][2], + vec[0]*mtx[0][3] + vec[1]*mtx[1][3] + vec[2]*mtx[2][3] + vec[3]*mtx[3][3] + }; +} + + +bool CalcContextParams(ALCcontext *Context) +{ + ALcontextProps *props{Context->Update.exchange(nullptr, std::memory_order_acq_rel)}; + if(!props) return false; + + ALlistener &Listener = Context->Listener; + Listener.Params.MetersPerUnit = props->MetersPerUnit; + + Listener.Params.DopplerFactor = props->DopplerFactor; + Listener.Params.SpeedOfSound = props->SpeedOfSound * props->DopplerVelocity; + if(!OverrideReverbSpeedOfSound) + Listener.Params.ReverbSpeedOfSound = Listener.Params.SpeedOfSound * + Listener.Params.MetersPerUnit; + + Listener.Params.SourceDistanceModel = props->SourceDistanceModel; + Listener.Params.mDistanceModel = props->mDistanceModel; + + AtomicReplaceHead(Context->FreeContextProps, props); + return true; +} + +bool CalcListenerParams(ALCcontext *Context) +{ + ALlistener &Listener = Context->Listener; + + ALlistenerProps *props{Listener.Update.exchange(nullptr, std::memory_order_acq_rel)}; + if(!props) return false; + + /* AT then UP */ + alu::Vector N{props->OrientAt[0], props->OrientAt[1], props->OrientAt[2], 0.0f}; + N.normalize(); + alu::Vector V{props->OrientUp[0], props->OrientUp[1], props->OrientUp[2], 0.0f}; + V.normalize(); + /* Build and normalize right-vector */ + alu::Vector U{aluCrossproduct(N, V)}; + U.normalize(); + + Listener.Params.Matrix = alu::Matrix{ + U[0], V[0], -N[0], 0.0f, + U[1], V[1], -N[1], 0.0f, + U[2], V[2], -N[2], 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + const alu::Vector P{Listener.Params.Matrix * + alu::Vector{props->Position[0], props->Position[1], props->Position[2], 1.0f}}; + Listener.Params.Matrix.setRow(3, -P[0], -P[1], -P[2], 1.0f); + + const alu::Vector vel{props->Velocity[0], props->Velocity[1], props->Velocity[2], 0.0f}; + Listener.Params.Velocity = Listener.Params.Matrix * vel; + + Listener.Params.Gain = props->Gain * Context->GainBoost; + + AtomicReplaceHead(Context->FreeListenerProps, props); + return true; +} + +bool CalcEffectSlotParams(ALeffectslot *slot, ALCcontext *context, bool force) +{ + ALeffectslotProps *props{slot->Update.exchange(nullptr, std::memory_order_acq_rel)}; + if(!props && !force) return false; + + EffectState *state; + if(!props) + state = slot->Params.mEffectState; + else + { + slot->Params.Gain = props->Gain; + slot->Params.AuxSendAuto = props->AuxSendAuto; + slot->Params.Target = props->Target; + slot->Params.EffectType = props->Type; + slot->Params.mEffectProps = props->Props; + if(IsReverbEffect(props->Type)) + { + slot->Params.RoomRolloff = props->Props.Reverb.RoomRolloffFactor; + slot->Params.DecayTime = props->Props.Reverb.DecayTime; + slot->Params.DecayLFRatio = props->Props.Reverb.DecayLFRatio; + slot->Params.DecayHFRatio = props->Props.Reverb.DecayHFRatio; + slot->Params.DecayHFLimit = props->Props.Reverb.DecayHFLimit; + slot->Params.AirAbsorptionGainHF = props->Props.Reverb.AirAbsorptionGainHF; + } + else + { + slot->Params.RoomRolloff = 0.0f; + slot->Params.DecayTime = 0.0f; + slot->Params.DecayLFRatio = 0.0f; + slot->Params.DecayHFRatio = 0.0f; + slot->Params.DecayHFLimit = AL_FALSE; + slot->Params.AirAbsorptionGainHF = 1.0f; + } + + state = props->State; + props->State = nullptr; + EffectState *oldstate{slot->Params.mEffectState}; + slot->Params.mEffectState = state; + + /* Manually decrement the old effect state's refcount if it's greater + * than 1. We need to be a bit clever here to avoid the refcount + * reaching 0 since it can't be deleted in the mixer. + */ + ALuint oldval{oldstate->mRef.load(std::memory_order_acquire)}; + while(oldval > 1 && !oldstate->mRef.compare_exchange_weak(oldval, oldval-1, + std::memory_order_acq_rel, std::memory_order_acquire)) + { + /* oldval was updated with the current value on failure, so just + * try again. + */ + } + + if(oldval < 2) + { + /* Otherwise, if it would be deleted, send it off with a release + * event. + */ + RingBuffer *ring{context->AsyncEvents.get()}; + auto evt_vec = ring->getWriteVector(); + if(LIKELY(evt_vec.first.len > 0)) + { + AsyncEvent *evt{new (evt_vec.first.buf) AsyncEvent{EventType_ReleaseEffectState}}; + evt->u.mEffectState = oldstate; + ring->writeAdvance(1); + context->EventSem.post(); + } + else + { + /* If writing the event failed, the queue was probably full. + * Store the old state in the property object where it can + * eventually be cleaned up sometime later (not ideal, but + * better than blocking or leaking). + */ + props->State = oldstate; + } + } + + AtomicReplaceHead(context->FreeEffectslotProps, props); + } + + EffectTarget output; + if(ALeffectslot *target{slot->Params.Target}) + output = EffectTarget{&target->Wet, nullptr}; + else + { + ALCdevice *device{context->Device}; + output = EffectTarget{&device->Dry, &device->RealOut}; + } + state->update(context, slot, &slot->Params.mEffectProps, output); + return true; +} + + +/* Scales the given azimuth toward the side (+/- pi/2 radians) for positions in + * front. + */ +inline float ScaleAzimuthFront(float azimuth, float scale) +{ + const ALfloat abs_azi{std::fabs(azimuth)}; + if(!(abs_azi > al::MathDefs<float>::Pi()*0.5f)) + return minf(abs_azi*scale, al::MathDefs<float>::Pi()*0.5f) * std::copysign(1.0f, azimuth); + return azimuth; +} + +void CalcPanningAndFilters(ALvoice *voice, const ALfloat xpos, const ALfloat ypos, + const ALfloat zpos, const ALfloat Distance, const ALfloat Spread, const ALfloat DryGain, + const ALfloat DryGainHF, const ALfloat DryGainLF, const ALfloat (&WetGain)[MAX_SENDS], + const ALfloat (&WetGainLF)[MAX_SENDS], const ALfloat (&WetGainHF)[MAX_SENDS], + ALeffectslot *(&SendSlots)[MAX_SENDS], const ALvoicePropsBase *props, + const ALlistener &Listener, const ALCdevice *Device) +{ + static constexpr ChanMap MonoMap[1]{ + { FrontCenter, 0.0f, 0.0f } + }, RearMap[2]{ + { BackLeft, Deg2Rad(-150.0f), Deg2Rad(0.0f) }, + { BackRight, Deg2Rad( 150.0f), Deg2Rad(0.0f) } + }, QuadMap[4]{ + { FrontLeft, Deg2Rad( -45.0f), Deg2Rad(0.0f) }, + { FrontRight, Deg2Rad( 45.0f), Deg2Rad(0.0f) }, + { BackLeft, Deg2Rad(-135.0f), Deg2Rad(0.0f) }, + { BackRight, Deg2Rad( 135.0f), Deg2Rad(0.0f) } + }, X51Map[6]{ + { FrontLeft, Deg2Rad( -30.0f), Deg2Rad(0.0f) }, + { FrontRight, Deg2Rad( 30.0f), Deg2Rad(0.0f) }, + { FrontCenter, Deg2Rad( 0.0f), Deg2Rad(0.0f) }, + { LFE, 0.0f, 0.0f }, + { SideLeft, Deg2Rad(-110.0f), Deg2Rad(0.0f) }, + { SideRight, Deg2Rad( 110.0f), Deg2Rad(0.0f) } + }, X61Map[7]{ + { FrontLeft, Deg2Rad(-30.0f), Deg2Rad(0.0f) }, + { FrontRight, Deg2Rad( 30.0f), Deg2Rad(0.0f) }, + { FrontCenter, Deg2Rad( 0.0f), Deg2Rad(0.0f) }, + { LFE, 0.0f, 0.0f }, + { BackCenter, Deg2Rad(180.0f), Deg2Rad(0.0f) }, + { SideLeft, Deg2Rad(-90.0f), Deg2Rad(0.0f) }, + { SideRight, Deg2Rad( 90.0f), Deg2Rad(0.0f) } + }, X71Map[8]{ + { FrontLeft, Deg2Rad( -30.0f), Deg2Rad(0.0f) }, + { FrontRight, Deg2Rad( 30.0f), Deg2Rad(0.0f) }, + { FrontCenter, Deg2Rad( 0.0f), Deg2Rad(0.0f) }, + { LFE, 0.0f, 0.0f }, + { BackLeft, Deg2Rad(-150.0f), Deg2Rad(0.0f) }, + { BackRight, Deg2Rad( 150.0f), Deg2Rad(0.0f) }, + { SideLeft, Deg2Rad( -90.0f), Deg2Rad(0.0f) }, + { SideRight, Deg2Rad( 90.0f), Deg2Rad(0.0f) } + }; + + ChanMap StereoMap[2]{ + { FrontLeft, Deg2Rad(-30.0f), Deg2Rad(0.0f) }, + { FrontRight, Deg2Rad( 30.0f), Deg2Rad(0.0f) } + }; + + const auto Frequency = static_cast<ALfloat>(Device->Frequency); + const ALsizei NumSends{Device->NumAuxSends}; + ASSUME(NumSends >= 0); + + bool DirectChannels{props->DirectChannels != AL_FALSE}; + const ChanMap *chans{nullptr}; + ALsizei num_channels{0}; + bool isbformat{false}; + ALfloat downmix_gain{1.0f}; + switch(voice->mFmtChannels) + { + case FmtMono: + chans = MonoMap; + num_channels = 1; + /* Mono buffers are never played direct. */ + DirectChannels = false; + break; + + case FmtStereo: + /* Convert counter-clockwise to clockwise. */ + StereoMap[0].angle = -props->StereoPan[0]; + StereoMap[1].angle = -props->StereoPan[1]; + + chans = StereoMap; + num_channels = 2; + downmix_gain = 1.0f / 2.0f; + break; + + case FmtRear: + chans = RearMap; + num_channels = 2; + downmix_gain = 1.0f / 2.0f; + break; + + case FmtQuad: + chans = QuadMap; + num_channels = 4; + downmix_gain = 1.0f / 4.0f; + break; + + case FmtX51: + chans = X51Map; + num_channels = 6; + /* NOTE: Excludes LFE. */ + downmix_gain = 1.0f / 5.0f; + break; + + case FmtX61: + chans = X61Map; + num_channels = 7; + /* NOTE: Excludes LFE. */ + downmix_gain = 1.0f / 6.0f; + break; + + case FmtX71: + chans = X71Map; + num_channels = 8; + /* NOTE: Excludes LFE. */ + downmix_gain = 1.0f / 7.0f; + break; + + case FmtBFormat2D: + num_channels = 3; + isbformat = true; + DirectChannels = false; + break; + + case FmtBFormat3D: + num_channels = 4; + isbformat = true; + DirectChannels = false; + break; + } + ASSUME(num_channels > 0); + + std::for_each(voice->mChans.begin(), voice->mChans.begin()+num_channels, + [NumSends](ALvoice::ChannelData &chandata) -> void + { + chandata.mDryParams.Hrtf.Target = HrtfFilter{}; + ClearArray(chandata.mDryParams.Gains.Target); + std::for_each(chandata.mWetParams.begin(), chandata.mWetParams.begin()+NumSends, + [](SendParams ¶ms) -> void { ClearArray(params.Gains.Target); }); + }); + + voice->mFlags &= ~(VOICE_HAS_HRTF | VOICE_HAS_NFC); + if(isbformat) + { + /* Special handling for B-Format sources. */ + + if(Distance > std::numeric_limits<float>::epsilon()) + { + /* Panning a B-Format sound toward some direction is easy. Just pan + * the first (W) channel as a normal mono sound and silence the + * others. + */ + + if(Device->AvgSpeakerDist > 0.0f) + { + /* Clamp the distance for really close sources, to prevent + * excessive bass. + */ + const ALfloat mdist{maxf(Distance, Device->AvgSpeakerDist/4.0f)}; + const ALfloat w0{SPEEDOFSOUNDMETRESPERSEC / (mdist * Frequency)}; + + /* Only need to adjust the first channel of a B-Format source. */ + voice->mChans[0].mDryParams.NFCtrlFilter.adjust(w0); + + voice->mFlags |= VOICE_HAS_NFC; + } + + ALfloat coeffs[MAX_AMBI_CHANNELS]; + if(Device->mRenderMode != StereoPair) + CalcDirectionCoeffs({xpos, ypos, zpos}, Spread, coeffs); + else + { + /* Clamp Y, in case rounding errors caused it to end up outside + * of -1...+1. + */ + const ALfloat ev{std::asin(clampf(ypos, -1.0f, 1.0f))}; + /* Negate Z for right-handed coords with -Z in front. */ + const ALfloat az{std::atan2(xpos, -zpos)}; + + /* A scalar of 1.5 for plain stereo results in +/-60 degrees + * being moved to +/-90 degrees for direct right and left + * speaker responses. + */ + CalcAngleCoeffs(ScaleAzimuthFront(az, 1.5f), ev, Spread, coeffs); + } + + /* NOTE: W needs to be scaled due to FuMa normalization. */ + const ALfloat &scale0 = AmbiScale::FromFuMa[0]; + ComputePanGains(&Device->Dry, coeffs, DryGain*scale0, + voice->mChans[0].mDryParams.Gains.Target); + for(ALsizei i{0};i < NumSends;i++) + { + if(const ALeffectslot *Slot{SendSlots[i]}) + ComputePanGains(&Slot->Wet, coeffs, WetGain[i]*scale0, + voice->mChans[0].mWetParams[i].Gains.Target); + } + } + else + { + if(Device->AvgSpeakerDist > 0.0f) + { + /* NOTE: The NFCtrlFilters were created with a w0 of 0, which + * is what we want for FOA input. The first channel may have + * been previously re-adjusted if panned, so reset it. + */ + voice->mChans[0].mDryParams.NFCtrlFilter.adjust(0.0f); + + voice->mFlags |= VOICE_HAS_NFC; + } + + /* Local B-Format sources have their XYZ channels rotated according + * to the orientation. + */ + /* AT then UP */ + alu::Vector N{props->OrientAt[0], props->OrientAt[1], props->OrientAt[2], 0.0f}; + N.normalize(); + alu::Vector V{props->OrientUp[0], props->OrientUp[1], props->OrientUp[2], 0.0f}; + V.normalize(); + if(!props->HeadRelative) + { + N = Listener.Params.Matrix * N; + V = Listener.Params.Matrix * V; + } + /* Build and normalize right-vector */ + alu::Vector U{aluCrossproduct(N, V)}; + U.normalize(); + + /* Build a rotate + conversion matrix (FuMa -> ACN+N3D). NOTE: This + * matrix is transposed, for the inputs to align on the rows and + * outputs on the columns. + */ + const ALfloat &wscale = AmbiScale::FromFuMa[0]; + const ALfloat &yscale = AmbiScale::FromFuMa[1]; + const ALfloat &zscale = AmbiScale::FromFuMa[2]; + const ALfloat &xscale = AmbiScale::FromFuMa[3]; + const ALfloat matrix[4][MAX_AMBI_CHANNELS]{ + // ACN0 ACN1 ACN2 ACN3 + { wscale, 0.0f, 0.0f, 0.0f }, // FuMa W + { 0.0f, -N[0]*xscale, N[1]*xscale, -N[2]*xscale }, // FuMa X + { 0.0f, U[0]*yscale, -U[1]*yscale, U[2]*yscale }, // FuMa Y + { 0.0f, -V[0]*zscale, V[1]*zscale, -V[2]*zscale } // FuMa Z + }; + + for(ALsizei c{0};c < num_channels;c++) + { + ComputePanGains(&Device->Dry, matrix[c], DryGain, + voice->mChans[c].mDryParams.Gains.Target); + + for(ALsizei i{0};i < NumSends;i++) + { + if(const ALeffectslot *Slot{SendSlots[i]}) + ComputePanGains(&Slot->Wet, matrix[c], WetGain[i], + voice->mChans[c].mWetParams[i].Gains.Target); + } + } + } + } + else if(DirectChannels) + { + /* Direct source channels always play local. Skip the virtual channels + * and write inputs to the matching real outputs. + */ + voice->mDirect.Buffer = Device->RealOut.Buffer; + + for(ALsizei c{0};c < num_channels;c++) + { + int idx{GetChannelIdxByName(Device->RealOut, chans[c].channel)}; + if(idx != -1) voice->mChans[c].mDryParams.Gains.Target[idx] = DryGain; + } + + /* Auxiliary sends still use normal channel panning since they mix to + * B-Format, which can't channel-match. + */ + for(ALsizei c{0};c < num_channels;c++) + { + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcAngleCoeffs(chans[c].angle, chans[c].elevation, 0.0f, coeffs); + + for(ALsizei i{0};i < NumSends;i++) + { + if(const ALeffectslot *Slot{SendSlots[i]}) + ComputePanGains(&Slot->Wet, coeffs, WetGain[i], + voice->mChans[c].mWetParams[i].Gains.Target); + } + } + } + else if(Device->mRenderMode == HrtfRender) + { + /* Full HRTF rendering. Skip the virtual channels and render to the + * real outputs. + */ + voice->mDirect.Buffer = Device->RealOut.Buffer; + + if(Distance > std::numeric_limits<float>::epsilon()) + { + const ALfloat ev{std::asin(clampf(ypos, -1.0f, 1.0f))}; + const ALfloat az{std::atan2(xpos, -zpos)}; + + /* Get the HRIR coefficients and delays just once, for the given + * source direction. + */ + GetHrtfCoeffs(Device->mHrtf, ev, az, Distance, Spread, + voice->mChans[0].mDryParams.Hrtf.Target.Coeffs, + voice->mChans[0].mDryParams.Hrtf.Target.Delay); + voice->mChans[0].mDryParams.Hrtf.Target.Gain = DryGain * downmix_gain; + + /* Remaining channels use the same results as the first. */ + for(ALsizei c{1};c < num_channels;c++) + { + /* Skip LFE */ + if(chans[c].channel == LFE) continue; + voice->mChans[c].mDryParams.Hrtf.Target = voice->mChans[0].mDryParams.Hrtf.Target; + } + + /* Calculate the directional coefficients once, which apply to all + * input channels of the source sends. + */ + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcDirectionCoeffs({xpos, ypos, zpos}, Spread, coeffs); + + for(ALsizei c{0};c < num_channels;c++) + { + /* Skip LFE */ + if(chans[c].channel == LFE) + continue; + for(ALsizei i{0};i < NumSends;i++) + { + if(const ALeffectslot *Slot{SendSlots[i]}) + ComputePanGains(&Slot->Wet, coeffs, WetGain[i] * downmix_gain, + voice->mChans[c].mWetParams[i].Gains.Target); + } + } + } + else + { + /* Local sources on HRTF play with each channel panned to its + * relative location around the listener, providing "virtual + * speaker" responses. + */ + for(ALsizei c{0};c < num_channels;c++) + { + /* Skip LFE */ + if(chans[c].channel == LFE) + continue; + + /* Get the HRIR coefficients and delays for this channel + * position. + */ + GetHrtfCoeffs(Device->mHrtf, chans[c].elevation, chans[c].angle, + std::numeric_limits<float>::infinity(), Spread, + voice->mChans[c].mDryParams.Hrtf.Target.Coeffs, + voice->mChans[c].mDryParams.Hrtf.Target.Delay); + voice->mChans[c].mDryParams.Hrtf.Target.Gain = DryGain; + + /* Normal panning for auxiliary sends. */ + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcAngleCoeffs(chans[c].angle, chans[c].elevation, Spread, coeffs); + + for(ALsizei i{0};i < NumSends;i++) + { + if(const ALeffectslot *Slot{SendSlots[i]}) + ComputePanGains(&Slot->Wet, coeffs, WetGain[i], + voice->mChans[c].mWetParams[i].Gains.Target); + } + } + } + + voice->mFlags |= VOICE_HAS_HRTF; + } + else + { + /* Non-HRTF rendering. Use normal panning to the output. */ + + if(Distance > std::numeric_limits<float>::epsilon()) + { + /* Calculate NFC filter coefficient if needed. */ + if(Device->AvgSpeakerDist > 0.0f) + { + /* Clamp the distance for really close sources, to prevent + * excessive bass. + */ + const ALfloat mdist{maxf(Distance, Device->AvgSpeakerDist/4.0f)}; + const ALfloat w0{SPEEDOFSOUNDMETRESPERSEC / (mdist * Frequency)}; + + /* Adjust NFC filters. */ + for(ALsizei c{0};c < num_channels;c++) + voice->mChans[c].mDryParams.NFCtrlFilter.adjust(w0); + + voice->mFlags |= VOICE_HAS_NFC; + } + + /* Calculate the directional coefficients once, which apply to all + * input channels. + */ + ALfloat coeffs[MAX_AMBI_CHANNELS]; + if(Device->mRenderMode != StereoPair) + CalcDirectionCoeffs({xpos, ypos, zpos}, Spread, coeffs); + else + { + const ALfloat ev{std::asin(clampf(ypos, -1.0f, 1.0f))}; + const ALfloat az{std::atan2(xpos, -zpos)}; + CalcAngleCoeffs(ScaleAzimuthFront(az, 1.5f), ev, Spread, coeffs); + } + + for(ALsizei c{0};c < num_channels;c++) + { + /* Special-case LFE */ + if(chans[c].channel == LFE) + { + if(Device->Dry.Buffer.data() == Device->RealOut.Buffer.data()) + { + int idx = GetChannelIdxByName(Device->RealOut, chans[c].channel); + if(idx != -1) voice->mChans[c].mDryParams.Gains.Target[idx] = DryGain; + } + continue; + } + + ComputePanGains(&Device->Dry, coeffs, DryGain * downmix_gain, + voice->mChans[c].mDryParams.Gains.Target); + } + + for(ALsizei c{0};c < num_channels;c++) + { + /* Skip LFE */ + if(chans[c].channel == LFE) + continue; + for(ALsizei i{0};i < NumSends;i++) + { + if(const ALeffectslot *Slot{SendSlots[i]}) + ComputePanGains(&Slot->Wet, coeffs, WetGain[i] * downmix_gain, + voice->mChans[c].mWetParams[i].Gains.Target); + } + } + } + else + { + if(Device->AvgSpeakerDist > 0.0f) + { + /* If the source distance is 0, set w0 to w1 to act as a pass- + * through. We still want to pass the signal through the + * filters so they keep an appropriate history, in case the + * source moves away from the listener. + */ + const ALfloat w0{SPEEDOFSOUNDMETRESPERSEC / (Device->AvgSpeakerDist * Frequency)}; + + for(ALsizei c{0};c < num_channels;c++) + voice->mChans[c].mDryParams.NFCtrlFilter.adjust(w0); + + voice->mFlags |= VOICE_HAS_NFC; + } + + for(ALsizei c{0};c < num_channels;c++) + { + /* Special-case LFE */ + if(chans[c].channel == LFE) + { + if(Device->Dry.Buffer.data() == Device->RealOut.Buffer.data()) + { + int idx = GetChannelIdxByName(Device->RealOut, chans[c].channel); + if(idx != -1) voice->mChans[c].mDryParams.Gains.Target[idx] = DryGain; + } + continue; + } + + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcAngleCoeffs( + (Device->mRenderMode==StereoPair) ? ScaleAzimuthFront(chans[c].angle, 3.0f) + : chans[c].angle, + chans[c].elevation, Spread, coeffs + ); + + ComputePanGains(&Device->Dry, coeffs, DryGain, + voice->mChans[c].mDryParams.Gains.Target); + for(ALsizei i{0};i < NumSends;i++) + { + if(const ALeffectslot *Slot{SendSlots[i]}) + ComputePanGains(&Slot->Wet, coeffs, WetGain[i], + voice->mChans[c].mWetParams[i].Gains.Target); + } + } + } + } + + { + const ALfloat hfScale{props->Direct.HFReference / Frequency}; + const ALfloat lfScale{props->Direct.LFReference / Frequency}; + const ALfloat gainHF{maxf(DryGainHF, 0.001f)}; /* Limit -60dB */ + const ALfloat gainLF{maxf(DryGainLF, 0.001f)}; + + voice->mDirect.FilterType = AF_None; + if(gainHF != 1.0f) voice->mDirect.FilterType |= AF_LowPass; + if(gainLF != 1.0f) voice->mDirect.FilterType |= AF_HighPass; + auto &lowpass = voice->mChans[0].mDryParams.LowPass; + auto &highpass = voice->mChans[0].mDryParams.HighPass; + lowpass.setParams(BiquadType::HighShelf, gainHF, hfScale, + lowpass.rcpQFromSlope(gainHF, 1.0f)); + highpass.setParams(BiquadType::LowShelf, gainLF, lfScale, + highpass.rcpQFromSlope(gainLF, 1.0f)); + for(ALsizei c{1};c < num_channels;c++) + { + voice->mChans[c].mDryParams.LowPass.copyParamsFrom(lowpass); + voice->mChans[c].mDryParams.HighPass.copyParamsFrom(highpass); + } + } + for(ALsizei i{0};i < NumSends;i++) + { + const ALfloat hfScale{props->Send[i].HFReference / Frequency}; + const ALfloat lfScale{props->Send[i].LFReference / Frequency}; + const ALfloat gainHF{maxf(WetGainHF[i], 0.001f)}; + const ALfloat gainLF{maxf(WetGainLF[i], 0.001f)}; + + voice->mSend[i].FilterType = AF_None; + if(gainHF != 1.0f) voice->mSend[i].FilterType |= AF_LowPass; + if(gainLF != 1.0f) voice->mSend[i].FilterType |= AF_HighPass; + + auto &lowpass = voice->mChans[0].mWetParams[i].LowPass; + auto &highpass = voice->mChans[0].mWetParams[i].HighPass; + lowpass.setParams(BiquadType::HighShelf, gainHF, hfScale, + lowpass.rcpQFromSlope(gainHF, 1.0f)); + highpass.setParams(BiquadType::LowShelf, gainLF, lfScale, + highpass.rcpQFromSlope(gainLF, 1.0f)); + for(ALsizei c{1};c < num_channels;c++) + { + voice->mChans[c].mWetParams[i].LowPass.copyParamsFrom(lowpass); + voice->mChans[c].mWetParams[i].HighPass.copyParamsFrom(highpass); + } + } +} + +void CalcNonAttnSourceParams(ALvoice *voice, const ALvoicePropsBase *props, const ALCcontext *ALContext) +{ + const ALCdevice *Device{ALContext->Device}; + ALeffectslot *SendSlots[MAX_SENDS]; + + voice->mDirect.Buffer = Device->Dry.Buffer; + for(ALsizei i{0};i < Device->NumAuxSends;i++) + { + SendSlots[i] = props->Send[i].Slot; + if(!SendSlots[i] && i == 0) + SendSlots[i] = ALContext->DefaultSlot.get(); + if(!SendSlots[i] || SendSlots[i]->Params.EffectType == AL_EFFECT_NULL) + { + SendSlots[i] = nullptr; + voice->mSend[i].Buffer = {}; + } + else + voice->mSend[i].Buffer = SendSlots[i]->Wet.Buffer; + } + + /* Calculate the stepping value */ + const auto Pitch = static_cast<ALfloat>(voice->mFrequency) / + static_cast<ALfloat>(Device->Frequency) * props->Pitch; + if(Pitch > static_cast<ALfloat>(MAX_PITCH)) + voice->mStep = MAX_PITCH<<FRACTIONBITS; + else + voice->mStep = maxi(fastf2i(Pitch * FRACTIONONE), 1); + if(props->mResampler == BSinc24Resampler) + BsincPrepare(voice->mStep, &voice->mResampleState.bsinc, &bsinc24); + else if(props->mResampler == BSinc12Resampler) + BsincPrepare(voice->mStep, &voice->mResampleState.bsinc, &bsinc12); + voice->mResampler = SelectResampler(props->mResampler); + + /* Calculate gains */ + const ALlistener &Listener = ALContext->Listener; + ALfloat DryGain{clampf(props->Gain, props->MinGain, props->MaxGain)}; + DryGain *= props->Direct.Gain * Listener.Params.Gain; + DryGain = minf(DryGain, GAIN_MIX_MAX); + ALfloat DryGainHF{props->Direct.GainHF}; + ALfloat DryGainLF{props->Direct.GainLF}; + ALfloat WetGain[MAX_SENDS], WetGainHF[MAX_SENDS], WetGainLF[MAX_SENDS]; + for(ALsizei i{0};i < Device->NumAuxSends;i++) + { + WetGain[i] = clampf(props->Gain, props->MinGain, props->MaxGain); + WetGain[i] *= props->Send[i].Gain * Listener.Params.Gain; + WetGain[i] = minf(WetGain[i], GAIN_MIX_MAX); + WetGainHF[i] = props->Send[i].GainHF; + WetGainLF[i] = props->Send[i].GainLF; + } + + CalcPanningAndFilters(voice, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, DryGain, DryGainHF, DryGainLF, + WetGain, WetGainLF, WetGainHF, SendSlots, props, Listener, Device); +} + +void CalcAttnSourceParams(ALvoice *voice, const ALvoicePropsBase *props, const ALCcontext *ALContext) +{ + const ALCdevice *Device{ALContext->Device}; + const ALsizei NumSends{Device->NumAuxSends}; + const ALlistener &Listener = ALContext->Listener; + + /* Set mixing buffers and get send parameters. */ + voice->mDirect.Buffer = Device->Dry.Buffer; + ALeffectslot *SendSlots[MAX_SENDS]; + ALfloat RoomRolloff[MAX_SENDS]; + ALfloat DecayDistance[MAX_SENDS]; + ALfloat DecayLFDistance[MAX_SENDS]; + ALfloat DecayHFDistance[MAX_SENDS]; + for(ALsizei i{0};i < NumSends;i++) + { + SendSlots[i] = props->Send[i].Slot; + if(!SendSlots[i] && i == 0) + SendSlots[i] = ALContext->DefaultSlot.get(); + if(!SendSlots[i] || SendSlots[i]->Params.EffectType == AL_EFFECT_NULL) + { + SendSlots[i] = nullptr; + RoomRolloff[i] = 0.0f; + DecayDistance[i] = 0.0f; + DecayLFDistance[i] = 0.0f; + DecayHFDistance[i] = 0.0f; + } + else if(SendSlots[i]->Params.AuxSendAuto) + { + RoomRolloff[i] = SendSlots[i]->Params.RoomRolloff + props->RoomRolloffFactor; + /* Calculate the distances to where this effect's decay reaches + * -60dB. + */ + DecayDistance[i] = SendSlots[i]->Params.DecayTime * + Listener.Params.ReverbSpeedOfSound; + DecayLFDistance[i] = DecayDistance[i] * SendSlots[i]->Params.DecayLFRatio; + DecayHFDistance[i] = DecayDistance[i] * SendSlots[i]->Params.DecayHFRatio; + if(SendSlots[i]->Params.DecayHFLimit) + { + ALfloat airAbsorption{SendSlots[i]->Params.AirAbsorptionGainHF}; + if(airAbsorption < 1.0f) + { + /* Calculate the distance to where this effect's air + * absorption reaches -60dB, and limit the effect's HF + * decay distance (so it doesn't take any longer to decay + * than the air would allow). + */ + ALfloat absorb_dist{std::log10(REVERB_DECAY_GAIN) / std::log10(airAbsorption)}; + DecayHFDistance[i] = minf(absorb_dist, DecayHFDistance[i]); + } + } + } + else + { + /* If the slot's auxiliary send auto is off, the data sent to the + * effect slot is the same as the dry path, sans filter effects */ + RoomRolloff[i] = props->RolloffFactor; + DecayDistance[i] = 0.0f; + DecayLFDistance[i] = 0.0f; + DecayHFDistance[i] = 0.0f; + } + + if(!SendSlots[i]) + voice->mSend[i].Buffer = {}; + else + voice->mSend[i].Buffer = SendSlots[i]->Wet.Buffer; + } + + /* Transform source to listener space (convert to head relative) */ + alu::Vector Position{props->Position[0], props->Position[1], props->Position[2], 1.0f}; + alu::Vector Velocity{props->Velocity[0], props->Velocity[1], props->Velocity[2], 0.0f}; + alu::Vector Direction{props->Direction[0], props->Direction[1], props->Direction[2], 0.0f}; + if(props->HeadRelative == AL_FALSE) + { + /* Transform source vectors */ + Position = Listener.Params.Matrix * Position; + Velocity = Listener.Params.Matrix * Velocity; + Direction = Listener.Params.Matrix * Direction; + } + else + { + /* Offset the source velocity to be relative of the listener velocity */ + Velocity += Listener.Params.Velocity; + } + + const bool directional{Direction.normalize() > 0.0f}; + alu::Vector ToSource{Position[0], Position[1], Position[2], 0.0f}; + const ALfloat Distance{ToSource.normalize()}; + + /* Initial source gain */ + ALfloat DryGain{props->Gain}; + ALfloat DryGainHF{1.0f}; + ALfloat DryGainLF{1.0f}; + ALfloat WetGain[MAX_SENDS], WetGainHF[MAX_SENDS], WetGainLF[MAX_SENDS]; + for(ALsizei i{0};i < NumSends;i++) + { + WetGain[i] = props->Gain; + WetGainHF[i] = 1.0f; + WetGainLF[i] = 1.0f; + } + + /* Calculate distance attenuation */ + ALfloat ClampedDist{Distance}; + + switch(Listener.Params.SourceDistanceModel ? + props->mDistanceModel : Listener.Params.mDistanceModel) + { + case DistanceModel::InverseClamped: + ClampedDist = clampf(ClampedDist, props->RefDistance, props->MaxDistance); + if(props->MaxDistance < props->RefDistance) break; + /*fall-through*/ + case DistanceModel::Inverse: + if(!(props->RefDistance > 0.0f)) + ClampedDist = props->RefDistance; + else + { + ALfloat dist = lerp(props->RefDistance, ClampedDist, props->RolloffFactor); + if(dist > 0.0f) DryGain *= props->RefDistance / dist; + for(ALsizei i{0};i < NumSends;i++) + { + dist = lerp(props->RefDistance, ClampedDist, RoomRolloff[i]); + if(dist > 0.0f) WetGain[i] *= props->RefDistance / dist; + } + } + break; + + case DistanceModel::LinearClamped: + ClampedDist = clampf(ClampedDist, props->RefDistance, props->MaxDistance); + if(props->MaxDistance < props->RefDistance) break; + /*fall-through*/ + case DistanceModel::Linear: + if(!(props->MaxDistance != props->RefDistance)) + ClampedDist = props->RefDistance; + else + { + ALfloat attn = props->RolloffFactor * (ClampedDist-props->RefDistance) / + (props->MaxDistance-props->RefDistance); + DryGain *= maxf(1.0f - attn, 0.0f); + for(ALsizei i{0};i < NumSends;i++) + { + attn = RoomRolloff[i] * (ClampedDist-props->RefDistance) / + (props->MaxDistance-props->RefDistance); + WetGain[i] *= maxf(1.0f - attn, 0.0f); + } + } + break; + + case DistanceModel::ExponentClamped: + ClampedDist = clampf(ClampedDist, props->RefDistance, props->MaxDistance); + if(props->MaxDistance < props->RefDistance) break; + /*fall-through*/ + case DistanceModel::Exponent: + if(!(ClampedDist > 0.0f && props->RefDistance > 0.0f)) + ClampedDist = props->RefDistance; + else + { + DryGain *= std::pow(ClampedDist/props->RefDistance, -props->RolloffFactor); + for(ALsizei i{0};i < NumSends;i++) + WetGain[i] *= std::pow(ClampedDist/props->RefDistance, -RoomRolloff[i]); + } + break; + + case DistanceModel::Disable: + ClampedDist = props->RefDistance; + break; + } + + /* Calculate directional soundcones */ + if(directional && props->InnerAngle < 360.0f) + { + const ALfloat Angle{Rad2Deg(std::acos(-aluDotproduct(Direction, ToSource)) * + ConeScale * 2.0f)}; + + ALfloat ConeVolume, ConeHF; + if(!(Angle > props->InnerAngle)) + { + ConeVolume = 1.0f; + ConeHF = 1.0f; + } + else if(Angle < props->OuterAngle) + { + ALfloat scale = ( Angle-props->InnerAngle) / + (props->OuterAngle-props->InnerAngle); + ConeVolume = lerp(1.0f, props->OuterGain, scale); + ConeHF = lerp(1.0f, props->OuterGainHF, scale); + } + else + { + ConeVolume = props->OuterGain; + ConeHF = props->OuterGainHF; + } + + DryGain *= ConeVolume; + if(props->DryGainHFAuto) + DryGainHF *= ConeHF; + if(props->WetGainAuto) + std::transform(std::begin(WetGain), std::begin(WetGain)+NumSends, std::begin(WetGain), + [ConeVolume](ALfloat gain) noexcept -> ALfloat { return gain * ConeVolume; } + ); + if(props->WetGainHFAuto) + std::transform(std::begin(WetGainHF), std::begin(WetGainHF)+NumSends, + std::begin(WetGainHF), + [ConeHF](ALfloat gain) noexcept -> ALfloat { return gain * ConeHF; } + ); + } + + /* Apply gain and frequency filters */ + DryGain = clampf(DryGain, props->MinGain, props->MaxGain); + DryGain = minf(DryGain*props->Direct.Gain*Listener.Params.Gain, GAIN_MIX_MAX); + DryGainHF *= props->Direct.GainHF; + DryGainLF *= props->Direct.GainLF; + for(ALsizei i{0};i < NumSends;i++) + { + WetGain[i] = clampf(WetGain[i], props->MinGain, props->MaxGain); + WetGain[i] = minf(WetGain[i]*props->Send[i].Gain*Listener.Params.Gain, GAIN_MIX_MAX); + WetGainHF[i] *= props->Send[i].GainHF; + WetGainLF[i] *= props->Send[i].GainLF; + } + + /* Distance-based air absorption and initial send decay. */ + if(ClampedDist > props->RefDistance && props->RolloffFactor > 0.0f) + { + ALfloat meters_base{(ClampedDist-props->RefDistance) * props->RolloffFactor * + Listener.Params.MetersPerUnit}; + if(props->AirAbsorptionFactor > 0.0f) + { + ALfloat hfattn{std::pow(AIRABSORBGAINHF, meters_base * props->AirAbsorptionFactor)}; + DryGainHF *= hfattn; + std::transform(std::begin(WetGainHF), std::begin(WetGainHF)+NumSends, + std::begin(WetGainHF), + [hfattn](ALfloat gain) noexcept -> ALfloat { return gain * hfattn; } + ); + } + + if(props->WetGainAuto) + { + /* Apply a decay-time transformation to the wet path, based on the + * source distance in meters. The initial decay of the reverb + * effect is calculated and applied to the wet path. + */ + for(ALsizei i{0};i < NumSends;i++) + { + if(!(DecayDistance[i] > 0.0f)) + continue; + + const ALfloat gain{std::pow(REVERB_DECAY_GAIN, meters_base/DecayDistance[i])}; + WetGain[i] *= gain; + /* Yes, the wet path's air absorption is applied with + * WetGainAuto on, rather than WetGainHFAuto. + */ + if(gain > 0.0f) + { + ALfloat gainhf{std::pow(REVERB_DECAY_GAIN, meters_base/DecayHFDistance[i])}; + WetGainHF[i] *= minf(gainhf / gain, 1.0f); + ALfloat gainlf{std::pow(REVERB_DECAY_GAIN, meters_base/DecayLFDistance[i])}; + WetGainLF[i] *= minf(gainlf / gain, 1.0f); + } + } + } + } + + + /* Initial source pitch */ + ALfloat Pitch{props->Pitch}; + + /* Calculate velocity-based doppler effect */ + ALfloat DopplerFactor{props->DopplerFactor * Listener.Params.DopplerFactor}; + if(DopplerFactor > 0.0f) + { + const alu::Vector &lvelocity = Listener.Params.Velocity; + ALfloat vss{aluDotproduct(Velocity, ToSource) * -DopplerFactor}; + ALfloat vls{aluDotproduct(lvelocity, ToSource) * -DopplerFactor}; + + const ALfloat SpeedOfSound{Listener.Params.SpeedOfSound}; + if(!(vls < SpeedOfSound)) + { + /* Listener moving away from the source at the speed of sound. + * Sound waves can't catch it. + */ + Pitch = 0.0f; + } + else if(!(vss < SpeedOfSound)) + { + /* Source moving toward the listener at the speed of sound. Sound + * waves bunch up to extreme frequencies. + */ + Pitch = std::numeric_limits<float>::infinity(); + } + else + { + /* Source and listener movement is nominal. Calculate the proper + * doppler shift. + */ + Pitch *= (SpeedOfSound-vls) / (SpeedOfSound-vss); + } + } + + /* Adjust pitch based on the buffer and output frequencies, and calculate + * fixed-point stepping value. + */ + Pitch *= static_cast<ALfloat>(voice->mFrequency)/static_cast<ALfloat>(Device->Frequency); + if(Pitch > static_cast<ALfloat>(MAX_PITCH)) + voice->mStep = MAX_PITCH<<FRACTIONBITS; + else + voice->mStep = maxi(fastf2i(Pitch * FRACTIONONE), 1); + if(props->mResampler == BSinc24Resampler) + BsincPrepare(voice->mStep, &voice->mResampleState.bsinc, &bsinc24); + else if(props->mResampler == BSinc12Resampler) + BsincPrepare(voice->mStep, &voice->mResampleState.bsinc, &bsinc12); + voice->mResampler = SelectResampler(props->mResampler); + + ALfloat spread{0.0f}; + if(props->Radius > Distance) + spread = al::MathDefs<float>::Tau() - Distance/props->Radius*al::MathDefs<float>::Pi(); + else if(Distance > 0.0f) + spread = std::asin(props->Radius/Distance) * 2.0f; + + CalcPanningAndFilters(voice, ToSource[0], ToSource[1], ToSource[2]*ZScale, + Distance*Listener.Params.MetersPerUnit, spread, DryGain, DryGainHF, DryGainLF, WetGain, + WetGainLF, WetGainHF, SendSlots, props, Listener, Device); +} + +void CalcSourceParams(ALvoice *voice, ALCcontext *context, bool force) +{ + ALvoiceProps *props{voice->mUpdate.exchange(nullptr, std::memory_order_acq_rel)}; + if(!props && !force) return; + + if(props) + { + voice->mProps = *props; + + AtomicReplaceHead(context->FreeVoiceProps, props); + } + + if((voice->mProps.mSpatializeMode == SpatializeAuto && voice->mFmtChannels == FmtMono) || + voice->mProps.mSpatializeMode == SpatializeOn) + CalcAttnSourceParams(voice, &voice->mProps, context); + else + CalcNonAttnSourceParams(voice, &voice->mProps, context); +} + + +void ProcessParamUpdates(ALCcontext *ctx, const ALeffectslotArray *slots) +{ + IncrementRef(&ctx->UpdateCount); + if(LIKELY(!ctx->HoldUpdates.load(std::memory_order_acquire))) + { + bool cforce{CalcContextParams(ctx)}; + bool force{CalcListenerParams(ctx) || cforce}; + force = std::accumulate(slots->begin(), slots->end(), force, + [ctx,cforce](bool force, ALeffectslot *slot) -> bool + { return CalcEffectSlotParams(slot, ctx, cforce) | force; } + ); + + std::for_each(ctx->Voices->begin(), + ctx->Voices->begin() + ctx->VoiceCount.load(std::memory_order_acquire), + [ctx,force](ALvoice &voice) -> void + { + ALuint sid{voice.mSourceID.load(std::memory_order_acquire)}; + if(sid) CalcSourceParams(&voice, ctx, force); + } + ); + } + IncrementRef(&ctx->UpdateCount); +} + +void ProcessContext(ALCcontext *ctx, const ALsizei SamplesToDo) +{ + ASSUME(SamplesToDo > 0); + + const ALeffectslotArray *auxslots{ctx->ActiveAuxSlots.load(std::memory_order_acquire)}; + + /* Process pending propery updates for objects on the context. */ + ProcessParamUpdates(ctx, auxslots); + + /* Clear auxiliary effect slot mixing buffers. */ + std::for_each(auxslots->begin(), auxslots->end(), + [SamplesToDo](ALeffectslot *slot) -> void + { + for(auto &buffer : slot->MixBuffer) + std::fill_n(buffer.begin(), SamplesToDo, 0.0f); + } + ); + + /* Process voices that have a playing source. */ + std::for_each(ctx->Voices->begin(), + ctx->Voices->begin() + ctx->VoiceCount.load(std::memory_order_acquire), + [SamplesToDo,ctx](ALvoice &voice) -> void + { + const ALvoice::State vstate{voice.mPlayState.load(std::memory_order_acquire)}; + if(vstate == ALvoice::Stopped) return; + const ALuint sid{voice.mSourceID.load(std::memory_order_relaxed)}; + if(voice.mStep < 1) return; + + MixVoice(&voice, vstate, sid, ctx, SamplesToDo); + } + ); + + /* Process effects. */ + if(auxslots->size() < 1) return; + auto slots = auxslots->data(); + auto slots_end = slots + auxslots->size(); + + /* First sort the slots into scratch storage, so that effects come before + * their effect target (or their targets' target). + */ + auto sorted_slots = const_cast<ALeffectslot**>(slots_end); + auto sorted_slots_end = sorted_slots; + auto in_chain = [](const ALeffectslot *slot1, const ALeffectslot *slot2) noexcept -> bool + { + while((slot1=slot1->Params.Target) != nullptr) { + if(slot1 == slot2) return true; + } + return false; + }; + + *sorted_slots_end = *slots; + ++sorted_slots_end; + while(++slots != slots_end) + { + /* If this effect slot targets an effect slot already in the list (i.e. + * slots outputs to something in sorted_slots), directly or indirectly, + * insert it prior to that element. + */ + auto checker = sorted_slots; + do { + if(in_chain(*slots, *checker)) break; + } while(++checker != sorted_slots_end); + + checker = std::move_backward(checker, sorted_slots_end, sorted_slots_end+1); + *--checker = *slots; + ++sorted_slots_end; + } + + std::for_each(sorted_slots, sorted_slots_end, + [SamplesToDo](const ALeffectslot *slot) -> void + { + EffectState *state{slot->Params.mEffectState}; + state->process(SamplesToDo, slot->Wet.Buffer.data(), + static_cast<ALsizei>(slot->Wet.Buffer.size()), state->mOutTarget); + } + ); +} + + +void ApplyStablizer(FrontStablizer *Stablizer, const al::span<FloatBufferLine> Buffer, + const ALuint lidx, const ALuint ridx, const ALuint cidx, const ALsizei SamplesToDo) +{ + ASSUME(SamplesToDo > 0); + + /* Apply a delay to all channels, except the front-left and front-right, so + * they maintain correct timing. + */ + const size_t NumChannels{Buffer.size()}; + for(size_t i{0u};i < NumChannels;i++) + { + if(i == lidx || i == ridx) + continue; + + auto &DelayBuf = Stablizer->DelayBuf[i]; + auto buffer_end = Buffer[i].begin() + SamplesToDo; + if(LIKELY(SamplesToDo >= ALsizei{FrontStablizer::DelayLength})) + { + auto delay_end = std::rotate(Buffer[i].begin(), + buffer_end - FrontStablizer::DelayLength, buffer_end); + std::swap_ranges(Buffer[i].begin(), delay_end, std::begin(DelayBuf)); + } + else + { + auto delay_start = std::swap_ranges(Buffer[i].begin(), buffer_end, + std::begin(DelayBuf)); + std::rotate(std::begin(DelayBuf), delay_start, std::end(DelayBuf)); + } + } + + ALfloat (&lsplit)[2][BUFFERSIZE] = Stablizer->LSplit; + ALfloat (&rsplit)[2][BUFFERSIZE] = Stablizer->RSplit; + auto &tmpbuf = Stablizer->TempBuf; + + /* This applies the band-splitter, preserving phase at the cost of some + * delay. The shorter the delay, the more error seeps into the result. + */ + auto apply_splitter = [&tmpbuf,SamplesToDo](const FloatBufferLine &Buffer, + ALfloat (&DelayBuf)[FrontStablizer::DelayLength], BandSplitter &Filter, + ALfloat (&splitbuf)[2][BUFFERSIZE]) -> void + { + /* Combine the delayed samples and the input samples into the temp + * buffer, in reverse. Then copy the final samples back into the delay + * buffer for next time. Note that the delay buffer's samples are + * stored backwards here. + */ + auto tmpbuf_end = std::begin(tmpbuf) + SamplesToDo; + std::copy_n(std::begin(DelayBuf), FrontStablizer::DelayLength, tmpbuf_end); + std::reverse_copy(Buffer.begin(), Buffer.begin()+SamplesToDo, std::begin(tmpbuf)); + std::copy_n(std::begin(tmpbuf), FrontStablizer::DelayLength, std::begin(DelayBuf)); + + /* Apply an all-pass on the reversed signal, then reverse the samples + * to get the forward signal with a reversed phase shift. + */ + Filter.applyAllpass(tmpbuf, SamplesToDo+FrontStablizer::DelayLength); + std::reverse(std::begin(tmpbuf), tmpbuf_end+FrontStablizer::DelayLength); + + /* Now apply the band-splitter, combining its phase shift with the + * reversed phase shift, restoring the original phase on the split + * signal. + */ + Filter.process(splitbuf[1], splitbuf[0], tmpbuf, SamplesToDo); + }; + apply_splitter(Buffer[lidx], Stablizer->DelayBuf[lidx], Stablizer->LFilter, lsplit); + apply_splitter(Buffer[ridx], Stablizer->DelayBuf[ridx], Stablizer->RFilter, rsplit); + + for(ALsizei i{0};i < SamplesToDo;i++) + { + ALfloat lfsum{lsplit[0][i] + rsplit[0][i]}; + ALfloat hfsum{lsplit[1][i] + rsplit[1][i]}; + ALfloat s{lsplit[0][i] + lsplit[1][i] - rsplit[0][i] - rsplit[1][i]}; + + /* This pans the separate low- and high-frequency sums between being on + * the center channel and the left/right channels. The low-frequency + * sum is 1/3rd toward center (2/3rds on left/right) and the high- + * frequency sum is 1/4th toward center (3/4ths on left/right). These + * values can be tweaked. + */ + ALfloat m{lfsum*std::cos(1.0f/3.0f * (al::MathDefs<float>::Pi()*0.5f)) + + hfsum*std::cos(1.0f/4.0f * (al::MathDefs<float>::Pi()*0.5f))}; + ALfloat c{lfsum*std::sin(1.0f/3.0f * (al::MathDefs<float>::Pi()*0.5f)) + + hfsum*std::sin(1.0f/4.0f * (al::MathDefs<float>::Pi()*0.5f))}; + + /* The generated center channel signal adds to the existing signal, + * while the modified left and right channels replace. + */ + Buffer[lidx][i] = (m + s) * 0.5f; + Buffer[ridx][i] = (m - s) * 0.5f; + Buffer[cidx][i] += c * 0.5f; + } +} + +void ApplyDistanceComp(const al::span<FloatBufferLine> Samples, const ALsizei SamplesToDo, + const DistanceComp::DistData *distcomp) +{ + ASSUME(SamplesToDo > 0); + + for(auto &chanbuffer : Samples) + { + const ALfloat gain{distcomp->Gain}; + const ALsizei base{distcomp->Length}; + ALfloat *distbuf{al::assume_aligned<16>(distcomp->Buffer)}; + ++distcomp; + + if(base < 1) + continue; + + ALfloat *inout{al::assume_aligned<16>(chanbuffer.data())}; + auto inout_end = inout + SamplesToDo; + if(LIKELY(SamplesToDo >= base)) + { + auto delay_end = std::rotate(inout, inout_end - base, inout_end); + std::swap_ranges(inout, delay_end, distbuf); + } + else + { + auto delay_start = std::swap_ranges(inout, inout_end, distbuf); + std::rotate(distbuf, delay_start, distbuf + base); + } + std::transform(inout, inout_end, inout, std::bind(std::multiplies<float>{}, _1, gain)); + } +} + +void ApplyDither(const al::span<FloatBufferLine> Samples, ALuint *dither_seed, + const ALfloat quant_scale, const ALsizei SamplesToDo) +{ + /* Dithering. Generate whitenoise (uniform distribution of random values + * between -1 and +1) and add it to the sample values, after scaling up to + * the desired quantization depth amd before rounding. + */ + const ALfloat invscale{1.0f / quant_scale}; + ALuint seed{*dither_seed}; + auto dither_channel = [&seed,invscale,quant_scale,SamplesToDo](FloatBufferLine &input) -> void + { + ASSUME(SamplesToDo > 0); + auto dither_sample = [&seed,invscale,quant_scale](const ALfloat sample) noexcept -> ALfloat + { + ALfloat val{sample * quant_scale}; + ALuint rng0{dither_rng(&seed)}; + ALuint rng1{dither_rng(&seed)}; + val += static_cast<ALfloat>(rng0*(1.0/UINT_MAX) - rng1*(1.0/UINT_MAX)); + return fast_roundf(val) * invscale; + }; + std::transform(input.begin(), input.begin()+SamplesToDo, input.begin(), dither_sample); + }; + std::for_each(Samples.begin(), Samples.end(), dither_channel); + *dither_seed = seed; +} + + +/* Base template left undefined. Should be marked =delete, but Clang 3.8.1 + * chokes on that given the inline specializations. + */ +template<typename T> +inline T SampleConv(ALfloat) noexcept; + +template<> inline ALfloat SampleConv(ALfloat val) noexcept +{ return val; } +template<> inline ALint SampleConv(ALfloat val) noexcept +{ + /* Floats have a 23-bit mantissa, plus an implied 1 bit and a sign bit. + * This means a normalized float has at most 25 bits of signed precision. + * When scaling and clamping for a signed 32-bit integer, these following + * values are the best a float can give. + */ + return fastf2i(clampf(val*2147483648.0f, -2147483648.0f, 2147483520.0f)); +} +template<> inline ALshort SampleConv(ALfloat val) noexcept +{ return fastf2i(clampf(val*32768.0f, -32768.0f, 32767.0f)); } +template<> inline ALbyte SampleConv(ALfloat val) noexcept +{ return fastf2i(clampf(val*128.0f, -128.0f, 127.0f)); } + +/* Define unsigned output variations. */ +template<> inline ALuint SampleConv(ALfloat val) noexcept +{ return SampleConv<ALint>(val) + 2147483648u; } +template<> inline ALushort SampleConv(ALfloat val) noexcept +{ return SampleConv<ALshort>(val) + 32768; } +template<> inline ALubyte SampleConv(ALfloat val) noexcept +{ return SampleConv<ALbyte>(val) + 128; } + +template<DevFmtType T> +void Write(const al::span<const FloatBufferLine> InBuffer, ALvoid *OutBuffer, const size_t Offset, + const ALsizei SamplesToDo) +{ + using SampleType = typename DevFmtTypeTraits<T>::Type; + + const size_t numchans{InBuffer.size()}; + ASSUME(numchans > 0); + + SampleType *outbase = static_cast<SampleType*>(OutBuffer) + Offset*numchans; + auto conv_channel = [&outbase,SamplesToDo,numchans](const FloatBufferLine &inbuf) -> void + { + ASSUME(SamplesToDo > 0); + SampleType *out{outbase++}; + auto conv_sample = [numchans,&out](const ALfloat s) noexcept -> void + { + *out = SampleConv<SampleType>(s); + out += numchans; + }; + std::for_each(inbuf.begin(), inbuf.begin()+SamplesToDo, conv_sample); + }; + std::for_each(InBuffer.cbegin(), InBuffer.cend(), conv_channel); +} + +} // namespace + +void aluMixData(ALCdevice *device, ALvoid *OutBuffer, ALsizei NumSamples) +{ + FPUCtl mixer_mode{}; + for(ALsizei SamplesDone{0};SamplesDone < NumSamples;) + { + const ALsizei SamplesToDo{mini(NumSamples-SamplesDone, BUFFERSIZE)}; + + /* Clear main mixing buffers. */ + std::for_each(device->MixBuffer.begin(), device->MixBuffer.end(), + [SamplesToDo](std::array<ALfloat,BUFFERSIZE> &buffer) -> void + { std::fill_n(buffer.begin(), SamplesToDo, 0.0f); } + ); + + /* Increment the mix count at the start (lsb should now be 1). */ + IncrementRef(&device->MixCount); + + /* For each context on this device, process and mix its sources and + * effects. + */ + for(ALCcontext *ctx : *device->mContexts.load(std::memory_order_acquire)) + ProcessContext(ctx, SamplesToDo); + + /* Increment the clock time. Every second's worth of samples is + * converted and added to clock base so that large sample counts don't + * overflow during conversion. This also guarantees a stable + * conversion. + */ + device->SamplesDone += SamplesToDo; + device->ClockBase += std::chrono::seconds{device->SamplesDone / device->Frequency}; + device->SamplesDone %= device->Frequency; + + /* Increment the mix count at the end (lsb should now be 0). */ + IncrementRef(&device->MixCount); + + /* Apply any needed post-process for finalizing the Dry mix to the + * RealOut (Ambisonic decode, UHJ encode, etc). + */ + if(LIKELY(device->PostProcess)) + device->PostProcess(device, SamplesToDo); + const al::span<FloatBufferLine> RealOut{device->RealOut.Buffer}; + + /* Apply front image stablization for surround sound, if applicable. */ + if(device->Stablizer) + { + const int lidx{GetChannelIdxByName(device->RealOut, FrontLeft)}; + const int ridx{GetChannelIdxByName(device->RealOut, FrontRight)}; + const int cidx{GetChannelIdxByName(device->RealOut, FrontCenter)}; + assert(lidx >= 0 && ridx >= 0 && cidx >= 0); + + ApplyStablizer(device->Stablizer.get(), RealOut, lidx, ridx, cidx, SamplesToDo); + } + + /* Apply compression, limiting sample amplitude if needed or desired. */ + if(Compressor *comp{device->Limiter.get()}) + comp->process(SamplesToDo, RealOut.data()); + + /* Apply delays and attenuation for mismatched speaker distances. */ + ApplyDistanceComp(RealOut, SamplesToDo, device->ChannelDelay.as_span().cbegin()); + + /* Apply dithering. The compressor should have left enough headroom for + * the dither noise to not saturate. + */ + if(device->DitherDepth > 0.0f) + ApplyDither(RealOut, &device->DitherSeed, device->DitherDepth, SamplesToDo); + + if(LIKELY(OutBuffer)) + { + /* Finally, interleave and convert samples, writing to the device's + * output buffer. + */ + switch(device->FmtType) + { +#define HANDLE_WRITE(T) case T: \ + Write<T>(RealOut, OutBuffer, SamplesDone, SamplesToDo); break; + HANDLE_WRITE(DevFmtByte) + HANDLE_WRITE(DevFmtUByte) + HANDLE_WRITE(DevFmtShort) + HANDLE_WRITE(DevFmtUShort) + HANDLE_WRITE(DevFmtInt) + HANDLE_WRITE(DevFmtUInt) + HANDLE_WRITE(DevFmtFloat) +#undef HANDLE_WRITE + } + } + + SamplesDone += SamplesToDo; + } +} + + +void aluHandleDisconnect(ALCdevice *device, const char *msg, ...) +{ + if(!device->Connected.exchange(false, std::memory_order_acq_rel)) + return; + + AsyncEvent evt{EventType_Disconnected}; + evt.u.user.type = AL_EVENT_TYPE_DISCONNECTED_SOFT; + evt.u.user.id = 0; + evt.u.user.param = 0; + + va_list args; + va_start(args, msg); + int msglen{vsnprintf(evt.u.user.msg, sizeof(evt.u.user.msg), msg, args)}; + va_end(args); + + if(msglen < 0 || static_cast<size_t>(msglen) >= sizeof(evt.u.user.msg)) + evt.u.user.msg[sizeof(evt.u.user.msg)-1] = 0; + + for(ALCcontext *ctx : *device->mContexts.load()) + { + const ALbitfieldSOFT enabledevt{ctx->EnabledEvts.load(std::memory_order_acquire)}; + if((enabledevt&EventType_Disconnected)) + { + RingBuffer *ring{ctx->AsyncEvents.get()}; + auto evt_data = ring->getWriteVector().first; + if(evt_data.len > 0) + { + new (evt_data.buf) AsyncEvent{evt}; + ring->writeAdvance(1); + ctx->EventSem.post(); + } + } + + auto stop_voice = [](ALvoice &voice) -> void + { + voice.mCurrentBuffer.store(nullptr, std::memory_order_relaxed); + voice.mLoopBuffer.store(nullptr, std::memory_order_relaxed); + voice.mSourceID.store(0u, std::memory_order_relaxed); + voice.mPlayState.store(ALvoice::Stopped, std::memory_order_release); + }; + std::for_each(ctx->Voices->begin(), + ctx->Voices->begin() + ctx->VoiceCount.load(std::memory_order_acquire), + stop_voice); + } +} diff --git a/alc/alu.h b/alc/alu.h new file mode 100644 index 00000000..9acf904a --- /dev/null +++ b/alc/alu.h @@ -0,0 +1,466 @@ +#ifndef _ALU_H_ +#define _ALU_H_ + +#include <array> +#include <atomic> +#include <cmath> +#include <cstddef> + +#include "AL/al.h" +#include "AL/alc.h" +#include "AL/alext.h" + +#include "alBuffer.h" +#include "alcmain.h" +#include "almalloc.h" +#include "alspan.h" +#include "ambidefs.h" +#include "filters/biquad.h" +#include "filters/nfc.h" +#include "filters/splitter.h" +#include "hrtf.h" +#include "logging.h" + +struct ALbufferlistitem; +struct ALeffectslot; +struct BSincTable; + + +enum class DistanceModel; + +#define MAX_PITCH 255 +#define MAX_SENDS 16 + + +#define DITHER_RNG_SEED 22222 + + +enum SpatializeMode { + SpatializeOff = AL_FALSE, + SpatializeOn = AL_TRUE, + SpatializeAuto = AL_AUTO_SOFT +}; + +enum Resampler { + PointResampler, + LinearResampler, + FIR4Resampler, + BSinc12Resampler, + BSinc24Resampler, + + ResamplerMax = BSinc24Resampler +}; +extern Resampler ResamplerDefault; + +/* The number of distinct scale and phase intervals within the bsinc filter + * table. + */ +#define BSINC_SCALE_BITS 4 +#define BSINC_SCALE_COUNT (1<<BSINC_SCALE_BITS) +#define BSINC_PHASE_BITS 4 +#define BSINC_PHASE_COUNT (1<<BSINC_PHASE_BITS) + +/* Interpolator state. Kind of a misnomer since the interpolator itself is + * stateless. This just keeps it from having to recompute scale-related + * mappings for every sample. + */ +struct BsincState { + ALfloat sf; /* Scale interpolation factor. */ + ALsizei m; /* Coefficient count. */ + ALsizei l; /* Left coefficient offset. */ + /* Filter coefficients, followed by the scale, phase, and scale-phase + * delta coefficients. Starting at phase index 0, each subsequent phase + * index follows contiguously. + */ + const ALfloat *filter; +}; + +union InterpState { + BsincState bsinc; +}; + +using ResamplerFunc = const ALfloat*(*)(const InterpState *state, + const ALfloat *RESTRICT src, ALsizei frac, ALint increment, + ALfloat *RESTRICT dst, ALsizei dstlen); + +void BsincPrepare(const ALuint increment, BsincState *state, const BSincTable *table); + +extern const BSincTable bsinc12; +extern const BSincTable bsinc24; + + +enum { + AF_None = 0, + AF_LowPass = 1, + AF_HighPass = 2, + AF_BandPass = AF_LowPass | AF_HighPass +}; + + +struct MixHrtfFilter { + const HrirArray<ALfloat> *Coeffs; + ALsizei Delay[2]; + ALfloat Gain; + ALfloat GainStep; +}; + + +struct DirectParams { + BiquadFilter LowPass; + BiquadFilter HighPass; + + NfcFilter NFCtrlFilter; + + struct { + HrtfFilter Old; + HrtfFilter Target; + HrtfState State; + } Hrtf; + + struct { + ALfloat Current[MAX_OUTPUT_CHANNELS]; + ALfloat Target[MAX_OUTPUT_CHANNELS]; + } Gains; +}; + +struct SendParams { + BiquadFilter LowPass; + BiquadFilter HighPass; + + struct { + ALfloat Current[MAX_OUTPUT_CHANNELS]; + ALfloat Target[MAX_OUTPUT_CHANNELS]; + } Gains; +}; + + +struct ALvoicePropsBase { + ALfloat Pitch; + ALfloat Gain; + ALfloat OuterGain; + ALfloat MinGain; + ALfloat MaxGain; + ALfloat InnerAngle; + ALfloat OuterAngle; + ALfloat RefDistance; + ALfloat MaxDistance; + ALfloat RolloffFactor; + std::array<ALfloat,3> Position; + std::array<ALfloat,3> Velocity; + std::array<ALfloat,3> Direction; + std::array<ALfloat,3> OrientAt; + std::array<ALfloat,3> OrientUp; + ALboolean HeadRelative; + DistanceModel mDistanceModel; + Resampler mResampler; + ALboolean DirectChannels; + SpatializeMode mSpatializeMode; + + ALboolean DryGainHFAuto; + ALboolean WetGainAuto; + ALboolean WetGainHFAuto; + ALfloat OuterGainHF; + + ALfloat AirAbsorptionFactor; + ALfloat RoomRolloffFactor; + ALfloat DopplerFactor; + + std::array<ALfloat,2> StereoPan; + + ALfloat Radius; + + /** Direct filter and auxiliary send info. */ + struct { + ALfloat Gain; + ALfloat GainHF; + ALfloat HFReference; + ALfloat GainLF; + ALfloat LFReference; + } Direct; + struct SendData { + ALeffectslot *Slot; + ALfloat Gain; + ALfloat GainHF; + ALfloat HFReference; + ALfloat GainLF; + ALfloat LFReference; + } Send[MAX_SENDS]; +}; + +struct ALvoiceProps : public ALvoicePropsBase { + std::atomic<ALvoiceProps*> next{nullptr}; + + DEF_NEWDEL(ALvoiceProps) +}; + +#define VOICE_IS_STATIC (1u<<0) +#define VOICE_IS_FADING (1u<<1) /* Fading sources use gain stepping for smooth transitions. */ +#define VOICE_IS_AMBISONIC (1u<<2) /* Voice needs HF scaling for ambisonic upsampling. */ +#define VOICE_HAS_HRTF (1u<<3) +#define VOICE_HAS_NFC (1u<<4) + +struct ALvoice { + enum State { + Stopped = 0, + Playing = 1, + Stopping = 2 + }; + + std::atomic<ALvoiceProps*> mUpdate{nullptr}; + + std::atomic<ALuint> mSourceID{0u}; + std::atomic<State> mPlayState{Stopped}; + + ALvoicePropsBase mProps; + + /** + * Source offset in samples, relative to the currently playing buffer, NOT + * the whole queue. + */ + std::atomic<ALuint> mPosition; + /** Fractional (fixed-point) offset to the next sample. */ + std::atomic<ALsizei> mPositionFrac; + + /* Current buffer queue item being played. */ + std::atomic<ALbufferlistitem*> mCurrentBuffer; + + /* Buffer queue item to loop to at end of queue (will be NULL for non- + * looping voices). + */ + std::atomic<ALbufferlistitem*> mLoopBuffer; + + /* Properties for the attached buffer(s). */ + FmtChannels mFmtChannels; + ALuint mFrequency; + ALsizei mNumChannels; + ALsizei mSampleSize; + + /** Current target parameters used for mixing. */ + ALint mStep; + + ResamplerFunc mResampler; + + InterpState mResampleState; + + ALuint mFlags; + + struct DirectData { + int FilterType; + al::span<FloatBufferLine> Buffer; + }; + DirectData mDirect; + + struct SendData { + int FilterType; + al::span<FloatBufferLine> Buffer; + }; + std::array<SendData,MAX_SENDS> mSend; + + struct ChannelData { + alignas(16) std::array<ALfloat,MAX_RESAMPLE_PADDING*2> mPrevSamples; + + ALfloat mAmbiScale; + BandSplitter mAmbiSplitter; + + DirectParams mDryParams; + std::array<SendParams,MAX_SENDS> mWetParams; + }; + std::array<ChannelData,MAX_INPUT_CHANNELS> mChans; + + ALvoice() = default; + ALvoice(const ALvoice&) = delete; + ~ALvoice() { delete mUpdate.exchange(nullptr, std::memory_order_acq_rel); } + ALvoice& operator=(const ALvoice&) = delete; + ALvoice& operator=(ALvoice&& rhs) noexcept + { + ALvoiceProps *old_update{mUpdate.load(std::memory_order_relaxed)}; + mUpdate.store(rhs.mUpdate.exchange(old_update, std::memory_order_relaxed), + std::memory_order_relaxed); + + mSourceID.store(rhs.mSourceID.load(std::memory_order_relaxed), std::memory_order_relaxed); + mPlayState.store(rhs.mPlayState.load(std::memory_order_relaxed), + std::memory_order_relaxed); + + mProps = rhs.mProps; + + mPosition.store(rhs.mPosition.load(std::memory_order_relaxed), std::memory_order_relaxed); + mPositionFrac.store(rhs.mPositionFrac.load(std::memory_order_relaxed), + std::memory_order_relaxed); + + mCurrentBuffer.store(rhs.mCurrentBuffer.load(std::memory_order_relaxed), + std::memory_order_relaxed); + mLoopBuffer.store(rhs.mLoopBuffer.load(std::memory_order_relaxed), + std::memory_order_relaxed); + + mFmtChannels = rhs.mFmtChannels; + mFrequency = rhs.mFrequency; + mNumChannels = rhs.mNumChannels; + mSampleSize = rhs.mSampleSize; + + mStep = rhs.mStep; + mResampler = rhs.mResampler; + + mResampleState = rhs.mResampleState; + + mFlags = rhs.mFlags; + + mDirect = rhs.mDirect; + mSend = rhs.mSend; + mChans = rhs.mChans; + + return *this; + } +}; + + +using MixerFunc = void(*)(const ALfloat *data, const al::span<FloatBufferLine> OutBuffer, + ALfloat *CurrentGains, const ALfloat *TargetGains, const ALsizei Counter, const ALsizei OutPos, + const ALsizei BufferSize); +using RowMixerFunc = void(*)(FloatBufferLine &OutBuffer, const ALfloat *gains, + const al::span<const FloatBufferLine> InSamples, const ALsizei InPos, + const ALsizei BufferSize); +using HrtfMixerFunc = void(*)(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + MixHrtfFilter *hrtfparams, const ALsizei BufferSize); +using HrtfMixerBlendFunc = void(*)(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + const HrtfFilter *oldparams, MixHrtfFilter *newparams, const ALsizei BufferSize); +using HrtfDirectMixerFunc = void(*)(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples, DirectHrtfState *State, + const ALsizei BufferSize); + + +#define GAIN_MIX_MAX (1000.0f) /* +60dB */ + +#define GAIN_SILENCE_THRESHOLD (0.00001f) /* -100dB */ + +#define SPEEDOFSOUNDMETRESPERSEC (343.3f) +#define AIRABSORBGAINHF (0.99426f) /* -0.05dB */ + +/* Target gain for the reverb decay feedback reaching the decay time. */ +#define REVERB_DECAY_GAIN (0.001f) /* -60 dB */ + +#define FRACTIONBITS (12) +#define FRACTIONONE (1<<FRACTIONBITS) +#define FRACTIONMASK (FRACTIONONE-1) + + +inline ALfloat lerp(ALfloat val1, ALfloat val2, ALfloat mu) noexcept +{ return val1 + (val2-val1)*mu; } +inline ALfloat cubic(ALfloat val1, ALfloat val2, ALfloat val3, ALfloat val4, ALfloat mu) noexcept +{ + ALfloat mu2 = mu*mu, mu3 = mu2*mu; + ALfloat a0 = -0.5f*mu3 + mu2 + -0.5f*mu; + ALfloat a1 = 1.5f*mu3 + -2.5f*mu2 + 1.0f; + ALfloat a2 = -1.5f*mu3 + 2.0f*mu2 + 0.5f*mu; + ALfloat a3 = 0.5f*mu3 + -0.5f*mu2; + return val1*a0 + val2*a1 + val3*a2 + val4*a3; +} + + +enum HrtfRequestMode { + Hrtf_Default = 0, + Hrtf_Enable = 1, + Hrtf_Disable = 2, +}; + +void aluInit(void); + +void aluInitMixer(void); + +ResamplerFunc SelectResampler(Resampler resampler); + +/* aluInitRenderer + * + * Set up the appropriate panning method and mixing method given the device + * properties. + */ +void aluInitRenderer(ALCdevice *device, ALint hrtf_id, HrtfRequestMode hrtf_appreq, HrtfRequestMode hrtf_userreq); + +void aluInitEffectPanning(ALeffectslot *slot, ALCdevice *device); + +void ProcessHrtf(ALCdevice *device, const ALsizei SamplesToDo); +void ProcessAmbiDec(ALCdevice *device, const ALsizei SamplesToDo); +void ProcessUhj(ALCdevice *device, const ALsizei SamplesToDo); +void ProcessBs2b(ALCdevice *device, const ALsizei SamplesToDo); + +/** + * Calculates ambisonic encoder coefficients using the X, Y, and Z direction + * components, which must represent a normalized (unit length) vector, and the + * spread is the angular width of the sound (0...tau). + * + * NOTE: The components use ambisonic coordinates. As a result: + * + * Ambisonic Y = OpenAL -X + * Ambisonic Z = OpenAL Y + * Ambisonic X = OpenAL -Z + * + * The components are ordered such that OpenAL's X, Y, and Z are the first, + * second, and third parameters respectively -- simply negate X and Z. + */ +void CalcAmbiCoeffs(const ALfloat y, const ALfloat z, const ALfloat x, const ALfloat spread, + ALfloat (&coeffs)[MAX_AMBI_CHANNELS]); + +/** + * CalcDirectionCoeffs + * + * Calculates ambisonic coefficients based on an OpenAL direction vector. The + * vector must be normalized (unit length), and the spread is the angular width + * of the sound (0...tau). + */ +inline void CalcDirectionCoeffs(const ALfloat (&dir)[3], ALfloat spread, ALfloat (&coeffs)[MAX_AMBI_CHANNELS]) +{ + /* Convert from OpenAL coords to Ambisonics. */ + CalcAmbiCoeffs(-dir[0], dir[1], -dir[2], spread, coeffs); +} + +/** + * CalcAngleCoeffs + * + * Calculates ambisonic coefficients based on azimuth and elevation. The + * azimuth and elevation parameters are in radians, going right and up + * respectively. + */ +inline void CalcAngleCoeffs(ALfloat azimuth, ALfloat elevation, ALfloat spread, ALfloat (&coeffs)[MAX_AMBI_CHANNELS]) +{ + ALfloat x = -std::sin(azimuth) * std::cos(elevation); + ALfloat y = std::sin(elevation); + ALfloat z = std::cos(azimuth) * std::cos(elevation); + + CalcAmbiCoeffs(x, y, z, spread, coeffs); +} + + +/** + * ComputePanGains + * + * Computes panning gains using the given channel decoder coefficients and the + * pre-calculated direction or angle coefficients. For B-Format sources, the + * coeffs are a 'slice' of a transform matrix for the input channel, used to + * scale and orient the sound samples. + */ +void ComputePanGains(const MixParams *mix, const ALfloat*RESTRICT coeffs, ALfloat ingain, ALfloat (&gains)[MAX_OUTPUT_CHANNELS]); + + +inline std::array<ALfloat,MAX_AMBI_CHANNELS> GetAmbiIdentityRow(size_t i) noexcept +{ + std::array<ALfloat,MAX_AMBI_CHANNELS> ret{}; + ret[i] = 1.0f; + return ret; +} + + +void MixVoice(ALvoice *voice, ALvoice::State vstate, const ALuint SourceID, ALCcontext *Context, const ALsizei SamplesToDo); + +void aluMixData(ALCdevice *device, ALvoid *OutBuffer, ALsizei NumSamples); +/* Caller must lock the device state, and the mixer must not be running. */ +void aluHandleDisconnect(ALCdevice *device, const char *msg, ...) DECL_FORMAT(printf, 2, 3); + +extern MixerFunc MixSamples; +extern RowMixerFunc MixRowSamples; + +extern const ALfloat ConeScale; +extern const ALfloat ZScale; +extern const ALboolean OverrideReverbSpeedOfSound; + +#endif diff --git a/alc/ambdec.cpp b/alc/ambdec.cpp new file mode 100644 index 00000000..0991cfc5 --- /dev/null +++ b/alc/ambdec.cpp @@ -0,0 +1,436 @@ + +#include "config.h" + +#include "ambdec.h" + +#include <cctype> +#include <cstring> +#include <algorithm> + +#include <limits> +#include <string> +#include <fstream> +#include <sstream> + +#include "logging.h" +#include "compat.h" + + +namespace { + +template<typename T, std::size_t N> +constexpr inline std::size_t size(const T(&)[N]) noexcept +{ return N; } + +int readline(std::istream &f, std::string &output) +{ + while(f.good() && f.peek() == '\n') + f.ignore(); + + return std::getline(f, output) && !output.empty(); +} + +bool read_clipped_line(std::istream &f, std::string &buffer) +{ + while(readline(f, buffer)) + { + std::size_t pos{0}; + while(pos < buffer.length() && std::isspace(buffer[pos])) + pos++; + buffer.erase(0, pos); + + std::size_t cmtpos{buffer.find_first_of('#')}; + if(cmtpos < buffer.length()) + buffer.resize(cmtpos); + while(!buffer.empty() && std::isspace(buffer.back())) + buffer.pop_back(); + + if(!buffer.empty()) + return true; + } + return false; +} + + +std::string read_word(std::istream &f) +{ + std::string ret; + f >> ret; + return ret; +} + +bool is_at_end(const std::string &buffer, std::size_t endpos) +{ + while(endpos < buffer.length() && std::isspace(buffer[endpos])) + ++endpos; + return !(endpos < buffer.length()); +} + + +bool load_ambdec_speakers(al::vector<AmbDecConf::SpeakerConf> &spkrs, const std::size_t num_speakers, std::istream &f, std::string &buffer) +{ + while(spkrs.size() < num_speakers) + { + std::istringstream istr{buffer}; + + std::string cmd{read_word(istr)}; + if(cmd.empty()) + { + if(!read_clipped_line(f, buffer)) + { + ERR("Unexpected end of file\n"); + return false; + } + continue; + } + + if(cmd == "add_spkr") + { + spkrs.emplace_back(); + AmbDecConf::SpeakerConf &spkr = spkrs.back(); + const size_t spkr_num{spkrs.size()}; + + istr >> spkr.Name; + if(istr.fail()) WARN("Name not specified for speaker %zu\n", spkr_num); + istr >> spkr.Distance; + if(istr.fail()) WARN("Distance not specified for speaker %zu\n", spkr_num); + istr >> spkr.Azimuth; + if(istr.fail()) WARN("Azimuth not specified for speaker %zu\n", spkr_num); + istr >> spkr.Elevation; + if(istr.fail()) WARN("Elevation not specified for speaker %zu\n", spkr_num); + istr >> spkr.Connection; + if(istr.fail()) TRACE("Connection not specified for speaker %zu\n", spkr_num); + } + else + { + ERR("Unexpected speakers command: %s\n", cmd.c_str()); + return false; + } + + istr.clear(); + const auto endpos = static_cast<std::size_t>(istr.tellg()); + if(!is_at_end(buffer, endpos)) + { + ERR("Unexpected junk on line: %s\n", buffer.c_str()+endpos); + return false; + } + buffer.clear(); + } + + return true; +} + +bool load_ambdec_matrix(float (&gains)[MAX_AMBI_ORDER+1], al::vector<AmbDecConf::CoeffArray> &matrix, const std::size_t maxrow, std::istream &f, std::string &buffer) +{ + bool gotgains{false}; + std::size_t cur{0u}; + while(cur < maxrow) + { + std::istringstream istr{buffer}; + + std::string cmd{read_word(istr)}; + if(cmd.empty()) + { + if(!read_clipped_line(f, buffer)) + { + ERR("Unexpected end of file\n"); + return false; + } + continue; + } + + if(cmd == "order_gain") + { + std::size_t curgain{0u}; + float value; + while(istr.good()) + { + istr >> value; + if(istr.fail()) break; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk on gain %zu: %s\n", curgain+1, + buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + return false; + } + if(curgain < size(gains)) + gains[curgain++] = value; + } + std::fill(std::begin(gains)+curgain, std::end(gains), 0.0f); + gotgains = true; + } + else if(cmd == "add_row") + { + matrix.emplace_back(); + AmbDecConf::CoeffArray &mtxrow = matrix.back(); + std::size_t curidx{0u}; + float value{}; + while(istr.good()) + { + istr >> value; + if(istr.fail()) break; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk on matrix element %zux%zu: %s\n", curidx, + matrix.size(), buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + matrix.pop_back(); + return false; + } + if(curidx < mtxrow.size()) + mtxrow[curidx++] = value; + } + std::fill(mtxrow.begin()+curidx, mtxrow.end(), 0.0f); + cur++; + } + else + { + ERR("Unexpected matrix command: %s\n", cmd.c_str()); + return false; + } + + istr.clear(); + const auto endpos = static_cast<std::size_t>(istr.tellg()); + if(!is_at_end(buffer, endpos)) + { + ERR("Unexpected junk on line: %s\n", buffer.c_str()+endpos); + return false; + } + buffer.clear(); + } + + if(!gotgains) + { + ERR("Matrix order_gain not specified\n"); + return false; + } + + return true; +} + +} // namespace + +int AmbDecConf::load(const char *fname) noexcept +{ + al::ifstream f{fname}; + if(!f.is_open()) + { + ERR("Failed to open: %s\n", fname); + return 0; + } + + std::size_t num_speakers{0u}; + std::string buffer; + while(read_clipped_line(f, buffer)) + { + std::istringstream istr{buffer}; + + std::string command{read_word(istr)}; + if(command.empty()) + { + ERR("Malformed line: %s\n", buffer.c_str()); + return 0; + } + + if(command == "/description") + istr >> Description; + else if(command == "/version") + { + istr >> Version; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk after version: %s\n", + buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + return 0; + } + if(Version != 3) + { + ERR("Unsupported version: %u\n", Version); + return 0; + } + } + else if(command == "/dec/chan_mask") + { + istr >> std::hex >> ChanMask >> std::dec; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk after mask: %s\n", + buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + return 0; + } + } + else if(command == "/dec/freq_bands") + { + istr >> FreqBands; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk after freq_bands: %s\n", + buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + return 0; + } + if(FreqBands != 1 && FreqBands != 2) + { + ERR("Invalid freq_bands value: %u\n", FreqBands); + return 0; + } + } + else if(command == "/dec/speakers") + { + istr >> num_speakers; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk after speakers: %s\n", + buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + return 0; + } + Speakers.reserve(num_speakers); + LFMatrix.reserve(num_speakers); + HFMatrix.reserve(num_speakers); + } + else if(command == "/dec/coeff_scale") + { + std::string scale = read_word(istr); + if(scale == "n3d") CoeffScale = AmbDecScale::N3D; + else if(scale == "sn3d") CoeffScale = AmbDecScale::SN3D; + else if(scale == "fuma") CoeffScale = AmbDecScale::FuMa; + else + { + ERR("Unsupported coeff scale: %s\n", scale.c_str()); + return 0; + } + } + else if(command == "/opt/xover_freq") + { + istr >> XOverFreq; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk after xover_freq: %s\n", + buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + return 0; + } + } + else if(command == "/opt/xover_ratio") + { + istr >> XOverRatio; + if(!istr.eof() && !std::isspace(istr.peek())) + { + ERR("Extra junk after xover_ratio: %s\n", + buffer.c_str()+static_cast<std::size_t>(istr.tellg())); + return 0; + } + } + else if(command == "/opt/input_scale" || command == "/opt/nfeff_comp" || + command == "/opt/delay_comp" || command == "/opt/level_comp") + { + /* Unused */ + read_word(istr); + } + else if(command == "/speakers/{") + { + const auto endpos = static_cast<std::size_t>(istr.tellg()); + if(!is_at_end(buffer, endpos)) + { + ERR("Unexpected junk on line: %s\n", buffer.c_str()+endpos); + return 0; + } + buffer.clear(); + + if(!load_ambdec_speakers(Speakers, num_speakers, f, buffer)) + return 0; + + if(!read_clipped_line(f, buffer)) + { + ERR("Unexpected end of file\n"); + return 0; + } + std::istringstream istr2{buffer}; + std::string endmark{read_word(istr2)}; + if(endmark != "/}") + { + ERR("Expected /} after speaker definitions, got %s\n", endmark.c_str()); + return 0; + } + istr.swap(istr2); + } + else if(command == "/lfmatrix/{" || command == "/hfmatrix/{" || command == "/matrix/{") + { + const auto endpos = static_cast<std::size_t>(istr.tellg()); + if(!is_at_end(buffer, endpos)) + { + ERR("Unexpected junk on line: %s\n", buffer.c_str()+endpos); + return 0; + } + buffer.clear(); + + if(FreqBands == 1) + { + if(command != "/matrix/{") + { + ERR("Unexpected \"%s\" type for a single-band decoder\n", command.c_str()); + return 0; + } + if(!load_ambdec_matrix(HFOrderGain, HFMatrix, num_speakers, f, buffer)) + return 0; + } + else + { + if(command == "/lfmatrix/{") + { + if(!load_ambdec_matrix(LFOrderGain, LFMatrix, num_speakers, f, buffer)) + return 0; + } + else if(command == "/hfmatrix/{") + { + if(!load_ambdec_matrix(HFOrderGain, HFMatrix, num_speakers, f, buffer)) + return 0; + } + else + { + ERR("Unexpected \"%s\" type for a dual-band decoder\n", command.c_str()); + return 0; + } + } + + if(!read_clipped_line(f, buffer)) + { + ERR("Unexpected end of file\n"); + return 0; + } + std::istringstream istr2{buffer}; + std::string endmark{read_word(istr2)}; + if(endmark != "/}") + { + ERR("Expected /} after matrix definitions, got %s\n", endmark.c_str()); + return 0; + } + istr.swap(istr2); + } + else if(command == "/end") + { + const auto endpos = static_cast<std::size_t>(istr.tellg()); + if(!is_at_end(buffer, endpos)) + { + ERR("Unexpected junk on end: %s\n", buffer.c_str()+endpos); + return 0; + } + + return 1; + } + else + { + ERR("Unexpected command: %s\n", command.c_str()); + return 0; + } + + istr.clear(); + const auto endpos = static_cast<std::size_t>(istr.tellg()); + if(!is_at_end(buffer, endpos)) + { + ERR("Unexpected junk on line: %s\n", buffer.c_str()+endpos); + return 0; + } + buffer.clear(); + } + ERR("Unexpected end of file\n"); + + return 0; +} diff --git a/alc/ambdec.h b/alc/ambdec.h new file mode 100644 index 00000000..ff7b71ee --- /dev/null +++ b/alc/ambdec.h @@ -0,0 +1,48 @@ +#ifndef AMBDEC_H +#define AMBDEC_H + +#include <array> +#include <string> + +#include "ambidefs.h" +#include "vector.h" + +/* Helpers to read .ambdec configuration files. */ + +enum class AmbDecScale { + N3D, + SN3D, + FuMa, +}; +struct AmbDecConf { + std::string Description; + int Version{0}; /* Must be 3 */ + + unsigned int ChanMask{0u}; + unsigned int FreqBands{0u}; /* Must be 1 or 2 */ + AmbDecScale CoeffScale{}; + + float XOverFreq{0.0f}; + float XOverRatio{0.0f}; + + struct SpeakerConf { + std::string Name; + float Distance{0.0f}; + float Azimuth{0.0f}; + float Elevation{0.0f}; + std::string Connection; + }; + al::vector<SpeakerConf> Speakers; + + using CoeffArray = std::array<float,MAX_AMBI_CHANNELS>; + /* Unused when FreqBands == 1 */ + float LFOrderGain[MAX_AMBI_ORDER+1]{}; + al::vector<CoeffArray> LFMatrix; + + float HFOrderGain[MAX_AMBI_ORDER+1]{}; + al::vector<CoeffArray> HFMatrix; + + int load(const char *fname) noexcept; +}; + +#endif /* AMBDEC_H */ diff --git a/alc/ambidefs.h b/alc/ambidefs.h new file mode 100644 index 00000000..17a9815b --- /dev/null +++ b/alc/ambidefs.h @@ -0,0 +1,119 @@ +#ifndef AMBIDEFS_H +#define AMBIDEFS_H + +#include <array> + +/* The maximum number of Ambisonics channels. For a given order (o), the size + * needed will be (o+1)**2, thus zero-order has 1, first-order has 4, second- + * order has 9, third-order has 16, and fourth-order has 25. + */ +#define MAX_AMBI_ORDER 3 +constexpr inline size_t AmbiChannelsFromOrder(size_t order) noexcept +{ return (order+1) * (order+1); } +#define MAX_AMBI_CHANNELS AmbiChannelsFromOrder(MAX_AMBI_ORDER) + +/* A bitmask of ambisonic channels for 0 to 4th order. This only specifies up + * to 4th order, which is the highest order a 32-bit mask value can specify (a + * 64-bit mask could handle up to 7th order). + */ +#define AMBI_0ORDER_MASK 0x00000001 +#define AMBI_1ORDER_MASK 0x0000000f +#define AMBI_2ORDER_MASK 0x000001ff +#define AMBI_3ORDER_MASK 0x0000ffff +#define AMBI_4ORDER_MASK 0x01ffffff + +/* A bitmask of ambisonic channels with height information. If none of these + * channels are used/needed, there's no height (e.g. with most surround sound + * speaker setups). This is ACN ordering, with bit 0 being ACN 0, etc. + */ +#define AMBI_PERIPHONIC_MASK (0xfe7ce4) + +/* The maximum number of ambisonic channels for 2D (non-periphonic) + * representation. This is 2 per each order above zero-order, plus 1 for zero- + * order. Or simply, o*2 + 1. + */ +constexpr inline size_t Ambi2DChannelsFromOrder(size_t order) noexcept +{ return order*2 + 1; } +#define MAX_AMBI2D_CHANNELS Ambi2DChannelsFromOrder(MAX_AMBI_ORDER) + + +/* NOTE: These are scale factors as applied to Ambisonics content. Decoder + * coefficients should be divided by these values to get proper scalings. + */ +struct AmbiScale { + static constexpr std::array<float,MAX_AMBI_CHANNELS> FromN3D{{ + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f + }}; + static constexpr std::array<float,MAX_AMBI_CHANNELS> FromSN3D{{ + 1.000000000f, /* ACN 0, sqrt(1) */ + 1.732050808f, /* ACN 1, sqrt(3) */ + 1.732050808f, /* ACN 2, sqrt(3) */ + 1.732050808f, /* ACN 3, sqrt(3) */ + 2.236067978f, /* ACN 4, sqrt(5) */ + 2.236067978f, /* ACN 5, sqrt(5) */ + 2.236067978f, /* ACN 6, sqrt(5) */ + 2.236067978f, /* ACN 7, sqrt(5) */ + 2.236067978f, /* ACN 8, sqrt(5) */ + 2.645751311f, /* ACN 9, sqrt(7) */ + 2.645751311f, /* ACN 10, sqrt(7) */ + 2.645751311f, /* ACN 11, sqrt(7) */ + 2.645751311f, /* ACN 12, sqrt(7) */ + 2.645751311f, /* ACN 13, sqrt(7) */ + 2.645751311f, /* ACN 14, sqrt(7) */ + 2.645751311f, /* ACN 15, sqrt(7) */ + }}; + static constexpr std::array<float,MAX_AMBI_CHANNELS> FromFuMa{{ + 1.414213562f, /* ACN 0 (W), sqrt(2) */ + 1.732050808f, /* ACN 1 (Y), sqrt(3) */ + 1.732050808f, /* ACN 2 (Z), sqrt(3) */ + 1.732050808f, /* ACN 3 (X), sqrt(3) */ + 1.936491673f, /* ACN 4 (V), sqrt(15)/2 */ + 1.936491673f, /* ACN 5 (T), sqrt(15)/2 */ + 2.236067978f, /* ACN 6 (R), sqrt(5) */ + 1.936491673f, /* ACN 7 (S), sqrt(15)/2 */ + 1.936491673f, /* ACN 8 (U), sqrt(15)/2 */ + 2.091650066f, /* ACN 9 (Q), sqrt(35/8) */ + 1.972026594f, /* ACN 10 (O), sqrt(35)/3 */ + 2.231093404f, /* ACN 11 (M), sqrt(224/45) */ + 2.645751311f, /* ACN 12 (K), sqrt(7) */ + 2.231093404f, /* ACN 13 (L), sqrt(224/45) */ + 1.972026594f, /* ACN 14 (N), sqrt(35)/3 */ + 2.091650066f, /* ACN 15 (P), sqrt(35/8) */ + }}; +}; + +struct AmbiIndex { + static constexpr std::array<int,MAX_AMBI_CHANNELS> FromFuMa{{ + 0, /* W */ + 3, /* X */ + 1, /* Y */ + 2, /* Z */ + 6, /* R */ + 7, /* S */ + 5, /* T */ + 8, /* U */ + 4, /* V */ + 12, /* K */ + 13, /* L */ + 11, /* M */ + 14, /* N */ + 10, /* O */ + 15, /* P */ + 9, /* Q */ + }}; + static constexpr std::array<int,MAX_AMBI_CHANNELS> FromACN{{ + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15 + }}; + + static constexpr std::array<int,MAX_AMBI2D_CHANNELS> From2D{{ + 0, 1,3, 4,8, 9,15 + }}; + static constexpr std::array<int,MAX_AMBI_CHANNELS> From3D{{ + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15 + }}; +}; + +#endif /* AMBIDEFS_H */ 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 */ diff --git a/alc/bformatdec.cpp b/alc/bformatdec.cpp new file mode 100644 index 00000000..889bbf3a --- /dev/null +++ b/alc/bformatdec.cpp @@ -0,0 +1,200 @@ + +#include "config.h" + +#include "bformatdec.h" + +#include <algorithm> +#include <array> +#include <cassert> +#include <cmath> +#include <iterator> +#include <numeric> + +#include "almalloc.h" +#include "alu.h" +#include "ambdec.h" +#include "filters/splitter.h" +#include "opthelpers.h" + + +namespace { + +constexpr ALfloat Ambi3DDecoderHFScale[MAX_AMBI_ORDER+1] = { + 1.00000000e+00f, 1.00000000e+00f +}; +constexpr ALfloat Ambi3DDecoderHFScale2O[MAX_AMBI_ORDER+1] = { + 7.45355990e-01f, 1.00000000e+00f +}; +constexpr ALfloat Ambi3DDecoderHFScale3O[MAX_AMBI_ORDER+1] = { + 5.89792205e-01f, 8.79693856e-01f +}; + +inline auto GetDecoderHFScales(ALsizei order) noexcept -> const ALfloat(&)[MAX_AMBI_ORDER+1] +{ + if(order >= 3) return Ambi3DDecoderHFScale3O; + if(order == 2) return Ambi3DDecoderHFScale2O; + return Ambi3DDecoderHFScale; +} + +inline auto GetAmbiScales(AmbDecScale scaletype) noexcept -> const std::array<float,MAX_AMBI_CHANNELS>& +{ + if(scaletype == AmbDecScale::FuMa) return AmbiScale::FromFuMa; + if(scaletype == AmbDecScale::SN3D) return AmbiScale::FromSN3D; + return AmbiScale::FromN3D; +} + +} // namespace + + +BFormatDec::BFormatDec(const AmbDecConf *conf, const bool allow_2band, const ALuint inchans, + const ALuint srate, const ALsizei (&chanmap)[MAX_OUTPUT_CHANNELS]) +{ + mDualBand = allow_2band && (conf->FreqBands == 2); + if(!mDualBand) + mSamples.resize(2); + else + { + ASSUME(inchans > 0); + mSamples.resize(inchans * 2); + mSamplesHF = mSamples.data(); + mSamplesLF = mSamplesHF + inchans; + } + mNumChannels = inchans; + + mEnabled = std::accumulate(std::begin(chanmap), std::begin(chanmap)+conf->Speakers.size(), 0u, + [](ALuint mask, const ALsizei &chan) noexcept -> ALuint + { return mask | (1 << chan); } + ); + + const ALfloat xover_norm{conf->XOverFreq / static_cast<float>(srate)}; + + const bool periphonic{(conf->ChanMask&AMBI_PERIPHONIC_MASK) != 0}; + const std::array<float,MAX_AMBI_CHANNELS> &coeff_scale = GetAmbiScales(conf->CoeffScale); + const size_t coeff_count{periphonic ? MAX_AMBI_CHANNELS : MAX_AMBI2D_CHANNELS}; + + if(!mDualBand) + { + for(size_t i{0u};i < conf->Speakers.size();i++) + { + ALfloat (&mtx)[MAX_AMBI_CHANNELS] = mMatrix.Single[chanmap[i]]; + for(size_t j{0},k{0};j < coeff_count;j++) + { + const size_t l{periphonic ? j : AmbiIndex::From2D[j]}; + if(!(conf->ChanMask&(1u<<l))) continue; + mtx[j] = conf->HFMatrix[i][k] / coeff_scale[l] * + ((l>=9) ? conf->HFOrderGain[3] : + (l>=4) ? conf->HFOrderGain[2] : + (l>=1) ? conf->HFOrderGain[1] : conf->HFOrderGain[0]); + ++k; + } + } + } + else + { + mXOver[0].init(xover_norm); + std::fill(std::begin(mXOver)+1, std::end(mXOver), mXOver[0]); + + const float ratio{std::pow(10.0f, conf->XOverRatio / 40.0f)}; + for(size_t i{0u};i < conf->Speakers.size();i++) + { + ALfloat (&mtx)[sNumBands][MAX_AMBI_CHANNELS] = mMatrix.Dual[chanmap[i]]; + for(size_t j{0},k{0};j < coeff_count;j++) + { + const size_t l{periphonic ? j : AmbiIndex::From2D[j]}; + if(!(conf->ChanMask&(1u<<l))) continue; + mtx[sHFBand][j] = conf->HFMatrix[i][k] / coeff_scale[l] * + ((l>=9) ? conf->HFOrderGain[3] : + (l>=4) ? conf->HFOrderGain[2] : + (l>=1) ? conf->HFOrderGain[1] : conf->HFOrderGain[0]) * ratio; + mtx[sLFBand][j] = conf->LFMatrix[i][k] / coeff_scale[l] * + ((l>=9) ? conf->LFOrderGain[3] : + (l>=4) ? conf->LFOrderGain[2] : + (l>=1) ? conf->LFOrderGain[1] : conf->LFOrderGain[0]) / ratio; + ++k; + } + } + } +} + +BFormatDec::BFormatDec(const ALuint inchans, const ALsizei chancount, + const ChannelDec (&chancoeffs)[MAX_OUTPUT_CHANNELS], + const ALsizei (&chanmap)[MAX_OUTPUT_CHANNELS]) +{ + mSamples.resize(2); + mNumChannels = inchans; + + ASSUME(chancount > 0); + mEnabled = std::accumulate(std::begin(chanmap), std::begin(chanmap)+chancount, 0u, + [](ALuint mask, const ALsizei &chan) noexcept -> ALuint + { return mask | (1 << chan); } + ); + + const ChannelDec *incoeffs{chancoeffs}; + auto set_coeffs = [this,inchans,&incoeffs](const ALsizei chanidx) noexcept -> void + { + ASSUME(chanidx >= 0); + ALfloat (&mtx)[MAX_AMBI_CHANNELS] = mMatrix.Single[chanidx]; + const ALfloat (&coeffs)[MAX_AMBI_CHANNELS] = *(incoeffs++); + + ASSUME(inchans > 0); + std::copy_n(std::begin(coeffs), inchans, std::begin(mtx)); + }; + std::for_each(chanmap, chanmap+chancount, set_coeffs); +} + + +void BFormatDec::process(const al::span<FloatBufferLine> OutBuffer, + const FloatBufferLine *InSamples, const ALsizei SamplesToDo) +{ + if(mDualBand) + { + for(ALuint i{0};i < mNumChannels;i++) + mXOver[i].process(mSamplesHF[i].data(), mSamplesLF[i].data(), InSamples[i].data(), + SamplesToDo); + + const al::span<const FloatBufferLine> hfsamples{mSamplesHF, mNumChannels}; + const al::span<const FloatBufferLine> lfsamples{mSamplesLF, mNumChannels}; + ALfloat (*mixmtx)[sNumBands][MAX_AMBI_CHANNELS]{mMatrix.Dual}; + ALuint enabled{mEnabled}; + for(FloatBufferLine &outbuf : OutBuffer) + { + if(LIKELY(enabled&1)) + { + MixRowSamples(outbuf, (*mixmtx)[sHFBand], hfsamples, 0, SamplesToDo); + MixRowSamples(outbuf, (*mixmtx)[sLFBand], lfsamples, 0, SamplesToDo); + } + ++mixmtx; + enabled >>= 1; + } + } + else + { + const al::span<const FloatBufferLine> insamples{InSamples, mNumChannels}; + ALfloat (*mixmtx)[MAX_AMBI_CHANNELS]{mMatrix.Single}; + ALuint enabled{mEnabled}; + for(FloatBufferLine &outbuf : OutBuffer) + { + if(LIKELY(enabled&1)) + MixRowSamples(outbuf, *mixmtx, insamples, 0, SamplesToDo); + ++mixmtx; + enabled >>= 1; + } + } +} + + +std::array<ALfloat,MAX_AMBI_ORDER+1> BFormatDec::GetHFOrderScales(const ALsizei in_order, const ALsizei out_order) noexcept +{ + std::array<ALfloat,MAX_AMBI_ORDER+1> ret{}; + + assert(out_order >= in_order); + ASSUME(out_order >= in_order); + + const ALfloat (&target)[MAX_AMBI_ORDER+1] = GetDecoderHFScales(out_order); + const ALfloat (&input)[MAX_AMBI_ORDER+1] = GetDecoderHFScales(in_order); + + for(ALsizei i{0};i < in_order+1;++i) + ret[i] = input[i] / target[i]; + + return ret; +} diff --git a/alc/bformatdec.h b/alc/bformatdec.h new file mode 100644 index 00000000..06974651 --- /dev/null +++ b/alc/bformatdec.h @@ -0,0 +1,62 @@ +#ifndef BFORMATDEC_H +#define BFORMATDEC_H + +#include <array> +#include <cstddef> + +#include "AL/al.h" + +#include "alcmain.h" +#include "almalloc.h" +#include "alspan.h" +#include "ambidefs.h" +#include "filters/splitter.h" +#include "vector.h" + +struct AmbDecConf; + + +using ChannelDec = ALfloat[MAX_AMBI_CHANNELS]; + +class BFormatDec { + static constexpr size_t sHFBand{0}; + static constexpr size_t sLFBand{1}; + static constexpr size_t sNumBands{2}; + + ALuint mEnabled{0u}; /* Bitfield of enabled channels. */ + + union MatrixU { + ALfloat Dual[MAX_OUTPUT_CHANNELS][sNumBands][MAX_AMBI_CHANNELS]; + ALfloat Single[MAX_OUTPUT_CHANNELS][MAX_AMBI_CHANNELS]; + } mMatrix{}; + + /* NOTE: BandSplitter filters are unused with single-band decoding */ + BandSplitter mXOver[MAX_AMBI_CHANNELS]; + + al::vector<FloatBufferLine, 16> mSamples; + /* These two alias into Samples */ + FloatBufferLine *mSamplesHF{nullptr}; + FloatBufferLine *mSamplesLF{nullptr}; + + ALuint mNumChannels{0u}; + bool mDualBand{false}; + +public: + BFormatDec(const AmbDecConf *conf, const bool allow_2band, const ALuint inchans, + const ALuint srate, const ALsizei (&chanmap)[MAX_OUTPUT_CHANNELS]); + BFormatDec(const ALuint inchans, const ALsizei chancount, + const ChannelDec (&chancoeffs)[MAX_OUTPUT_CHANNELS], + const ALsizei (&chanmap)[MAX_OUTPUT_CHANNELS]); + + /* Decodes the ambisonic input to the given output channels. */ + void process(const al::span<FloatBufferLine> OutBuffer, const FloatBufferLine *InSamples, + const ALsizei SamplesToDo); + + /* Retrieves per-order HF scaling factors for "upsampling" ambisonic data. */ + static std::array<ALfloat,MAX_AMBI_ORDER+1> GetHFOrderScales(const ALsizei in_order, + const ALsizei out_order) noexcept; + + DEF_NEWDEL(BFormatDec) +}; + +#endif /* BFORMATDEC_H */ diff --git a/alc/bs2b.cpp b/alc/bs2b.cpp new file mode 100644 index 00000000..2d1b96aa --- /dev/null +++ b/alc/bs2b.cpp @@ -0,0 +1,188 @@ +/*- + * Copyright (c) 2005 Boris Mikhaylov + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include <cmath> +#include <cstring> +#include <algorithm> + +#include "bs2b.h" +#include "math_defs.h" + + +/* Set up all data. */ +static void init(struct bs2b *bs2b) +{ + float Fc_lo, Fc_hi; + float G_lo, G_hi; + float x, g; + + switch(bs2b->level) + { + case BS2B_LOW_CLEVEL: /* Low crossfeed level */ + Fc_lo = 360.0f; + Fc_hi = 501.0f; + G_lo = 0.398107170553497f; + G_hi = 0.205671765275719f; + break; + + case BS2B_MIDDLE_CLEVEL: /* Middle crossfeed level */ + Fc_lo = 500.0f; + Fc_hi = 711.0f; + G_lo = 0.459726988530872f; + G_hi = 0.228208484414988f; + break; + + case BS2B_HIGH_CLEVEL: /* High crossfeed level (virtual speakers are closer to itself) */ + Fc_lo = 700.0f; + Fc_hi = 1021.0f; + G_lo = 0.530884444230988f; + G_hi = 0.250105790667544f; + break; + + case BS2B_LOW_ECLEVEL: /* Low easy crossfeed level */ + Fc_lo = 360.0f; + Fc_hi = 494.0f; + G_lo = 0.316227766016838f; + G_hi = 0.168236228897329f; + break; + + case BS2B_MIDDLE_ECLEVEL: /* Middle easy crossfeed level */ + Fc_lo = 500.0f; + Fc_hi = 689.0f; + G_lo = 0.354813389233575f; + G_hi = 0.187169483835901f; + break; + + default: /* High easy crossfeed level */ + bs2b->level = BS2B_HIGH_ECLEVEL; + + Fc_lo = 700.0f; + Fc_hi = 975.0f; + G_lo = 0.398107170553497f; + G_hi = 0.205671765275719f; + break; + } /* switch */ + + g = 1.0f / (1.0f - G_hi + G_lo); + + /* $fc = $Fc / $s; + * $d = 1 / 2 / pi / $fc; + * $x = exp(-1 / $d); + */ + x = std::exp(-al::MathDefs<float>::Tau() * Fc_lo / bs2b->srate); + bs2b->b1_lo = x; + bs2b->a0_lo = G_lo * (1.0f - x) * g; + + x = std::exp(-al::MathDefs<float>::Tau() * Fc_hi / bs2b->srate); + bs2b->b1_hi = x; + bs2b->a0_hi = (1.0f - G_hi * (1.0f - x)) * g; + bs2b->a1_hi = -x * g; +} /* init */ + + +/* Exported functions. + * See descriptions in "bs2b.h" + */ + +void bs2b_set_params(struct bs2b *bs2b, int level, int srate) +{ + if(srate <= 0) srate = 1; + + bs2b->level = level; + bs2b->srate = srate; + init(bs2b); +} /* bs2b_set_params */ + +int bs2b_get_level(struct bs2b *bs2b) +{ + return bs2b->level; +} /* bs2b_get_level */ + +int bs2b_get_srate(struct bs2b *bs2b) +{ + return bs2b->srate; +} /* bs2b_get_srate */ + +void bs2b_clear(struct bs2b *bs2b) +{ + std::fill(std::begin(bs2b->last_sample), std::end(bs2b->last_sample), bs2b::t_last_sample{}); +} /* bs2b_clear */ + +void bs2b_cross_feed(struct bs2b *bs2b, float *RESTRICT Left, float *RESTRICT Right, int SamplesToDo) +{ + float lsamples[128][2]; + float rsamples[128][2]; + int base; + + for(base = 0;base < SamplesToDo;) + { + int todo = std::min(128, SamplesToDo-base); + int i; + + /* Process left input */ + lsamples[0][0] = bs2b->a0_lo*Left[0] + + bs2b->b1_lo*bs2b->last_sample[0].lo; + lsamples[0][1] = bs2b->a0_hi*Left[0] + + bs2b->a1_hi*bs2b->last_sample[0].asis + + bs2b->b1_hi*bs2b->last_sample[0].hi; + for(i = 1;i < todo;i++) + { + lsamples[i][0] = bs2b->a0_lo*Left[i] + + bs2b->b1_lo*lsamples[i-1][0]; + lsamples[i][1] = bs2b->a0_hi*Left[i] + + bs2b->a1_hi*Left[i-1] + + bs2b->b1_hi*lsamples[i-1][1]; + } + bs2b->last_sample[0].asis = Left[i-1]; + bs2b->last_sample[0].lo = lsamples[i-1][0]; + bs2b->last_sample[0].hi = lsamples[i-1][1]; + + /* Process right input */ + rsamples[0][0] = bs2b->a0_lo*Right[0] + + bs2b->b1_lo*bs2b->last_sample[1].lo; + rsamples[0][1] = bs2b->a0_hi*Right[0] + + bs2b->a1_hi*bs2b->last_sample[1].asis + + bs2b->b1_hi*bs2b->last_sample[1].hi; + for(i = 1;i < todo;i++) + { + rsamples[i][0] = bs2b->a0_lo*Right[i] + + bs2b->b1_lo*rsamples[i-1][0]; + rsamples[i][1] = bs2b->a0_hi*Right[i] + + bs2b->a1_hi*Right[i-1] + + bs2b->b1_hi*rsamples[i-1][1]; + } + bs2b->last_sample[1].asis = Right[i-1]; + bs2b->last_sample[1].lo = rsamples[i-1][0]; + bs2b->last_sample[1].hi = rsamples[i-1][1]; + + /* Crossfeed */ + for(i = 0;i < todo;i++) + *(Left++) = lsamples[i][1] + rsamples[i][0]; + for(i = 0;i < todo;i++) + *(Right++) = rsamples[i][1] + lsamples[i][0]; + + base += todo; + } +} /* bs2b_cross_feed */ diff --git a/alc/bs2b.h b/alc/bs2b.h new file mode 100644 index 00000000..e235e765 --- /dev/null +++ b/alc/bs2b.h @@ -0,0 +1,90 @@ +/*- + * Copyright (c) 2005 Boris Mikhaylov + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef BS2B_H +#define BS2B_H + +#include "almalloc.h" + +/* Number of crossfeed levels */ +#define BS2B_CLEVELS 3 + +/* Normal crossfeed levels */ +#define BS2B_HIGH_CLEVEL 3 +#define BS2B_MIDDLE_CLEVEL 2 +#define BS2B_LOW_CLEVEL 1 + +/* Easy crossfeed levels */ +#define BS2B_HIGH_ECLEVEL BS2B_HIGH_CLEVEL + BS2B_CLEVELS +#define BS2B_MIDDLE_ECLEVEL BS2B_MIDDLE_CLEVEL + BS2B_CLEVELS +#define BS2B_LOW_ECLEVEL BS2B_LOW_CLEVEL + BS2B_CLEVELS + +/* Default crossfeed levels */ +#define BS2B_DEFAULT_CLEVEL BS2B_HIGH_ECLEVEL +/* Default sample rate (Hz) */ +#define BS2B_DEFAULT_SRATE 44100 + +struct bs2b { + int level; /* Crossfeed level */ + int srate; /* Sample rate (Hz) */ + + /* Lowpass IIR filter coefficients */ + float a0_lo; + float b1_lo; + + /* Highboost IIR filter coefficients */ + float a0_hi; + float a1_hi; + float b1_hi; + + /* Buffer of last filtered sample. + * [0] - first channel, [1] - second channel + */ + struct t_last_sample { + float asis; + float lo; + float hi; + } last_sample[2]; + + DEF_NEWDEL(bs2b) +}; + +/* Clear buffers and set new coefficients with new crossfeed level and sample + * rate values. + * level - crossfeed level of *LEVEL values. + * srate - sample rate by Hz. + */ +void bs2b_set_params(bs2b *bs2b, int level, int srate); + +/* Return current crossfeed level value */ +int bs2b_get_level(bs2b *bs2b); + +/* Return current sample rate value */ +int bs2b_get_srate(bs2b *bs2b); + +/* Clear buffer */ +void bs2b_clear(bs2b *bs2b); + +void bs2b_cross_feed(bs2b *bs2b, float *RESTRICT Left, float *RESTRICT Right, int SamplesToDo); + +#endif /* BS2B_H */ diff --git a/alc/compat.h b/alc/compat.h new file mode 100644 index 00000000..4ffc40bf --- /dev/null +++ b/alc/compat.h @@ -0,0 +1,121 @@ +#ifndef AL_COMPAT_H +#define AL_COMPAT_H + +#ifdef __cplusplus + +#ifdef _WIN32 + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +#include <array> +#include <string> +#include <fstream> + +inline std::string wstr_to_utf8(const WCHAR *wstr) +{ + std::string ret; + + int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr); + if(len > 0) + { + ret.resize(len); + WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &ret[0], len, nullptr, nullptr); + ret.pop_back(); + } + + return ret; +} + +inline std::wstring utf8_to_wstr(const char *str) +{ + std::wstring ret; + + int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if(len > 0) + { + ret.resize(len); + MultiByteToWideChar(CP_UTF8, 0, str, -1, &ret[0], len); + ret.pop_back(); + } + + return ret; +} + + +namespace al { + +// Windows' std::ifstream fails with non-ANSI paths since the standard only +// specifies names using const char* (or std::string). MSVC has a non-standard +// extension using const wchar_t* (or std::wstring?) to handle Unicode paths, +// but not all Windows compilers support it. So we have to make our own istream +// that accepts UTF-8 paths and forwards to Unicode-aware I/O functions. +class filebuf final : public std::streambuf { + std::array<char_type,4096> mBuffer; + HANDLE mFile{INVALID_HANDLE_VALUE}; + + int_type underflow() override; + pos_type seekoff(off_type offset, std::ios_base::seekdir whence, std::ios_base::openmode mode) override; + pos_type seekpos(pos_type pos, std::ios_base::openmode mode) override; + +public: + filebuf() = default; + ~filebuf() override; + + bool open(const wchar_t *filename, std::ios_base::openmode mode); + bool open(const char *filename, std::ios_base::openmode mode); + + bool is_open() const noexcept { return mFile != INVALID_HANDLE_VALUE; } +}; + +// Inherit from std::istream to use our custom streambuf +class ifstream final : public std::istream { + filebuf mStreamBuf; + +public: + ifstream(const wchar_t *filename, std::ios_base::openmode mode = std::ios_base::in); + ifstream(const std::wstring &filename, std::ios_base::openmode mode = std::ios_base::in) + : ifstream(filename.c_str(), mode) { } + ifstream(const char *filename, std::ios_base::openmode mode = std::ios_base::in); + ifstream(const std::string &filename, std::ios_base::openmode mode = std::ios_base::in) + : ifstream(filename.c_str(), mode) { } + ~ifstream() override; + + bool is_open() const noexcept { return mStreamBuf.is_open(); } +}; + +} // namespace al + +#define HAVE_DYNLOAD 1 + +#else /* _WIN32 */ + +#include <fstream> + +namespace al { + +using filebuf = std::filebuf; +using ifstream = std::ifstream; + +} // namespace al + +#if defined(HAVE_DLFCN_H) +#define HAVE_DYNLOAD 1 +#endif + +#endif /* _WIN32 */ + +#include <string> + +struct PathNamePair { std::string path, fname; }; +const PathNamePair &GetProcBinary(void); + +#ifdef HAVE_DYNLOAD +void *LoadLib(const char *name); +void CloseLib(void *handle); +void *GetSymbol(void *handle, const char *name); +#endif + +#endif /* __cplusplus */ + +#endif /* AL_COMPAT_H */ diff --git a/alc/converter.cpp b/alc/converter.cpp new file mode 100644 index 00000000..0f8e8941 --- /dev/null +++ b/alc/converter.cpp @@ -0,0 +1,367 @@ + +#include "config.h" + +#include "converter.h" + +#include <algorithm> + +#include "fpu_modes.h" +#include "mixer/defs.h" + + +namespace { + +/* Base template left undefined. Should be marked =delete, but Clang 3.8.1 + * chokes on that given the inline specializations. + */ +template<DevFmtType T> +inline ALfloat LoadSample(typename DevFmtTypeTraits<T>::Type val) noexcept; + +template<> inline ALfloat LoadSample<DevFmtByte>(DevFmtTypeTraits<DevFmtByte>::Type val) noexcept +{ return val * (1.0f/128.0f); } +template<> inline ALfloat LoadSample<DevFmtShort>(DevFmtTypeTraits<DevFmtShort>::Type val) noexcept +{ return val * (1.0f/32768.0f); } +template<> inline ALfloat LoadSample<DevFmtInt>(DevFmtTypeTraits<DevFmtInt>::Type val) noexcept +{ return val * (1.0f/2147483648.0f); } +template<> inline ALfloat LoadSample<DevFmtFloat>(DevFmtTypeTraits<DevFmtFloat>::Type val) noexcept +{ return val; } + +template<> inline ALfloat LoadSample<DevFmtUByte>(DevFmtTypeTraits<DevFmtUByte>::Type val) noexcept +{ return LoadSample<DevFmtByte>(val - 128); } +template<> inline ALfloat LoadSample<DevFmtUShort>(DevFmtTypeTraits<DevFmtUShort>::Type val) noexcept +{ return LoadSample<DevFmtShort>(val - 32768); } +template<> inline ALfloat LoadSample<DevFmtUInt>(DevFmtTypeTraits<DevFmtUInt>::Type val) noexcept +{ return LoadSample<DevFmtInt>(val - 2147483648u); } + + +template<DevFmtType T> +inline void LoadSampleArray(ALfloat *RESTRICT dst, const void *src, const size_t srcstep, + const ALsizei samples) noexcept +{ + using SampleType = typename DevFmtTypeTraits<T>::Type; + + const SampleType *ssrc = static_cast<const SampleType*>(src); + for(ALsizei i{0};i < samples;i++) + dst[i] = LoadSample<T>(ssrc[i*srcstep]); +} + +void LoadSamples(ALfloat *dst, const ALvoid *src, const size_t srcstep, const DevFmtType srctype, + const ALsizei samples) noexcept +{ +#define HANDLE_FMT(T) \ + case T: LoadSampleArray<T>(dst, src, srcstep, samples); break + switch(srctype) + { + HANDLE_FMT(DevFmtByte); + HANDLE_FMT(DevFmtUByte); + HANDLE_FMT(DevFmtShort); + HANDLE_FMT(DevFmtUShort); + HANDLE_FMT(DevFmtInt); + HANDLE_FMT(DevFmtUInt); + HANDLE_FMT(DevFmtFloat); + } +#undef HANDLE_FMT +} + + +template<DevFmtType T> +inline typename DevFmtTypeTraits<T>::Type StoreSample(ALfloat) noexcept; + +template<> inline ALfloat StoreSample<DevFmtFloat>(ALfloat val) noexcept +{ return val; } +template<> inline ALint StoreSample<DevFmtInt>(ALfloat val) noexcept +{ return fastf2i(clampf(val*2147483648.0f, -2147483648.0f, 2147483520.0f)); } +template<> inline ALshort StoreSample<DevFmtShort>(ALfloat val) noexcept +{ return fastf2i(clampf(val*32768.0f, -32768.0f, 32767.0f)); } +template<> inline ALbyte StoreSample<DevFmtByte>(ALfloat val) noexcept +{ return fastf2i(clampf(val*128.0f, -128.0f, 127.0f)); } + +/* Define unsigned output variations. */ +template<> inline ALuint StoreSample<DevFmtUInt>(ALfloat val) noexcept +{ return StoreSample<DevFmtInt>(val) + 2147483648u; } +template<> inline ALushort StoreSample<DevFmtUShort>(ALfloat val) noexcept +{ return StoreSample<DevFmtShort>(val) + 32768; } +template<> inline ALubyte StoreSample<DevFmtUByte>(ALfloat val) noexcept +{ return StoreSample<DevFmtByte>(val) + 128; } + +template<DevFmtType T> +inline void StoreSampleArray(void *dst, const ALfloat *RESTRICT src, const size_t dststep, + const ALsizei samples) noexcept +{ + using SampleType = typename DevFmtTypeTraits<T>::Type; + + SampleType *sdst = static_cast<SampleType*>(dst); + for(ALsizei i{0};i < samples;i++) + sdst[i*dststep] = StoreSample<T>(src[i]); +} + + +void StoreSamples(ALvoid *dst, const ALfloat *src, const size_t dststep, const DevFmtType dsttype, + const ALsizei samples) noexcept +{ +#define HANDLE_FMT(T) \ + case T: StoreSampleArray<T>(dst, src, dststep, samples); break + switch(dsttype) + { + HANDLE_FMT(DevFmtByte); + HANDLE_FMT(DevFmtUByte); + HANDLE_FMT(DevFmtShort); + HANDLE_FMT(DevFmtUShort); + HANDLE_FMT(DevFmtInt); + HANDLE_FMT(DevFmtUInt); + HANDLE_FMT(DevFmtFloat); + } +#undef HANDLE_FMT +} + + +template<DevFmtType T> +void Mono2Stereo(ALfloat *RESTRICT dst, const void *src, const ALsizei frames) noexcept +{ + using SampleType = typename DevFmtTypeTraits<T>::Type; + + const SampleType *ssrc = static_cast<const SampleType*>(src); + for(ALsizei i{0};i < frames;i++) + dst[i*2 + 1] = dst[i*2 + 0] = LoadSample<T>(ssrc[i]) * 0.707106781187f; +} + +template<DevFmtType T> +void Stereo2Mono(ALfloat *RESTRICT dst, const void *src, const ALsizei frames) noexcept +{ + using SampleType = typename DevFmtTypeTraits<T>::Type; + + const SampleType *ssrc = static_cast<const SampleType*>(src); + for(ALsizei i{0};i < frames;i++) + dst[i] = (LoadSample<T>(ssrc[i*2 + 0])+LoadSample<T>(ssrc[i*2 + 1])) * + 0.707106781187f; +} + +} // namespace + +SampleConverterPtr CreateSampleConverter(DevFmtType srcType, DevFmtType dstType, ALsizei numchans, + ALsizei srcRate, ALsizei dstRate, Resampler resampler) +{ + if(numchans <= 0 || srcRate <= 0 || dstRate <= 0) + return nullptr; + + void *ptr{al_calloc(16, SampleConverter::Sizeof(numchans))}; + SampleConverterPtr converter{new (ptr) SampleConverter{static_cast<size_t>(numchans)}}; + converter->mSrcType = srcType; + converter->mDstType = dstType; + converter->mSrcTypeSize = BytesFromDevFmt(srcType); + converter->mDstTypeSize = BytesFromDevFmt(dstType); + + converter->mSrcPrepCount = 0; + converter->mFracOffset = 0; + + /* Have to set the mixer FPU mode since that's what the resampler code expects. */ + FPUCtl mixer_mode{}; + auto step = static_cast<ALsizei>( + mind(static_cast<ALdouble>(srcRate)/dstRate*FRACTIONONE + 0.5, MAX_PITCH*FRACTIONONE)); + converter->mIncrement = maxi(step, 1); + if(converter->mIncrement == FRACTIONONE) + converter->mResample = Resample_<CopyTag,CTag>; + else + { + if(resampler == BSinc24Resampler) + BsincPrepare(converter->mIncrement, &converter->mState.bsinc, &bsinc24); + else if(resampler == BSinc12Resampler) + BsincPrepare(converter->mIncrement, &converter->mState.bsinc, &bsinc12); + converter->mResample = SelectResampler(resampler); + } + + return converter; +} + +ALsizei SampleConverter::availableOut(ALsizei srcframes) const +{ + ALint prepcount{mSrcPrepCount}; + if(prepcount < 0) + { + /* Negative prepcount means we need to skip that many input samples. */ + if(-prepcount >= srcframes) + return 0; + srcframes += prepcount; + prepcount = 0; + } + + if(srcframes < 1) + { + /* No output samples if there's no input samples. */ + return 0; + } + + if(prepcount < MAX_RESAMPLE_PADDING*2 && + MAX_RESAMPLE_PADDING*2 - prepcount >= srcframes) + { + /* Not enough input samples to generate an output sample. */ + return 0; + } + + auto DataSize64 = static_cast<uint64_t>(prepcount); + DataSize64 += srcframes; + DataSize64 -= MAX_RESAMPLE_PADDING*2; + DataSize64 <<= FRACTIONBITS; + DataSize64 -= mFracOffset; + + /* If we have a full prep, we can generate at least one sample. */ + return static_cast<ALsizei>(clampu64((DataSize64 + mIncrement-1)/mIncrement, 1, BUFFERSIZE)); +} + +ALsizei SampleConverter::convert(const ALvoid **src, ALsizei *srcframes, ALvoid *dst, ALsizei dstframes) +{ + const ALsizei SrcFrameSize{static_cast<ALsizei>(mChan.size()) * mSrcTypeSize}; + const ALsizei DstFrameSize{static_cast<ALsizei>(mChan.size()) * mDstTypeSize}; + const ALsizei increment{mIncrement}; + auto SamplesIn = static_cast<const al::byte*>(*src); + ALsizei NumSrcSamples{*srcframes}; + + FPUCtl mixer_mode{}; + ALsizei pos{0}; + while(pos < dstframes && NumSrcSamples > 0) + { + ALint prepcount{mSrcPrepCount}; + if(prepcount < 0) + { + /* Negative prepcount means we need to skip that many input samples. */ + if(-prepcount >= NumSrcSamples) + { + mSrcPrepCount = prepcount + NumSrcSamples; + NumSrcSamples = 0; + break; + } + SamplesIn += SrcFrameSize*-prepcount; + NumSrcSamples += prepcount; + mSrcPrepCount = 0; + continue; + } + ALint toread{mini(NumSrcSamples, BUFFERSIZE - MAX_RESAMPLE_PADDING*2)}; + + if(prepcount < MAX_RESAMPLE_PADDING*2 && + MAX_RESAMPLE_PADDING*2 - prepcount >= toread) + { + /* Not enough input samples to generate an output sample. Store + * what we're given for later. + */ + for(size_t chan{0u};chan < mChan.size();chan++) + LoadSamples(&mChan[chan].PrevSamples[prepcount], SamplesIn + mSrcTypeSize*chan, + mChan.size(), mSrcType, toread); + + mSrcPrepCount = prepcount + toread; + NumSrcSamples = 0; + break; + } + + ALfloat *RESTRICT SrcData{mSrcSamples}; + ALfloat *RESTRICT DstData{mDstSamples}; + ALsizei DataPosFrac{mFracOffset}; + auto DataSize64 = static_cast<uint64_t>(prepcount); + DataSize64 += toread; + DataSize64 -= MAX_RESAMPLE_PADDING*2; + DataSize64 <<= FRACTIONBITS; + DataSize64 -= DataPosFrac; + + /* If we have a full prep, we can generate at least one sample. */ + auto DstSize = static_cast<ALsizei>( + clampu64((DataSize64 + increment-1)/increment, 1, BUFFERSIZE)); + DstSize = mini(DstSize, dstframes-pos); + + for(size_t chan{0u};chan < mChan.size();chan++) + { + const al::byte *SrcSamples{SamplesIn + mSrcTypeSize*chan}; + al::byte *DstSamples = static_cast<al::byte*>(dst) + mDstTypeSize*chan; + + /* Load the previous samples into the source data first, then the + * new samples from the input buffer. + */ + std::copy_n(mChan[chan].PrevSamples, prepcount, SrcData); + LoadSamples(SrcData + prepcount, SrcSamples, mChan.size(), mSrcType, toread); + + /* Store as many prep samples for next time as possible, given the + * number of output samples being generated. + */ + ALsizei SrcDataEnd{(DstSize*increment + DataPosFrac)>>FRACTIONBITS}; + if(SrcDataEnd >= prepcount+toread) + std::fill(std::begin(mChan[chan].PrevSamples), + std::end(mChan[chan].PrevSamples), 0.0f); + else + { + size_t len = mini(MAX_RESAMPLE_PADDING*2, prepcount+toread-SrcDataEnd); + std::copy_n(SrcData+SrcDataEnd, len, mChan[chan].PrevSamples); + std::fill(std::begin(mChan[chan].PrevSamples)+len, + std::end(mChan[chan].PrevSamples), 0.0f); + } + + /* Now resample, and store the result in the output buffer. */ + const ALfloat *ResampledData{mResample(&mState, SrcData+MAX_RESAMPLE_PADDING, + DataPosFrac, increment, DstData, DstSize)}; + + StoreSamples(DstSamples, ResampledData, mChan.size(), mDstType, DstSize); + } + + /* Update the number of prep samples still available, as well as the + * fractional offset. + */ + DataPosFrac += increment*DstSize; + mSrcPrepCount = mini(prepcount + toread - (DataPosFrac>>FRACTIONBITS), + MAX_RESAMPLE_PADDING*2); + mFracOffset = DataPosFrac & FRACTIONMASK; + + /* Update the src and dst pointers in case there's still more to do. */ + SamplesIn += SrcFrameSize*(DataPosFrac>>FRACTIONBITS); + NumSrcSamples -= mini(NumSrcSamples, (DataPosFrac>>FRACTIONBITS)); + + dst = static_cast<al::byte*>(dst) + DstFrameSize*DstSize; + pos += DstSize; + } + + *src = SamplesIn; + *srcframes = NumSrcSamples; + + return pos; +} + + +ChannelConverterPtr CreateChannelConverter(DevFmtType srcType, DevFmtChannels srcChans, DevFmtChannels dstChans) +{ + if(srcChans != dstChans && !((srcChans == DevFmtMono && dstChans == DevFmtStereo) || + (srcChans == DevFmtStereo && dstChans == DevFmtMono))) + return nullptr; + return al::make_unique<ChannelConverter>(srcType, srcChans, dstChans); +} + +void ChannelConverter::convert(const ALvoid *src, ALfloat *dst, ALsizei frames) const +{ + if(mSrcChans == DevFmtStereo && mDstChans == DevFmtMono) + { + switch(mSrcType) + { +#define HANDLE_FMT(T) case T: Stereo2Mono<T>(dst, src, frames); break + HANDLE_FMT(DevFmtByte); + HANDLE_FMT(DevFmtUByte); + HANDLE_FMT(DevFmtShort); + HANDLE_FMT(DevFmtUShort); + HANDLE_FMT(DevFmtInt); + HANDLE_FMT(DevFmtUInt); + HANDLE_FMT(DevFmtFloat); +#undef HANDLE_FMT + } + } + else if(mSrcChans == DevFmtMono && mDstChans == DevFmtStereo) + { + switch(mSrcType) + { +#define HANDLE_FMT(T) case T: Mono2Stereo<T>(dst, src, frames); break + HANDLE_FMT(DevFmtByte); + HANDLE_FMT(DevFmtUByte); + HANDLE_FMT(DevFmtShort); + HANDLE_FMT(DevFmtUShort); + HANDLE_FMT(DevFmtInt); + HANDLE_FMT(DevFmtUInt); + HANDLE_FMT(DevFmtFloat); +#undef HANDLE_FMT + } + } + else + LoadSamples(dst, src, 1u, mSrcType, frames*ChannelsFromDevFmt(mSrcChans, 0)); +} diff --git a/alc/converter.h b/alc/converter.h new file mode 100644 index 00000000..033e4d3f --- /dev/null +++ b/alc/converter.h @@ -0,0 +1,70 @@ +#ifndef CONVERTER_H +#define CONVERTER_H + +#include <memory> + +#include "alcmain.h" +#include "alu.h" +#include "almalloc.h" + +struct SampleConverter { + DevFmtType mSrcType{}; + DevFmtType mDstType{}; + ALsizei mSrcTypeSize{}; + ALsizei mDstTypeSize{}; + + ALint mSrcPrepCount{}; + + ALsizei mFracOffset{}; + ALsizei mIncrement{}; + InterpState mState{}; + ResamplerFunc mResample{}; + + alignas(16) ALfloat mSrcSamples[BUFFERSIZE]{}; + alignas(16) ALfloat mDstSamples[BUFFERSIZE]{}; + + struct ChanSamples { + alignas(16) ALfloat PrevSamples[MAX_RESAMPLE_PADDING*2]; + }; + al::FlexArray<ChanSamples> mChan; + + SampleConverter(size_t numchans) : mChan{numchans} { } + SampleConverter(const SampleConverter&) = delete; + SampleConverter& operator=(const SampleConverter&) = delete; + + ALsizei convert(const ALvoid **src, ALsizei *srcframes, ALvoid *dst, ALsizei dstframes); + ALsizei availableOut(ALsizei srcframes) const; + + static constexpr size_t Sizeof(size_t length) noexcept + { + return maxz(sizeof(SampleConverter), + al::FlexArray<ChanSamples>::Sizeof(length, offsetof(SampleConverter, mChan))); + } + + DEF_PLACE_NEWDEL() +}; +using SampleConverterPtr = std::unique_ptr<SampleConverter>; + +SampleConverterPtr CreateSampleConverter(DevFmtType srcType, DevFmtType dstType, ALsizei numchans, + ALsizei srcRate, ALsizei dstRate, Resampler resampler); + + +struct ChannelConverter { + DevFmtType mSrcType; + DevFmtChannels mSrcChans; + DevFmtChannels mDstChans; + + ChannelConverter(DevFmtType srctype, DevFmtChannels srcchans, DevFmtChannels dstchans) + : mSrcType(srctype), mSrcChans(srcchans), mDstChans(dstchans) + { } + + void convert(const ALvoid *src, ALfloat *dst, ALsizei frames) const; + + DEF_NEWDEL(ChannelConverter) +}; +using ChannelConverterPtr = std::unique_ptr<ChannelConverter>; + +ChannelConverterPtr CreateChannelConverter(DevFmtType srcType, DevFmtChannels srcChans, + DevFmtChannels dstChans); + +#endif /* CONVERTER_H */ diff --git a/alc/cpu_caps.h b/alc/cpu_caps.h new file mode 100644 index 00000000..64a4ee45 --- /dev/null +++ b/alc/cpu_caps.h @@ -0,0 +1,16 @@ +#ifndef CPU_CAPS_H +#define CPU_CAPS_H + + +extern int CPUCapFlags; +enum { + CPU_CAP_SSE = 1<<0, + CPU_CAP_SSE2 = 1<<1, + CPU_CAP_SSE3 = 1<<2, + CPU_CAP_SSE4_1 = 1<<3, + CPU_CAP_NEON = 1<<4, +}; + +void FillCPUCaps(int capfilter); + +#endif /* CPU_CAPS_H */ diff --git a/alc/effects/autowah.cpp b/alc/effects/autowah.cpp new file mode 100644 index 00000000..96292636 --- /dev/null +++ b/alc/effects/autowah.cpp @@ -0,0 +1,298 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2018 by Raul Herraiz. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cmath> +#include <cstdlib> + +#include <algorithm> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" +#include "filters/biquad.h" +#include "vecmat.h" + +namespace { + +#define MIN_FREQ 20.0f +#define MAX_FREQ 2500.0f +#define Q_FACTOR 5.0f + +struct ALautowahState final : public EffectState { + /* Effect parameters */ + ALfloat mAttackRate; + ALfloat mReleaseRate; + ALfloat mResonanceGain; + ALfloat mPeakGain; + ALfloat mFreqMinNorm; + ALfloat mBandwidthNorm; + ALfloat mEnvDelay; + + /* Filter components derived from the envelope. */ + struct { + ALfloat cos_w0; + ALfloat alpha; + } mEnv[BUFFERSIZE]; + + struct { + /* Effect filters' history. */ + struct { + ALfloat z1, z2; + } Filter; + + /* Effect gains for each output channel */ + ALfloat CurrentGains[MAX_OUTPUT_CHANNELS]; + ALfloat TargetGains[MAX_OUTPUT_CHANNELS]; + } mChans[MAX_AMBI_CHANNELS]; + + /* Effects buffers */ + alignas(16) ALfloat mBufferOut[BUFFERSIZE]; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(ALautowahState) +}; + +ALboolean ALautowahState::deviceUpdate(const ALCdevice*) +{ + /* (Re-)initializing parameters and clear the buffers. */ + + mAttackRate = 1.0f; + mReleaseRate = 1.0f; + mResonanceGain = 10.0f; + mPeakGain = 4.5f; + mFreqMinNorm = 4.5e-4f; + mBandwidthNorm = 0.05f; + mEnvDelay = 0.0f; + + for(auto &e : mEnv) + { + e.cos_w0 = 0.0f; + e.alpha = 0.0f; + } + + for(auto &chan : mChans) + { + std::fill(std::begin(chan.CurrentGains), std::end(chan.CurrentGains), 0.0f); + chan.Filter.z1 = 0.0f; + chan.Filter.z2 = 0.0f; + } + + return AL_TRUE; +} + +void ALautowahState::update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *device{context->Device}; + + const ALfloat ReleaseTime{clampf(props->Autowah.ReleaseTime, 0.001f, 1.0f)}; + + mAttackRate = expf(-1.0f / (props->Autowah.AttackTime*device->Frequency)); + mReleaseRate = expf(-1.0f / (ReleaseTime*device->Frequency)); + /* 0-20dB Resonance Peak gain */ + mResonanceGain = std::sqrt(std::log10(props->Autowah.Resonance)*10.0f / 3.0f); + mPeakGain = 1.0f - std::log10(props->Autowah.PeakGain/AL_AUTOWAH_MAX_PEAK_GAIN); + mFreqMinNorm = MIN_FREQ / device->Frequency; + mBandwidthNorm = (MAX_FREQ-MIN_FREQ) / device->Frequency; + + mOutTarget = target.Main->Buffer; + for(size_t i{0u};i < slot->Wet.Buffer.size();++i) + { + auto coeffs = GetAmbiIdentityRow(i); + ComputePanGains(target.Main, coeffs.data(), slot->Params.Gain, mChans[i].TargetGains); + } +} + +void ALautowahState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) +{ + const ALfloat attack_rate = mAttackRate; + const ALfloat release_rate = mReleaseRate; + const ALfloat res_gain = mResonanceGain; + const ALfloat peak_gain = mPeakGain; + const ALfloat freq_min = mFreqMinNorm; + const ALfloat bandwidth = mBandwidthNorm; + + ALfloat env_delay{mEnvDelay}; + for(ALsizei i{0};i < samplesToDo;i++) + { + ALfloat w0, sample, a; + + /* Envelope follower described on the book: Audio Effects, Theory, + * Implementation and Application. + */ + sample = peak_gain * std::fabs(samplesIn[0][i]); + a = (sample > env_delay) ? attack_rate : release_rate; + env_delay = lerp(sample, env_delay, a); + + /* Calculate the cos and alpha components for this sample's filter. */ + w0 = minf((bandwidth*env_delay + freq_min), 0.46f) * al::MathDefs<float>::Tau(); + mEnv[i].cos_w0 = cosf(w0); + mEnv[i].alpha = sinf(w0)/(2.0f * Q_FACTOR); + } + mEnvDelay = env_delay; + + ASSUME(numInput > 0); + for(ALsizei c{0};c < numInput;++c) + { + /* This effectively inlines BiquadFilter_setParams for a peaking + * filter and BiquadFilter_processC. The alpha and cosine components + * for the filter coefficients were previously calculated with the + * envelope. Because the filter changes for each sample, the + * coefficients are transient and don't need to be held. + */ + ALfloat z1{mChans[c].Filter.z1}; + ALfloat z2{mChans[c].Filter.z2}; + + for(ALsizei i{0};i < samplesToDo;i++) + { + const ALfloat alpha = mEnv[i].alpha; + const ALfloat cos_w0 = mEnv[i].cos_w0; + ALfloat input, output; + ALfloat a[3], b[3]; + + b[0] = 1.0f + alpha*res_gain; + b[1] = -2.0f * cos_w0; + b[2] = 1.0f - alpha*res_gain; + a[0] = 1.0f + alpha/res_gain; + a[1] = -2.0f * cos_w0; + a[2] = 1.0f - alpha/res_gain; + + input = samplesIn[c][i]; + output = input*(b[0]/a[0]) + z1; + z1 = input*(b[1]/a[0]) - output*(a[1]/a[0]) + z2; + z2 = input*(b[2]/a[0]) - output*(a[2]/a[0]); + mBufferOut[i] = output; + } + mChans[c].Filter.z1 = z1; + mChans[c].Filter.z2 = z2; + + /* Now, mix the processed sound data to the output. */ + MixSamples(mBufferOut, samplesOut, mChans[c].CurrentGains, mChans[c].TargetGains, + samplesToDo, 0, samplesToDo); + } +} + + +void ALautowah_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_AUTOWAH_ATTACK_TIME: + if(!(val >= AL_AUTOWAH_MIN_ATTACK_TIME && val <= AL_AUTOWAH_MAX_ATTACK_TIME)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Autowah attack time out of range"); + props->Autowah.AttackTime = val; + break; + + case AL_AUTOWAH_RELEASE_TIME: + if(!(val >= AL_AUTOWAH_MIN_RELEASE_TIME && val <= AL_AUTOWAH_MAX_RELEASE_TIME)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Autowah release time out of range"); + props->Autowah.ReleaseTime = val; + break; + + case AL_AUTOWAH_RESONANCE: + if(!(val >= AL_AUTOWAH_MIN_RESONANCE && val <= AL_AUTOWAH_MAX_RESONANCE)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Autowah resonance out of range"); + props->Autowah.Resonance = val; + break; + + case AL_AUTOWAH_PEAK_GAIN: + if(!(val >= AL_AUTOWAH_MIN_PEAK_GAIN && val <= AL_AUTOWAH_MAX_PEAK_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Autowah peak gain out of range"); + props->Autowah.PeakGain = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid autowah float property 0x%04x", param); + } +} +void ALautowah_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ ALautowah_setParamf(props, context, param, vals[0]); } + +void ALautowah_setParami(EffectProps*, ALCcontext *context, ALenum param, ALint) +{ alSetError(context, AL_INVALID_ENUM, "Invalid autowah integer property 0x%04x", param); } +void ALautowah_setParamiv(EffectProps*, ALCcontext *context, ALenum param, const ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid autowah integer vector property 0x%04x", param); } + +void ALautowah_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_AUTOWAH_ATTACK_TIME: + *val = props->Autowah.AttackTime; + break; + + case AL_AUTOWAH_RELEASE_TIME: + *val = props->Autowah.ReleaseTime; + break; + + case AL_AUTOWAH_RESONANCE: + *val = props->Autowah.Resonance; + break; + + case AL_AUTOWAH_PEAK_GAIN: + *val = props->Autowah.PeakGain; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid autowah float property 0x%04x", param); + } + +} +void ALautowah_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ ALautowah_getParamf(props, context, param, vals); } + +void ALautowah_getParami(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid autowah integer property 0x%04x", param); } +void ALautowah_getParamiv(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid autowah integer vector property 0x%04x", param); } + +DEFINE_ALEFFECT_VTABLE(ALautowah); + + +struct AutowahStateFactory final : public EffectStateFactory { + EffectState *create() override { return new ALautowahState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &ALautowah_vtable; } +}; + +EffectProps AutowahStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Autowah.AttackTime = AL_AUTOWAH_DEFAULT_ATTACK_TIME; + props.Autowah.ReleaseTime = AL_AUTOWAH_DEFAULT_RELEASE_TIME; + props.Autowah.Resonance = AL_AUTOWAH_DEFAULT_RESONANCE; + props.Autowah.PeakGain = AL_AUTOWAH_DEFAULT_PEAK_GAIN; + return props; +} + +} // namespace + +EffectStateFactory *AutowahStateFactory_getFactory() +{ + static AutowahStateFactory AutowahFactory{}; + return &AutowahFactory; +} diff --git a/alc/effects/base.h b/alc/effects/base.h new file mode 100644 index 00000000..4f48de22 --- /dev/null +++ b/alc/effects/base.h @@ -0,0 +1,196 @@ +#ifndef EFFECTS_BASE_H +#define EFFECTS_BASE_H + +#include "alcmain.h" +#include "almalloc.h" +#include "alspan.h" +#include "atomic.h" + + +struct ALeffectslot; + + +union EffectProps { + struct { + // Shared Reverb Properties + ALfloat Density; + ALfloat Diffusion; + ALfloat Gain; + ALfloat GainHF; + ALfloat DecayTime; + ALfloat DecayHFRatio; + ALfloat ReflectionsGain; + ALfloat ReflectionsDelay; + ALfloat LateReverbGain; + ALfloat LateReverbDelay; + ALfloat AirAbsorptionGainHF; + ALfloat RoomRolloffFactor; + ALboolean DecayHFLimit; + + // Additional EAX Reverb Properties + ALfloat GainLF; + ALfloat DecayLFRatio; + ALfloat ReflectionsPan[3]; + ALfloat LateReverbPan[3]; + ALfloat EchoTime; + ALfloat EchoDepth; + ALfloat ModulationTime; + ALfloat ModulationDepth; + ALfloat HFReference; + ALfloat LFReference; + } Reverb; + + struct { + ALfloat AttackTime; + ALfloat ReleaseTime; + ALfloat Resonance; + ALfloat PeakGain; + } Autowah; + + struct { + ALint Waveform; + ALint Phase; + ALfloat Rate; + ALfloat Depth; + ALfloat Feedback; + ALfloat Delay; + } Chorus; /* Also Flanger */ + + struct { + ALboolean OnOff; + } Compressor; + + struct { + ALfloat Edge; + ALfloat Gain; + ALfloat LowpassCutoff; + ALfloat EQCenter; + ALfloat EQBandwidth; + } Distortion; + + struct { + ALfloat Delay; + ALfloat LRDelay; + + ALfloat Damping; + ALfloat Feedback; + + ALfloat Spread; + } Echo; + + struct { + ALfloat LowCutoff; + ALfloat LowGain; + ALfloat Mid1Center; + ALfloat Mid1Gain; + ALfloat Mid1Width; + ALfloat Mid2Center; + ALfloat Mid2Gain; + ALfloat Mid2Width; + ALfloat HighCutoff; + ALfloat HighGain; + } Equalizer; + + struct { + ALfloat Frequency; + ALint LeftDirection; + ALint RightDirection; + } Fshifter; + + struct { + ALfloat Frequency; + ALfloat HighPassCutoff; + ALint Waveform; + } Modulator; + + struct { + ALint CoarseTune; + ALint FineTune; + } Pshifter; + + struct { + ALfloat Rate; + ALint PhonemeA; + ALint PhonemeB; + ALint PhonemeACoarseTuning; + ALint PhonemeBCoarseTuning; + ALint Waveform; + } Vmorpher; + + struct { + ALfloat Gain; + } Dedicated; +}; + + +struct EffectVtable { + void (*const setParami)(EffectProps *props, ALCcontext *context, ALenum param, ALint val); + void (*const setParamiv)(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals); + void (*const setParamf)(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val); + void (*const setParamfv)(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals); + + void (*const getParami)(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val); + void (*const getParamiv)(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals); + void (*const getParamf)(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val); + void (*const getParamfv)(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals); +}; + +#define DEFINE_ALEFFECT_VTABLE(T) \ +const EffectVtable T##_vtable = { \ + T##_setParami, T##_setParamiv, \ + T##_setParamf, T##_setParamfv, \ + T##_getParami, T##_getParamiv, \ + T##_getParamf, T##_getParamfv, \ +} + + +struct EffectTarget { + MixParams *Main; + RealMixParams *RealOut; +}; + +struct EffectState { + RefCount mRef{1u}; + + al::span<FloatBufferLine> mOutTarget; + + + virtual ~EffectState() = default; + + virtual ALboolean deviceUpdate(const ALCdevice *device) = 0; + virtual void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) = 0; + virtual void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) = 0; + + void IncRef() noexcept; + void DecRef() noexcept; +}; + + +struct EffectStateFactory { + virtual ~EffectStateFactory() { } + + virtual EffectState *create() = 0; + virtual EffectProps getDefaultProps() const noexcept = 0; + virtual const EffectVtable *getEffectVtable() const noexcept = 0; +}; + + +EffectStateFactory *NullStateFactory_getFactory(void); +EffectStateFactory *ReverbStateFactory_getFactory(void); +EffectStateFactory *StdReverbStateFactory_getFactory(void); +EffectStateFactory *AutowahStateFactory_getFactory(void); +EffectStateFactory *ChorusStateFactory_getFactory(void); +EffectStateFactory *CompressorStateFactory_getFactory(void); +EffectStateFactory *DistortionStateFactory_getFactory(void); +EffectStateFactory *EchoStateFactory_getFactory(void); +EffectStateFactory *EqualizerStateFactory_getFactory(void); +EffectStateFactory *FlangerStateFactory_getFactory(void); +EffectStateFactory *FshifterStateFactory_getFactory(void); +EffectStateFactory *ModulatorStateFactory_getFactory(void); +EffectStateFactory *PshifterStateFactory_getFactory(void); +EffectStateFactory* VmorpherStateFactory_getFactory(void); + +EffectStateFactory *DedicatedStateFactory_getFactory(void); + + +#endif /* EFFECTS_BASE_H */ diff --git a/alc/effects/chorus.cpp b/alc/effects/chorus.cpp new file mode 100644 index 00000000..d475b57a --- /dev/null +++ b/alc/effects/chorus.cpp @@ -0,0 +1,538 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2013 by Mike Gorchak + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <algorithm> +#include <climits> +#include <cmath> +#include <cstdlib> +#include <iterator> + +#include "AL/al.h" +#include "AL/alc.h" +#include "AL/efx.h" + +#include "alAuxEffectSlot.h" +#include "alcmain.h" +#include "alError.h" +#include "alcontext.h" +#include "almalloc.h" +#include "alnumeric.h" +#include "alspan.h" +#include "alu.h" +#include "ambidefs.h" +#include "effects/base.h" +#include "math_defs.h" +#include "opthelpers.h" +#include "vector.h" + + +namespace { + +static_assert(AL_CHORUS_WAVEFORM_SINUSOID == AL_FLANGER_WAVEFORM_SINUSOID, "Chorus/Flanger waveform value mismatch"); +static_assert(AL_CHORUS_WAVEFORM_TRIANGLE == AL_FLANGER_WAVEFORM_TRIANGLE, "Chorus/Flanger waveform value mismatch"); + +enum class WaveForm { + Sinusoid, + Triangle +}; + +void GetTriangleDelays(ALint *delays, const ALsizei start_offset, const ALsizei lfo_range, + const ALfloat lfo_scale, const ALfloat depth, const ALsizei delay, const ALsizei todo) +{ + ASSUME(start_offset >= 0); + ASSUME(lfo_range > 0); + ASSUME(todo > 0); + + ALsizei offset{start_offset}; + auto gen_lfo = [&offset,lfo_range,lfo_scale,depth,delay]() -> ALint + { + offset = (offset+1)%lfo_range; + return fastf2i((1.0f - std::abs(2.0f - lfo_scale*offset)) * depth) + delay; + }; + std::generate_n(delays, todo, gen_lfo); +} + +void GetSinusoidDelays(ALint *delays, const ALsizei start_offset, const ALsizei lfo_range, + const ALfloat lfo_scale, const ALfloat depth, const ALsizei delay, const ALsizei todo) +{ + ASSUME(start_offset >= 0); + ASSUME(lfo_range > 0); + ASSUME(todo > 0); + + ALsizei offset{start_offset}; + auto gen_lfo = [&offset,lfo_range,lfo_scale,depth,delay]() -> ALint + { + ASSUME(delay >= 0); + offset = (offset+1)%lfo_range; + return fastf2i(std::sin(lfo_scale*offset) * depth) + delay; + }; + std::generate_n(delays, todo, gen_lfo); +} + +struct ChorusState final : public EffectState { + al::vector<ALfloat,16> mSampleBuffer; + ALsizei mOffset{0}; + + ALsizei mLfoOffset{0}; + ALsizei mLfoRange{1}; + ALfloat mLfoScale{0.0f}; + ALint mLfoDisp{0}; + + /* Gains for left and right sides */ + struct { + ALfloat Current[MAX_OUTPUT_CHANNELS]{}; + ALfloat Target[MAX_OUTPUT_CHANNELS]{}; + } mGains[2]; + + /* effect parameters */ + WaveForm mWaveform{}; + ALint mDelay{0}; + ALfloat mDepth{0.0f}; + ALfloat mFeedback{0.0f}; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(ChorusState) +}; + +ALboolean ChorusState::deviceUpdate(const ALCdevice *Device) +{ + const ALfloat max_delay = maxf(AL_CHORUS_MAX_DELAY, AL_FLANGER_MAX_DELAY); + size_t maxlen; + + maxlen = NextPowerOf2(float2int(max_delay*2.0f*Device->Frequency) + 1u); + if(maxlen <= 0) return AL_FALSE; + + if(maxlen != mSampleBuffer.size()) + { + mSampleBuffer.resize(maxlen); + mSampleBuffer.shrink_to_fit(); + } + + std::fill(mSampleBuffer.begin(), mSampleBuffer.end(), 0.0f); + for(auto &e : mGains) + { + std::fill(std::begin(e.Current), std::end(e.Current), 0.0f); + std::fill(std::begin(e.Target), std::end(e.Target), 0.0f); + } + + return AL_TRUE; +} + +void ChorusState::update(const ALCcontext *Context, const ALeffectslot *Slot, const EffectProps *props, const EffectTarget target) +{ + static constexpr ALsizei mindelay = MAX_RESAMPLE_PADDING << FRACTIONBITS; + + switch(props->Chorus.Waveform) + { + case AL_CHORUS_WAVEFORM_TRIANGLE: + mWaveform = WaveForm::Triangle; + break; + case AL_CHORUS_WAVEFORM_SINUSOID: + mWaveform = WaveForm::Sinusoid; + break; + } + + /* The LFO depth is scaled to be relative to the sample delay. Clamp the + * delay and depth to allow enough padding for resampling. + */ + const ALCdevice *device{Context->Device}; + const auto frequency = static_cast<ALfloat>(device->Frequency); + mDelay = maxi(float2int(props->Chorus.Delay*frequency*FRACTIONONE + 0.5f), mindelay); + mDepth = minf(props->Chorus.Depth * mDelay, static_cast<ALfloat>(mDelay - mindelay)); + + mFeedback = props->Chorus.Feedback; + + /* Gains for left and right sides */ + ALfloat coeffs[2][MAX_AMBI_CHANNELS]; + CalcDirectionCoeffs({-1.0f, 0.0f, 0.0f}, 0.0f, coeffs[0]); + CalcDirectionCoeffs({ 1.0f, 0.0f, 0.0f}, 0.0f, coeffs[1]); + + mOutTarget = target.Main->Buffer; + ComputePanGains(target.Main, coeffs[0], Slot->Params.Gain, mGains[0].Target); + ComputePanGains(target.Main, coeffs[1], Slot->Params.Gain, mGains[1].Target); + + ALfloat rate{props->Chorus.Rate}; + if(!(rate > 0.0f)) + { + mLfoOffset = 0; + mLfoRange = 1; + mLfoScale = 0.0f; + mLfoDisp = 0; + } + else + { + /* Calculate LFO coefficient (number of samples per cycle). Limit the + * max range to avoid overflow when calculating the displacement. + */ + ALsizei lfo_range = float2int(minf(frequency/rate + 0.5f, static_cast<ALfloat>(INT_MAX/360 - 180))); + + mLfoOffset = float2int(static_cast<ALfloat>(mLfoOffset)/mLfoRange*lfo_range + 0.5f) % lfo_range; + mLfoRange = lfo_range; + switch(mWaveform) + { + case WaveForm::Triangle: + mLfoScale = 4.0f / mLfoRange; + break; + case WaveForm::Sinusoid: + mLfoScale = al::MathDefs<float>::Tau() / mLfoRange; + break; + } + + /* Calculate lfo phase displacement */ + ALint phase{props->Chorus.Phase}; + if(phase < 0) phase = 360 + phase; + mLfoDisp = (mLfoRange*phase + 180) / 360; + } +} + +void ChorusState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei /*numInput*/, const al::span<FloatBufferLine> samplesOut) +{ + const auto bufmask = static_cast<ALsizei>(mSampleBuffer.size()-1); + const ALfloat feedback{mFeedback}; + const ALsizei avgdelay{(mDelay + (FRACTIONONE>>1)) >> FRACTIONBITS}; + ALfloat *RESTRICT delaybuf{mSampleBuffer.data()}; + ALsizei offset{mOffset}; + + for(ALsizei base{0};base < samplesToDo;) + { + const ALsizei todo = mini(256, samplesToDo-base); + ALint moddelays[2][256]; + alignas(16) ALfloat temps[2][256]; + + if(mWaveform == WaveForm::Sinusoid) + { + GetSinusoidDelays(moddelays[0], mLfoOffset, mLfoRange, mLfoScale, mDepth, mDelay, + todo); + GetSinusoidDelays(moddelays[1], (mLfoOffset+mLfoDisp)%mLfoRange, mLfoRange, mLfoScale, + mDepth, mDelay, todo); + } + else /*if(mWaveform == WaveForm::Triangle)*/ + { + GetTriangleDelays(moddelays[0], mLfoOffset, mLfoRange, mLfoScale, mDepth, mDelay, + todo); + GetTriangleDelays(moddelays[1], (mLfoOffset+mLfoDisp)%mLfoRange, mLfoRange, mLfoScale, + mDepth, mDelay, todo); + } + mLfoOffset = (mLfoOffset+todo) % mLfoRange; + + for(ALsizei i{0};i < todo;i++) + { + // Feed the buffer's input first (necessary for delays < 1). + delaybuf[offset&bufmask] = samplesIn[0][base+i]; + + // Tap for the left output. + ALint delay{offset - (moddelays[0][i]>>FRACTIONBITS)}; + ALfloat mu{(moddelays[0][i]&FRACTIONMASK) * (1.0f/FRACTIONONE)}; + temps[0][i] = cubic(delaybuf[(delay+1) & bufmask], delaybuf[(delay ) & bufmask], + delaybuf[(delay-1) & bufmask], delaybuf[(delay-2) & bufmask], + mu); + + // Tap for the right output. + delay = offset - (moddelays[1][i]>>FRACTIONBITS); + mu = (moddelays[1][i]&FRACTIONMASK) * (1.0f/FRACTIONONE); + temps[1][i] = cubic(delaybuf[(delay+1) & bufmask], delaybuf[(delay ) & bufmask], + delaybuf[(delay-1) & bufmask], delaybuf[(delay-2) & bufmask], + mu); + + // Accumulate feedback from the average delay of the taps. + delaybuf[offset&bufmask] += delaybuf[(offset-avgdelay) & bufmask] * feedback; + offset++; + } + + for(ALsizei c{0};c < 2;c++) + MixSamples(temps[c], samplesOut, mGains[c].Current, mGains[c].Target, samplesToDo-base, + base, todo); + + base += todo; + } + + mOffset = offset; +} + + +void Chorus_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_CHORUS_WAVEFORM: + if(!(val >= AL_CHORUS_MIN_WAVEFORM && val <= AL_CHORUS_MAX_WAVEFORM)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Invalid chorus waveform"); + props->Chorus.Waveform = val; + break; + + case AL_CHORUS_PHASE: + if(!(val >= AL_CHORUS_MIN_PHASE && val <= AL_CHORUS_MAX_PHASE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Chorus phase out of range"); + props->Chorus.Phase = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid chorus integer property 0x%04x", param); + } +} +void Chorus_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ Chorus_setParami(props, context, param, vals[0]); } +void Chorus_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_CHORUS_RATE: + if(!(val >= AL_CHORUS_MIN_RATE && val <= AL_CHORUS_MAX_RATE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Chorus rate out of range"); + props->Chorus.Rate = val; + break; + + case AL_CHORUS_DEPTH: + if(!(val >= AL_CHORUS_MIN_DEPTH && val <= AL_CHORUS_MAX_DEPTH)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Chorus depth out of range"); + props->Chorus.Depth = val; + break; + + case AL_CHORUS_FEEDBACK: + if(!(val >= AL_CHORUS_MIN_FEEDBACK && val <= AL_CHORUS_MAX_FEEDBACK)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Chorus feedback out of range"); + props->Chorus.Feedback = val; + break; + + case AL_CHORUS_DELAY: + if(!(val >= AL_CHORUS_MIN_DELAY && val <= AL_CHORUS_MAX_DELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Chorus delay out of range"); + props->Chorus.Delay = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid chorus float property 0x%04x", param); + } +} +void Chorus_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Chorus_setParamf(props, context, param, vals[0]); } + +void Chorus_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_CHORUS_WAVEFORM: + *val = props->Chorus.Waveform; + break; + + case AL_CHORUS_PHASE: + *val = props->Chorus.Phase; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid chorus integer property 0x%04x", param); + } +} +void Chorus_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ Chorus_getParami(props, context, param, vals); } +void Chorus_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_CHORUS_RATE: + *val = props->Chorus.Rate; + break; + + case AL_CHORUS_DEPTH: + *val = props->Chorus.Depth; + break; + + case AL_CHORUS_FEEDBACK: + *val = props->Chorus.Feedback; + break; + + case AL_CHORUS_DELAY: + *val = props->Chorus.Delay; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid chorus float property 0x%04x", param); + } +} +void Chorus_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Chorus_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Chorus); + + +struct ChorusStateFactory final : public EffectStateFactory { + EffectState *create() override { return new ChorusState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Chorus_vtable; } +}; + +EffectProps ChorusStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Chorus.Waveform = AL_CHORUS_DEFAULT_WAVEFORM; + props.Chorus.Phase = AL_CHORUS_DEFAULT_PHASE; + props.Chorus.Rate = AL_CHORUS_DEFAULT_RATE; + props.Chorus.Depth = AL_CHORUS_DEFAULT_DEPTH; + props.Chorus.Feedback = AL_CHORUS_DEFAULT_FEEDBACK; + props.Chorus.Delay = AL_CHORUS_DEFAULT_DELAY; + return props; +} + + +void Flanger_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_FLANGER_WAVEFORM: + if(!(val >= AL_FLANGER_MIN_WAVEFORM && val <= AL_FLANGER_MAX_WAVEFORM)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Invalid flanger waveform"); + props->Chorus.Waveform = val; + break; + + case AL_FLANGER_PHASE: + if(!(val >= AL_FLANGER_MIN_PHASE && val <= AL_FLANGER_MAX_PHASE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Flanger phase out of range"); + props->Chorus.Phase = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid flanger integer property 0x%04x", param); + } +} +void Flanger_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ Flanger_setParami(props, context, param, vals[0]); } +void Flanger_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_FLANGER_RATE: + if(!(val >= AL_FLANGER_MIN_RATE && val <= AL_FLANGER_MAX_RATE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Flanger rate out of range"); + props->Chorus.Rate = val; + break; + + case AL_FLANGER_DEPTH: + if(!(val >= AL_FLANGER_MIN_DEPTH && val <= AL_FLANGER_MAX_DEPTH)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Flanger depth out of range"); + props->Chorus.Depth = val; + break; + + case AL_FLANGER_FEEDBACK: + if(!(val >= AL_FLANGER_MIN_FEEDBACK && val <= AL_FLANGER_MAX_FEEDBACK)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Flanger feedback out of range"); + props->Chorus.Feedback = val; + break; + + case AL_FLANGER_DELAY: + if(!(val >= AL_FLANGER_MIN_DELAY && val <= AL_FLANGER_MAX_DELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Flanger delay out of range"); + props->Chorus.Delay = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid flanger float property 0x%04x", param); + } +} +void Flanger_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Flanger_setParamf(props, context, param, vals[0]); } + +void Flanger_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_FLANGER_WAVEFORM: + *val = props->Chorus.Waveform; + break; + + case AL_FLANGER_PHASE: + *val = props->Chorus.Phase; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid flanger integer property 0x%04x", param); + } +} +void Flanger_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ Flanger_getParami(props, context, param, vals); } +void Flanger_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_FLANGER_RATE: + *val = props->Chorus.Rate; + break; + + case AL_FLANGER_DEPTH: + *val = props->Chorus.Depth; + break; + + case AL_FLANGER_FEEDBACK: + *val = props->Chorus.Feedback; + break; + + case AL_FLANGER_DELAY: + *val = props->Chorus.Delay; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid flanger float property 0x%04x", param); + } +} +void Flanger_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Flanger_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Flanger); + + +/* Flanger is basically a chorus with a really short delay. They can both use + * the same processing functions, so piggyback flanger on the chorus functions. + */ +struct FlangerStateFactory final : public EffectStateFactory { + EffectState *create() override { return new ChorusState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Flanger_vtable; } +}; + +EffectProps FlangerStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Chorus.Waveform = AL_FLANGER_DEFAULT_WAVEFORM; + props.Chorus.Phase = AL_FLANGER_DEFAULT_PHASE; + props.Chorus.Rate = AL_FLANGER_DEFAULT_RATE; + props.Chorus.Depth = AL_FLANGER_DEFAULT_DEPTH; + props.Chorus.Feedback = AL_FLANGER_DEFAULT_FEEDBACK; + props.Chorus.Delay = AL_FLANGER_DEFAULT_DELAY; + return props; +} + +} // namespace + +EffectStateFactory *ChorusStateFactory_getFactory() +{ + static ChorusStateFactory ChorusFactory{}; + return &ChorusFactory; +} + +EffectStateFactory *FlangerStateFactory_getFactory() +{ + static FlangerStateFactory FlangerFactory{}; + return &FlangerFactory; +} diff --git a/alc/effects/compressor.cpp b/alc/effects/compressor.cpp new file mode 100644 index 00000000..4a487097 --- /dev/null +++ b/alc/effects/compressor.cpp @@ -0,0 +1,222 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2013 by Anis A. Hireche + * 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 <cstdlib> + +#include "alcmain.h" +#include "alcontext.h" +#include "alu.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "vecmat.h" + + +namespace { + +#define AMP_ENVELOPE_MIN 0.5f +#define AMP_ENVELOPE_MAX 2.0f + +#define ATTACK_TIME 0.1f /* 100ms to rise from min to max */ +#define RELEASE_TIME 0.2f /* 200ms to drop from max to min */ + + +struct CompressorState final : public EffectState { + /* Effect gains for each channel */ + ALfloat mGain[MAX_AMBI_CHANNELS][MAX_OUTPUT_CHANNELS]{}; + + /* Effect parameters */ + ALboolean mEnabled{AL_TRUE}; + ALfloat mAttackMult{1.0f}; + ALfloat mReleaseMult{1.0f}; + ALfloat mEnvFollower{1.0f}; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(CompressorState) +}; + +ALboolean CompressorState::deviceUpdate(const ALCdevice *device) +{ + /* Number of samples to do a full attack and release (non-integer sample + * counts are okay). + */ + const ALfloat attackCount = static_cast<ALfloat>(device->Frequency) * ATTACK_TIME; + const ALfloat releaseCount = static_cast<ALfloat>(device->Frequency) * RELEASE_TIME; + + /* Calculate per-sample multipliers to attack and release at the desired + * rates. + */ + mAttackMult = std::pow(AMP_ENVELOPE_MAX/AMP_ENVELOPE_MIN, 1.0f/attackCount); + mReleaseMult = std::pow(AMP_ENVELOPE_MIN/AMP_ENVELOPE_MAX, 1.0f/releaseCount); + + return AL_TRUE; +} + +void CompressorState::update(const ALCcontext*, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + mEnabled = props->Compressor.OnOff; + + mOutTarget = target.Main->Buffer; + for(size_t i{0u};i < slot->Wet.Buffer.size();++i) + { + auto coeffs = GetAmbiIdentityRow(i); + ComputePanGains(target.Main, coeffs.data(), slot->Params.Gain, mGain[i]); + } +} + +void CompressorState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) +{ + for(ALsizei base{0};base < samplesToDo;) + { + ALfloat gains[256]; + const ALsizei td{mini(256, samplesToDo-base)}; + + /* Generate the per-sample gains from the signal envelope. */ + ALfloat env{mEnvFollower}; + if(mEnabled) + { + for(ALsizei i{0};i < td;++i) + { + /* Clamp the absolute amplitude to the defined envelope limits, + * then attack or release the envelope to reach it. + */ + const ALfloat amplitude{clampf(std::fabs(samplesIn[0][base+i]), AMP_ENVELOPE_MIN, + AMP_ENVELOPE_MAX)}; + if(amplitude > env) + env = minf(env*mAttackMult, amplitude); + else if(amplitude < env) + env = maxf(env*mReleaseMult, amplitude); + + /* Apply the reciprocal of the envelope to normalize the volume + * (compress the dynamic range). + */ + gains[i] = 1.0f / env; + } + } + else + { + /* Same as above, except the amplitude is forced to 1. This helps + * ensure smooth gain changes when the compressor is turned on and + * off. + */ + for(ALsizei i{0};i < td;++i) + { + const ALfloat amplitude{1.0f}; + if(amplitude > env) + env = minf(env*mAttackMult, amplitude); + else if(amplitude < env) + env = maxf(env*mReleaseMult, amplitude); + + gains[i] = 1.0f / env; + } + } + mEnvFollower = env; + + /* Now compress the signal amplitude to output. */ + ASSUME(numInput > 0); + for(ALsizei j{0};j < numInput;j++) + { + const ALfloat *outgains{mGain[j]}; + for(FloatBufferLine &output : samplesOut) + { + const ALfloat gain{*(outgains++)}; + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + + for(ALsizei i{0};i < td;i++) + output[base+i] += samplesIn[j][base+i] * gains[i] * gain; + } + } + + base += td; + } +} + + +void Compressor_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_COMPRESSOR_ONOFF: + if(!(val >= AL_COMPRESSOR_MIN_ONOFF && val <= AL_COMPRESSOR_MAX_ONOFF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Compressor state out of range"); + props->Compressor.OnOff = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid compressor integer property 0x%04x", + param); + } +} +void Compressor_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ Compressor_setParami(props, context, param, vals[0]); } +void Compressor_setParamf(EffectProps*, ALCcontext *context, ALenum param, ALfloat) +{ alSetError(context, AL_INVALID_ENUM, "Invalid compressor float property 0x%04x", param); } +void Compressor_setParamfv(EffectProps*, ALCcontext *context, ALenum param, const ALfloat*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid compressor float-vector property 0x%04x", param); } + +void Compressor_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_COMPRESSOR_ONOFF: + *val = props->Compressor.OnOff; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid compressor integer property 0x%04x", + param); + } +} +void Compressor_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ Compressor_getParami(props, context, param, vals); } +void Compressor_getParamf(const EffectProps*, ALCcontext *context, ALenum param, ALfloat*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid compressor float property 0x%04x", param); } +void Compressor_getParamfv(const EffectProps*, ALCcontext *context, ALenum param, ALfloat*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid compressor float-vector property 0x%04x", param); } + +DEFINE_ALEFFECT_VTABLE(Compressor); + + +struct CompressorStateFactory final : public EffectStateFactory { + EffectState *create() override { return new CompressorState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Compressor_vtable; } +}; + +EffectProps CompressorStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Compressor.OnOff = AL_COMPRESSOR_DEFAULT_ONOFF; + return props; +} + +} // namespace + +EffectStateFactory *CompressorStateFactory_getFactory() +{ + static CompressorStateFactory CompressorFactory{}; + return &CompressorFactory; +} diff --git a/alc/effects/dedicated.cpp b/alc/effects/dedicated.cpp new file mode 100644 index 00000000..b31b3750 --- /dev/null +++ b/alc/effects/dedicated.cpp @@ -0,0 +1,159 @@ +/** + * 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 <cstdlib> +#include <cmath> +#include <algorithm> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" + + +namespace { + +struct DedicatedState final : public EffectState { + ALfloat mCurrentGains[MAX_OUTPUT_CHANNELS]; + ALfloat mTargetGains[MAX_OUTPUT_CHANNELS]; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(DedicatedState) +}; + +ALboolean DedicatedState::deviceUpdate(const ALCdevice*) +{ + std::fill(std::begin(mCurrentGains), std::end(mCurrentGains), 0.0f); + return AL_TRUE; +} + +void DedicatedState::update(const ALCcontext*, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + std::fill(std::begin(mTargetGains), std::end(mTargetGains), 0.0f); + + const ALfloat Gain{slot->Params.Gain * props->Dedicated.Gain}; + + if(slot->Params.EffectType == AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT) + { + const int idx{!target.RealOut ? -1 : GetChannelIdxByName(*target.RealOut, LFE)}; + if(idx != -1) + { + mOutTarget = target.RealOut->Buffer; + mTargetGains[idx] = Gain; + } + } + else if(slot->Params.EffectType == AL_EFFECT_DEDICATED_DIALOGUE) + { + /* Dialog goes to the front-center speaker if it exists, otherwise it + * plays from the front-center location. */ + const int idx{!target.RealOut ? -1 : GetChannelIdxByName(*target.RealOut, FrontCenter)}; + if(idx != -1) + { + mOutTarget = target.RealOut->Buffer; + mTargetGains[idx] = Gain; + } + else + { + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcDirectionCoeffs({0.0f, 0.0f, -1.0f}, 0.0f, coeffs); + + mOutTarget = target.Main->Buffer; + ComputePanGains(target.Main, coeffs, Gain, mTargetGains); + } + } +} + +void DedicatedState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei /*numInput*/, const al::span<FloatBufferLine> samplesOut) +{ + MixSamples(samplesIn[0].data(), samplesOut, mCurrentGains, mTargetGains, samplesToDo, 0, + samplesToDo); +} + + +void Dedicated_setParami(EffectProps*, ALCcontext *context, ALenum param, ALint) +{ alSetError(context, AL_INVALID_ENUM, "Invalid dedicated integer property 0x%04x", param); } +void Dedicated_setParamiv(EffectProps*, ALCcontext *context, ALenum param, const ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid dedicated integer-vector property 0x%04x", param); } +void Dedicated_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_DEDICATED_GAIN: + if(!(val >= 0.0f && std::isfinite(val))) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Dedicated gain out of range"); + props->Dedicated.Gain = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid dedicated float property 0x%04x", param); + } +} +void Dedicated_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Dedicated_setParamf(props, context, param, vals[0]); } + +void Dedicated_getParami(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid dedicated integer property 0x%04x", param); } +void Dedicated_getParamiv(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid dedicated integer-vector property 0x%04x", param); } +void Dedicated_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_DEDICATED_GAIN: + *val = props->Dedicated.Gain; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid dedicated float property 0x%04x", param); + } +} +void Dedicated_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Dedicated_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Dedicated); + + +struct DedicatedStateFactory final : public EffectStateFactory { + EffectState *create() override { return new DedicatedState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Dedicated_vtable; } +}; + +EffectProps DedicatedStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Dedicated.Gain = 1.0f; + return props; +} + +} // namespace + +EffectStateFactory *DedicatedStateFactory_getFactory() +{ + static DedicatedStateFactory DedicatedFactory{}; + return &DedicatedFactory; +} diff --git a/alc/effects/distortion.cpp b/alc/effects/distortion.cpp new file mode 100644 index 00000000..59557395 --- /dev/null +++ b/alc/effects/distortion.cpp @@ -0,0 +1,269 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2013 by Mike Gorchak + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cmath> +#include <cstdlib> + +#include <cmath> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" +#include "filters/biquad.h" + + +namespace { + +struct DistortionState final : public EffectState { + /* Effect gains for each channel */ + ALfloat mGain[MAX_OUTPUT_CHANNELS]{}; + + /* Effect parameters */ + BiquadFilter mLowpass; + BiquadFilter mBandpass; + ALfloat mAttenuation{}; + ALfloat mEdgeCoeff{}; + + ALfloat mBuffer[2][BUFFERSIZE]{}; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(DistortionState) +}; + +ALboolean DistortionState::deviceUpdate(const ALCdevice*) +{ + mLowpass.clear(); + mBandpass.clear(); + return AL_TRUE; +} + +void DistortionState::update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *device{context->Device}; + + /* Store waveshaper edge settings. */ + const ALfloat edge{ + minf(std::sin(al::MathDefs<float>::Pi()*0.5f * props->Distortion.Edge), 0.99f)}; + mEdgeCoeff = 2.0f * edge / (1.0f-edge); + + ALfloat cutoff{props->Distortion.LowpassCutoff}; + /* Bandwidth value is constant in octaves. */ + ALfloat bandwidth{(cutoff / 2.0f) / (cutoff * 0.67f)}; + /* Multiply sampling frequency by the amount of oversampling done during + * processing. + */ + auto frequency = static_cast<ALfloat>(device->Frequency); + mLowpass.setParams(BiquadType::LowPass, 1.0f, cutoff / (frequency*4.0f), + mLowpass.rcpQFromBandwidth(cutoff / (frequency*4.0f), bandwidth)); + + cutoff = props->Distortion.EQCenter; + /* Convert bandwidth in Hz to octaves. */ + bandwidth = props->Distortion.EQBandwidth / (cutoff * 0.67f); + mBandpass.setParams(BiquadType::BandPass, 1.0f, cutoff / (frequency*4.0f), + mBandpass.rcpQFromBandwidth(cutoff / (frequency*4.0f), bandwidth)); + + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcDirectionCoeffs({0.0f, 0.0f, -1.0f}, 0.0f, coeffs); + + mOutTarget = target.Main->Buffer; + ComputePanGains(target.Main, coeffs, slot->Params.Gain*props->Distortion.Gain, mGain); +} + +void DistortionState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei /*numInput*/, const al::span<FloatBufferLine> samplesOut) +{ + const ALfloat fc{mEdgeCoeff}; + for(ALsizei base{0};base < samplesToDo;) + { + /* Perform 4x oversampling to avoid aliasing. Oversampling greatly + * improves distortion quality and allows to implement lowpass and + * bandpass filters using high frequencies, at which classic IIR + * filters became unstable. + */ + ALsizei todo{mini(BUFFERSIZE, (samplesToDo-base) * 4)}; + + /* Fill oversample buffer using zero stuffing. Multiply the sample by + * the amount of oversampling to maintain the signal's power. + */ + for(ALsizei i{0};i < todo;i++) + mBuffer[0][i] = !(i&3) ? samplesIn[0][(i>>2)+base] * 4.0f : 0.0f; + + /* First step, do lowpass filtering of original signal. Additionally + * perform buffer interpolation and lowpass cutoff for oversampling + * (which is fortunately first step of distortion). So combine three + * operations into the one. + */ + mLowpass.process(mBuffer[1], mBuffer[0], todo); + + /* Second step, do distortion using waveshaper function to emulate + * signal processing during tube overdriving. Three steps of + * waveshaping are intended to modify waveform without boost/clipping/ + * attenuation process. + */ + for(ALsizei i{0};i < todo;i++) + { + ALfloat smp{mBuffer[1][i]}; + + smp = (1.0f + fc) * smp/(1.0f + fc*fabsf(smp)); + smp = (1.0f + fc) * smp/(1.0f + fc*fabsf(smp)) * -1.0f; + smp = (1.0f + fc) * smp/(1.0f + fc*fabsf(smp)); + + mBuffer[0][i] = smp; + } + + /* Third step, do bandpass filtering of distorted signal. */ + mBandpass.process(mBuffer[1], mBuffer[0], todo); + + todo >>= 2; + const ALfloat *outgains{mGain}; + for(FloatBufferLine &output : samplesOut) + { + /* Fourth step, final, do attenuation and perform decimation, + * storing only one sample out of four. + */ + const ALfloat gain{*(outgains++)}; + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + + for(ALsizei i{0};i < todo;i++) + output[base+i] += gain * mBuffer[1][i*4]; + } + + base += todo; + } +} + + +void Distortion_setParami(EffectProps*, ALCcontext *context, ALenum param, ALint) +{ alSetError(context, AL_INVALID_ENUM, "Invalid distortion integer property 0x%04x", param); } +void Distortion_setParamiv(EffectProps*, ALCcontext *context, ALenum param, const ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid distortion integer-vector property 0x%04x", param); } +void Distortion_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_DISTORTION_EDGE: + if(!(val >= AL_DISTORTION_MIN_EDGE && val <= AL_DISTORTION_MAX_EDGE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Distortion edge out of range"); + props->Distortion.Edge = val; + break; + + case AL_DISTORTION_GAIN: + if(!(val >= AL_DISTORTION_MIN_GAIN && val <= AL_DISTORTION_MAX_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Distortion gain out of range"); + props->Distortion.Gain = val; + break; + + case AL_DISTORTION_LOWPASS_CUTOFF: + if(!(val >= AL_DISTORTION_MIN_LOWPASS_CUTOFF && val <= AL_DISTORTION_MAX_LOWPASS_CUTOFF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Distortion low-pass cutoff out of range"); + props->Distortion.LowpassCutoff = val; + break; + + case AL_DISTORTION_EQCENTER: + if(!(val >= AL_DISTORTION_MIN_EQCENTER && val <= AL_DISTORTION_MAX_EQCENTER)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Distortion EQ center out of range"); + props->Distortion.EQCenter = val; + break; + + case AL_DISTORTION_EQBANDWIDTH: + if(!(val >= AL_DISTORTION_MIN_EQBANDWIDTH && val <= AL_DISTORTION_MAX_EQBANDWIDTH)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Distortion EQ bandwidth out of range"); + props->Distortion.EQBandwidth = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid distortion float property 0x%04x", + param); + } +} +void Distortion_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Distortion_setParamf(props, context, param, vals[0]); } + +void Distortion_getParami(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid distortion integer property 0x%04x", param); } +void Distortion_getParamiv(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid distortion integer-vector property 0x%04x", param); } +void Distortion_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_DISTORTION_EDGE: + *val = props->Distortion.Edge; + break; + + case AL_DISTORTION_GAIN: + *val = props->Distortion.Gain; + break; + + case AL_DISTORTION_LOWPASS_CUTOFF: + *val = props->Distortion.LowpassCutoff; + break; + + case AL_DISTORTION_EQCENTER: + *val = props->Distortion.EQCenter; + break; + + case AL_DISTORTION_EQBANDWIDTH: + *val = props->Distortion.EQBandwidth; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid distortion float property 0x%04x", + param); + } +} +void Distortion_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Distortion_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Distortion); + + +struct DistortionStateFactory final : public EffectStateFactory { + EffectState *create() override { return new DistortionState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Distortion_vtable; } +}; + +EffectProps DistortionStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Distortion.Edge = AL_DISTORTION_DEFAULT_EDGE; + props.Distortion.Gain = AL_DISTORTION_DEFAULT_GAIN; + props.Distortion.LowpassCutoff = AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF; + props.Distortion.EQCenter = AL_DISTORTION_DEFAULT_EQCENTER; + props.Distortion.EQBandwidth = AL_DISTORTION_DEFAULT_EQBANDWIDTH; + return props; +} + +} // namespace + +EffectStateFactory *DistortionStateFactory_getFactory() +{ + static DistortionStateFactory DistortionFactory{}; + return &DistortionFactory; +} diff --git a/alc/effects/echo.cpp b/alc/effects/echo.cpp new file mode 100644 index 00000000..c10f2eb2 --- /dev/null +++ b/alc/effects/echo.cpp @@ -0,0 +1,271 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2009 by Chris Robinson. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cmath> +#include <cstdlib> + +#include <algorithm> + +#include "alcmain.h" +#include "alcontext.h" +#include "alFilter.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" +#include "filters/biquad.h" +#include "vector.h" + + +namespace { + +struct EchoState final : public EffectState { + al::vector<ALfloat,16> mSampleBuffer; + + // The echo is two tap. The delay is the number of samples from before the + // current offset + struct { + ALsizei delay{0}; + } mTap[2]; + ALsizei mOffset{0}; + + /* The panning gains for the two taps */ + struct { + ALfloat Current[MAX_OUTPUT_CHANNELS]{}; + ALfloat Target[MAX_OUTPUT_CHANNELS]{}; + } mGains[2]; + + BiquadFilter mFilter; + ALfloat mFeedGain{0.0f}; + + alignas(16) ALfloat mTempBuffer[2][BUFFERSIZE]; + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(EchoState) +}; + +ALboolean EchoState::deviceUpdate(const ALCdevice *Device) +{ + ALuint maxlen; + + // Use the next power of 2 for the buffer length, so the tap offsets can be + // wrapped using a mask instead of a modulo + maxlen = float2int(AL_ECHO_MAX_DELAY*Device->Frequency + 0.5f) + + float2int(AL_ECHO_MAX_LRDELAY*Device->Frequency + 0.5f); + maxlen = NextPowerOf2(maxlen); + if(maxlen <= 0) return AL_FALSE; + + if(maxlen != mSampleBuffer.size()) + { + mSampleBuffer.resize(maxlen); + mSampleBuffer.shrink_to_fit(); + } + + std::fill(mSampleBuffer.begin(), mSampleBuffer.end(), 0.0f); + for(auto &e : mGains) + { + std::fill(std::begin(e.Current), std::end(e.Current), 0.0f); + std::fill(std::begin(e.Target), std::end(e.Target), 0.0f); + } + + return AL_TRUE; +} + +void EchoState::update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *device = context->Device; + const auto frequency = static_cast<ALfloat>(device->Frequency); + + mTap[0].delay = maxi(float2int(props->Echo.Delay*frequency + 0.5f), 1); + mTap[1].delay = float2int(props->Echo.LRDelay*frequency + 0.5f) + mTap[0].delay; + + const ALfloat gainhf{maxf(1.0f - props->Echo.Damping, 0.0625f)}; /* Limit -24dB */ + mFilter.setParams(BiquadType::HighShelf, gainhf, LOWPASSFREQREF/frequency, + mFilter.rcpQFromSlope(gainhf, 1.0f)); + + mFeedGain = props->Echo.Feedback; + + /* Convert echo spread (where 0 = center, +/-1 = sides) to angle. */ + const ALfloat angle{std::asin(props->Echo.Spread)}; + + ALfloat coeffs[2][MAX_AMBI_CHANNELS]; + CalcAngleCoeffs(-angle, 0.0f, 0.0f, coeffs[0]); + CalcAngleCoeffs( angle, 0.0f, 0.0f, coeffs[1]); + + mOutTarget = target.Main->Buffer; + ComputePanGains(target.Main, coeffs[0], slot->Params.Gain, mGains[0].Target); + ComputePanGains(target.Main, coeffs[1], slot->Params.Gain, mGains[1].Target); +} + +void EchoState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei /*numInput*/, const al::span<FloatBufferLine> samplesOut) +{ + const auto mask = static_cast<ALsizei>(mSampleBuffer.size()-1); + ALfloat *RESTRICT delaybuf{mSampleBuffer.data()}; + ALsizei offset{mOffset}; + ALsizei tap1{offset - mTap[0].delay}; + ALsizei tap2{offset - mTap[1].delay}; + ALfloat z1, z2; + + ASSUME(samplesToDo > 0); + ASSUME(mask > 0); + + std::tie(z1, z2) = mFilter.getComponents(); + for(ALsizei i{0};i < samplesToDo;) + { + offset &= mask; + tap1 &= mask; + tap2 &= mask; + + ALsizei td{mini(mask+1 - maxi(offset, maxi(tap1, tap2)), samplesToDo-i)}; + do { + /* Feed the delay buffer's input first. */ + delaybuf[offset] = samplesIn[0][i]; + + /* Get delayed output from the first and second taps. Use the + * second tap for feedback. + */ + mTempBuffer[0][i] = delaybuf[tap1++]; + mTempBuffer[1][i] = delaybuf[tap2++]; + const float feedb{mTempBuffer[1][i++]}; + + /* Add feedback to the delay buffer with damping and attenuation. */ + delaybuf[offset++] += mFilter.processOne(feedb, z1, z2) * mFeedGain; + } while(--td); + } + mFilter.setComponents(z1, z2); + mOffset = offset; + + for(ALsizei c{0};c < 2;c++) + MixSamples(mTempBuffer[c], samplesOut, mGains[c].Current, mGains[c].Target, samplesToDo, 0, + samplesToDo); +} + + +void Echo_setParami(EffectProps*, ALCcontext *context, ALenum param, ALint) +{ alSetError(context, AL_INVALID_ENUM, "Invalid echo integer property 0x%04x", param); } +void Echo_setParamiv(EffectProps*, ALCcontext *context, ALenum param, const ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid echo integer-vector property 0x%04x", param); } +void Echo_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_ECHO_DELAY: + if(!(val >= AL_ECHO_MIN_DELAY && val <= AL_ECHO_MAX_DELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Echo delay out of range"); + props->Echo.Delay = val; + break; + + case AL_ECHO_LRDELAY: + if(!(val >= AL_ECHO_MIN_LRDELAY && val <= AL_ECHO_MAX_LRDELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Echo LR delay out of range"); + props->Echo.LRDelay = val; + break; + + case AL_ECHO_DAMPING: + if(!(val >= AL_ECHO_MIN_DAMPING && val <= AL_ECHO_MAX_DAMPING)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Echo damping out of range"); + props->Echo.Damping = val; + break; + + case AL_ECHO_FEEDBACK: + if(!(val >= AL_ECHO_MIN_FEEDBACK && val <= AL_ECHO_MAX_FEEDBACK)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Echo feedback out of range"); + props->Echo.Feedback = val; + break; + + case AL_ECHO_SPREAD: + if(!(val >= AL_ECHO_MIN_SPREAD && val <= AL_ECHO_MAX_SPREAD)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Echo spread out of range"); + props->Echo.Spread = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid echo float property 0x%04x", param); + } +} +void Echo_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Echo_setParamf(props, context, param, vals[0]); } + +void Echo_getParami(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid echo integer property 0x%04x", param); } +void Echo_getParamiv(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid echo integer-vector property 0x%04x", param); } +void Echo_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_ECHO_DELAY: + *val = props->Echo.Delay; + break; + + case AL_ECHO_LRDELAY: + *val = props->Echo.LRDelay; + break; + + case AL_ECHO_DAMPING: + *val = props->Echo.Damping; + break; + + case AL_ECHO_FEEDBACK: + *val = props->Echo.Feedback; + break; + + case AL_ECHO_SPREAD: + *val = props->Echo.Spread; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid echo float property 0x%04x", param); + } +} +void Echo_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Echo_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Echo); + + +struct EchoStateFactory final : public EffectStateFactory { + EffectState *create() override { return new EchoState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Echo_vtable; } +}; + +EffectProps EchoStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Echo.Delay = AL_ECHO_DEFAULT_DELAY; + props.Echo.LRDelay = AL_ECHO_DEFAULT_LRDELAY; + props.Echo.Damping = AL_ECHO_DEFAULT_DAMPING; + props.Echo.Feedback = AL_ECHO_DEFAULT_FEEDBACK; + props.Echo.Spread = AL_ECHO_DEFAULT_SPREAD; + return props; +} + +} // namespace + +EffectStateFactory *EchoStateFactory_getFactory() +{ + static EchoStateFactory EchoFactory{}; + return &EchoFactory; +} diff --git a/alc/effects/equalizer.cpp b/alc/effects/equalizer.cpp new file mode 100644 index 00000000..69ab5021 --- /dev/null +++ b/alc/effects/equalizer.cpp @@ -0,0 +1,337 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2013 by Mike Gorchak + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cmath> +#include <cstdlib> + +#include <algorithm> +#include <functional> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" +#include "filters/biquad.h" +#include "vecmat.h" + + +namespace { + +/* The document "Effects Extension Guide.pdf" says that low and high * + * frequencies are cutoff frequencies. This is not fully correct, they * + * are corner frequencies for low and high shelf filters. If they were * + * just cutoff frequencies, there would be no need in cutoff frequency * + * gains, which are present. Documentation for "Creative Proteus X2" * + * software describes 4-band equalizer functionality in a much better * + * way. This equalizer seems to be a predecessor of OpenAL 4-band * + * equalizer. With low and high shelf filters we are able to cutoff * + * frequencies below and/or above corner frequencies using attenuation * + * gains (below 1.0) and amplify all low and/or high frequencies using * + * gains above 1.0. * + * * + * Low-shelf Low Mid Band High Mid Band High-shelf * + * corner center center corner * + * frequency frequency frequency frequency * + * 50Hz..800Hz 200Hz..3000Hz 1000Hz..8000Hz 4000Hz..16000Hz * + * * + * | | | | * + * | | | | * + * B -----+ /--+--\ /--+--\ +----- * + * O |\ | | | | | | /| * + * O | \ - | - - | - / | * + * S + | \ | | | | | | / | * + * T | | | | | | | | | | * + * ---------+---------------+------------------+---------------+-------- * + * C | | | | | | | | | | * + * U - | / | | | | | | \ | * + * T | / - | - - | - \ | * + * O |/ | | | | | | \| * + * F -----+ \--+--/ \--+--/ +----- * + * F | | | | * + * | | | | * + * * + * Gains vary from 0.126 up to 7.943, which means from -18dB attenuation * + * up to +18dB amplification. Band width varies from 0.01 up to 1.0 in * + * octaves for two mid bands. * + * * + * Implementation is based on the "Cookbook formulae for audio EQ biquad * + * filter coefficients" by Robert Bristow-Johnson * + * http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt */ + + +struct EqualizerState final : public EffectState { + struct { + /* Effect parameters */ + BiquadFilter filter[4]; + + /* Effect gains for each channel */ + ALfloat CurrentGains[MAX_OUTPUT_CHANNELS]{}; + ALfloat TargetGains[MAX_OUTPUT_CHANNELS]{}; + } mChans[MAX_AMBI_CHANNELS]; + + ALfloat mSampleBuffer[BUFFERSIZE]{}; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(EqualizerState) +}; + +ALboolean EqualizerState::deviceUpdate(const ALCdevice*) +{ + for(auto &e : mChans) + { + std::for_each(std::begin(e.filter), std::end(e.filter), + std::mem_fn(&BiquadFilter::clear)); + std::fill(std::begin(e.CurrentGains), std::end(e.CurrentGains), 0.0f); + } + return AL_TRUE; +} + +void EqualizerState::update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *device = context->Device; + auto frequency = static_cast<ALfloat>(device->Frequency); + ALfloat gain, f0norm; + + /* Calculate coefficients for the each type of filter. Note that the shelf + * filters' gain is for the reference frequency, which is the centerpoint + * of the transition band. + */ + gain = maxf(sqrtf(props->Equalizer.LowGain), 0.0625f); /* Limit -24dB */ + f0norm = props->Equalizer.LowCutoff/frequency; + mChans[0].filter[0].setParams(BiquadType::LowShelf, gain, f0norm, + BiquadFilter::rcpQFromSlope(gain, 0.75f)); + + gain = maxf(props->Equalizer.Mid1Gain, 0.0625f); + f0norm = props->Equalizer.Mid1Center/frequency; + mChans[0].filter[1].setParams(BiquadType::Peaking, gain, f0norm, + BiquadFilter::rcpQFromBandwidth(f0norm, props->Equalizer.Mid1Width)); + + gain = maxf(props->Equalizer.Mid2Gain, 0.0625f); + f0norm = props->Equalizer.Mid2Center/frequency; + mChans[0].filter[2].setParams(BiquadType::Peaking, gain, f0norm, + BiquadFilter::rcpQFromBandwidth(f0norm, props->Equalizer.Mid2Width)); + + gain = maxf(sqrtf(props->Equalizer.HighGain), 0.0625f); + f0norm = props->Equalizer.HighCutoff/frequency; + mChans[0].filter[3].setParams(BiquadType::HighShelf, gain, f0norm, + BiquadFilter::rcpQFromSlope(gain, 0.75f)); + + /* Copy the filter coefficients for the other input channels. */ + for(size_t i{1u};i < slot->Wet.Buffer.size();++i) + { + mChans[i].filter[0].copyParamsFrom(mChans[0].filter[0]); + mChans[i].filter[1].copyParamsFrom(mChans[0].filter[1]); + mChans[i].filter[2].copyParamsFrom(mChans[0].filter[2]); + mChans[i].filter[3].copyParamsFrom(mChans[0].filter[3]); + } + + mOutTarget = target.Main->Buffer; + for(size_t i{0u};i < slot->Wet.Buffer.size();++i) + { + auto coeffs = GetAmbiIdentityRow(i); + ComputePanGains(target.Main, coeffs.data(), slot->Params.Gain, mChans[i].TargetGains); + } +} + +void EqualizerState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) +{ + ASSUME(numInput > 0); + for(ALsizei c{0};c < numInput;c++) + { + mChans[c].filter[0].process(mSampleBuffer, samplesIn[c].data(), samplesToDo); + mChans[c].filter[1].process(mSampleBuffer, mSampleBuffer, samplesToDo); + mChans[c].filter[2].process(mSampleBuffer, mSampleBuffer, samplesToDo); + mChans[c].filter[3].process(mSampleBuffer, mSampleBuffer, samplesToDo); + + MixSamples(mSampleBuffer, samplesOut, mChans[c].CurrentGains, mChans[c].TargetGains, + samplesToDo, 0, samplesToDo); + } +} + + +void Equalizer_setParami(EffectProps*, ALCcontext *context, ALenum param, ALint) +{ alSetError(context, AL_INVALID_ENUM, "Invalid equalizer integer property 0x%04x", param); } +void Equalizer_setParamiv(EffectProps*, ALCcontext *context, ALenum param, const ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid equalizer integer-vector property 0x%04x", param); } +void Equalizer_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_EQUALIZER_LOW_GAIN: + if(!(val >= AL_EQUALIZER_MIN_LOW_GAIN && val <= AL_EQUALIZER_MAX_LOW_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer low-band gain out of range"); + props->Equalizer.LowGain = val; + break; + + case AL_EQUALIZER_LOW_CUTOFF: + if(!(val >= AL_EQUALIZER_MIN_LOW_CUTOFF && val <= AL_EQUALIZER_MAX_LOW_CUTOFF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer low-band cutoff out of range"); + props->Equalizer.LowCutoff = val; + break; + + case AL_EQUALIZER_MID1_GAIN: + if(!(val >= AL_EQUALIZER_MIN_MID1_GAIN && val <= AL_EQUALIZER_MAX_MID1_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer mid1-band gain out of range"); + props->Equalizer.Mid1Gain = val; + break; + + case AL_EQUALIZER_MID1_CENTER: + if(!(val >= AL_EQUALIZER_MIN_MID1_CENTER && val <= AL_EQUALIZER_MAX_MID1_CENTER)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer mid1-band center out of range"); + props->Equalizer.Mid1Center = val; + break; + + case AL_EQUALIZER_MID1_WIDTH: + if(!(val >= AL_EQUALIZER_MIN_MID1_WIDTH && val <= AL_EQUALIZER_MAX_MID1_WIDTH)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer mid1-band width out of range"); + props->Equalizer.Mid1Width = val; + break; + + case AL_EQUALIZER_MID2_GAIN: + if(!(val >= AL_EQUALIZER_MIN_MID2_GAIN && val <= AL_EQUALIZER_MAX_MID2_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer mid2-band gain out of range"); + props->Equalizer.Mid2Gain = val; + break; + + case AL_EQUALIZER_MID2_CENTER: + if(!(val >= AL_EQUALIZER_MIN_MID2_CENTER && val <= AL_EQUALIZER_MAX_MID2_CENTER)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer mid2-band center out of range"); + props->Equalizer.Mid2Center = val; + break; + + case AL_EQUALIZER_MID2_WIDTH: + if(!(val >= AL_EQUALIZER_MIN_MID2_WIDTH && val <= AL_EQUALIZER_MAX_MID2_WIDTH)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer mid2-band width out of range"); + props->Equalizer.Mid2Width = val; + break; + + case AL_EQUALIZER_HIGH_GAIN: + if(!(val >= AL_EQUALIZER_MIN_HIGH_GAIN && val <= AL_EQUALIZER_MAX_HIGH_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer high-band gain out of range"); + props->Equalizer.HighGain = val; + break; + + case AL_EQUALIZER_HIGH_CUTOFF: + if(!(val >= AL_EQUALIZER_MIN_HIGH_CUTOFF && val <= AL_EQUALIZER_MAX_HIGH_CUTOFF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Equalizer high-band cutoff out of range"); + props->Equalizer.HighCutoff = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid equalizer float property 0x%04x", param); + } +} +void Equalizer_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Equalizer_setParamf(props, context, param, vals[0]); } + +void Equalizer_getParami(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid equalizer integer property 0x%04x", param); } +void Equalizer_getParamiv(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid equalizer integer-vector property 0x%04x", param); } +void Equalizer_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_EQUALIZER_LOW_GAIN: + *val = props->Equalizer.LowGain; + break; + + case AL_EQUALIZER_LOW_CUTOFF: + *val = props->Equalizer.LowCutoff; + break; + + case AL_EQUALIZER_MID1_GAIN: + *val = props->Equalizer.Mid1Gain; + break; + + case AL_EQUALIZER_MID1_CENTER: + *val = props->Equalizer.Mid1Center; + break; + + case AL_EQUALIZER_MID1_WIDTH: + *val = props->Equalizer.Mid1Width; + break; + + case AL_EQUALIZER_MID2_GAIN: + *val = props->Equalizer.Mid2Gain; + break; + + case AL_EQUALIZER_MID2_CENTER: + *val = props->Equalizer.Mid2Center; + break; + + case AL_EQUALIZER_MID2_WIDTH: + *val = props->Equalizer.Mid2Width; + break; + + case AL_EQUALIZER_HIGH_GAIN: + *val = props->Equalizer.HighGain; + break; + + case AL_EQUALIZER_HIGH_CUTOFF: + *val = props->Equalizer.HighCutoff; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid equalizer float property 0x%04x", param); + } +} +void Equalizer_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Equalizer_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Equalizer); + + +struct EqualizerStateFactory final : public EffectStateFactory { + EffectState *create() override { return new EqualizerState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Equalizer_vtable; } +}; + +EffectProps EqualizerStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Equalizer.LowCutoff = AL_EQUALIZER_DEFAULT_LOW_CUTOFF; + props.Equalizer.LowGain = AL_EQUALIZER_DEFAULT_LOW_GAIN; + props.Equalizer.Mid1Center = AL_EQUALIZER_DEFAULT_MID1_CENTER; + props.Equalizer.Mid1Gain = AL_EQUALIZER_DEFAULT_MID1_GAIN; + props.Equalizer.Mid1Width = AL_EQUALIZER_DEFAULT_MID1_WIDTH; + props.Equalizer.Mid2Center = AL_EQUALIZER_DEFAULT_MID2_CENTER; + props.Equalizer.Mid2Gain = AL_EQUALIZER_DEFAULT_MID2_GAIN; + props.Equalizer.Mid2Width = AL_EQUALIZER_DEFAULT_MID2_WIDTH; + props.Equalizer.HighCutoff = AL_EQUALIZER_DEFAULT_HIGH_CUTOFF; + props.Equalizer.HighGain = AL_EQUALIZER_DEFAULT_HIGH_GAIN; + return props; +} + +} // namespace + +EffectStateFactory *EqualizerStateFactory_getFactory() +{ + static EqualizerStateFactory EqualizerFactory{}; + return &EqualizerFactory; +} diff --git a/alc/effects/fshifter.cpp b/alc/effects/fshifter.cpp new file mode 100644 index 00000000..b47aa00e --- /dev/null +++ b/alc/effects/fshifter.cpp @@ -0,0 +1,301 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2018 by Raul Herraiz. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cmath> +#include <cstdlib> +#include <array> +#include <complex> +#include <algorithm> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" + +#include "alcomplex.h" + +namespace { + +using complex_d = std::complex<double>; + +#define HIL_SIZE 1024 +#define OVERSAMP (1<<2) + +#define HIL_STEP (HIL_SIZE / OVERSAMP) +#define FIFO_LATENCY (HIL_STEP * (OVERSAMP-1)) + +/* Define a Hann window, used to filter the HIL input and output. */ +/* Making this constexpr seems to require C++14. */ +std::array<ALdouble,HIL_SIZE> InitHannWindow() +{ + std::array<ALdouble,HIL_SIZE> ret; + /* Create lookup table of the Hann window for the desired size, i.e. HIL_SIZE */ + for(ALsizei i{0};i < HIL_SIZE>>1;i++) + { + ALdouble val = std::sin(al::MathDefs<double>::Pi() * i / ALdouble{HIL_SIZE-1}); + ret[i] = ret[HIL_SIZE-1-i] = val * val; + } + return ret; +} +alignas(16) const std::array<ALdouble,HIL_SIZE> HannWindow = InitHannWindow(); + + +struct FshifterState final : public EffectState { + /* Effect parameters */ + ALsizei mCount{}; + ALsizei mPhaseStep{}; + ALsizei mPhase{}; + ALdouble mLdSign{}; + + /*Effects buffers*/ + ALfloat mInFIFO[HIL_SIZE]{}; + complex_d mOutFIFO[HIL_SIZE]{}; + complex_d mOutputAccum[HIL_SIZE]{}; + complex_d mAnalytic[HIL_SIZE]{}; + complex_d mOutdata[BUFFERSIZE]{}; + + alignas(16) ALfloat mBufferOut[BUFFERSIZE]{}; + + /* Effect gains for each output channel */ + ALfloat mCurrentGains[MAX_OUTPUT_CHANNELS]{}; + ALfloat mTargetGains[MAX_OUTPUT_CHANNELS]{}; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(FshifterState) +}; + +ALboolean FshifterState::deviceUpdate(const ALCdevice*) +{ + /* (Re-)initializing parameters and clear the buffers. */ + mCount = FIFO_LATENCY; + mPhaseStep = 0; + mPhase = 0; + mLdSign = 1.0; + + std::fill(std::begin(mInFIFO), std::end(mInFIFO), 0.0f); + std::fill(std::begin(mOutFIFO), std::end(mOutFIFO), complex_d{}); + std::fill(std::begin(mOutputAccum), std::end(mOutputAccum), complex_d{}); + std::fill(std::begin(mAnalytic), std::end(mAnalytic), complex_d{}); + + std::fill(std::begin(mCurrentGains), std::end(mCurrentGains), 0.0f); + std::fill(std::begin(mTargetGains), std::end(mTargetGains), 0.0f); + + return AL_TRUE; +} + +void FshifterState::update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *device{context->Device}; + + ALfloat step{props->Fshifter.Frequency / static_cast<ALfloat>(device->Frequency)}; + mPhaseStep = fastf2i(minf(step, 0.5f) * FRACTIONONE); + + switch(props->Fshifter.LeftDirection) + { + case AL_FREQUENCY_SHIFTER_DIRECTION_DOWN: + mLdSign = -1.0; + break; + + case AL_FREQUENCY_SHIFTER_DIRECTION_UP: + mLdSign = 1.0; + break; + + case AL_FREQUENCY_SHIFTER_DIRECTION_OFF: + mPhase = 0; + mPhaseStep = 0; + break; + } + + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcDirectionCoeffs({0.0f, 0.0f, -1.0f}, 0.0f, coeffs); + + mOutTarget = target.Main->Buffer; + ComputePanGains(target.Main, coeffs, slot->Params.Gain, mTargetGains); +} + +void FshifterState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei /*numInput*/, const al::span<FloatBufferLine> samplesOut) +{ + static constexpr complex_d complex_zero{0.0, 0.0}; + ALfloat *RESTRICT BufferOut = mBufferOut; + ALsizei j, k, base; + + for(base = 0;base < samplesToDo;) + { + const ALsizei todo{mini(HIL_SIZE-mCount, samplesToDo-base)}; + + ASSUME(todo > 0); + + /* Fill FIFO buffer with samples data */ + k = mCount; + for(j = 0;j < todo;j++,k++) + { + mInFIFO[k] = samplesIn[0][base+j]; + mOutdata[base+j] = mOutFIFO[k-FIFO_LATENCY]; + } + mCount += todo; + base += todo; + + /* Check whether FIFO buffer is filled */ + if(mCount < HIL_SIZE) continue; + mCount = FIFO_LATENCY; + + /* Real signal windowing and store in Analytic buffer */ + for(k = 0;k < HIL_SIZE;k++) + { + mAnalytic[k].real(mInFIFO[k] * HannWindow[k]); + mAnalytic[k].imag(0.0); + } + + /* Processing signal by Discrete Hilbert Transform (analytical signal). */ + complex_hilbert(mAnalytic); + + /* Windowing and add to output accumulator */ + for(k = 0;k < HIL_SIZE;k++) + mOutputAccum[k] += 2.0/OVERSAMP*HannWindow[k]*mAnalytic[k]; + + /* Shift accumulator, input & output FIFO */ + for(k = 0;k < HIL_STEP;k++) mOutFIFO[k] = mOutputAccum[k]; + for(j = 0;k < HIL_SIZE;k++,j++) mOutputAccum[j] = mOutputAccum[k]; + for(;j < HIL_SIZE;j++) mOutputAccum[j] = complex_zero; + for(k = 0;k < FIFO_LATENCY;k++) + mInFIFO[k] = mInFIFO[k+HIL_STEP]; + } + + /* Process frequency shifter using the analytic signal obtained. */ + for(k = 0;k < samplesToDo;k++) + { + double phase = mPhase * ((1.0/FRACTIONONE) * al::MathDefs<double>::Tau()); + BufferOut[k] = static_cast<float>(mOutdata[k].real()*std::cos(phase) + + mOutdata[k].imag()*std::sin(phase)*mLdSign); + + mPhase += mPhaseStep; + mPhase &= FRACTIONMASK; + } + + /* Now, mix the processed sound data to the output. */ + MixSamples(BufferOut, samplesOut, mCurrentGains, mTargetGains, maxi(samplesToDo, 512), 0, + samplesToDo); +} + + +void Fshifter_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_FREQUENCY_SHIFTER_FREQUENCY: + if(!(val >= AL_FREQUENCY_SHIFTER_MIN_FREQUENCY && val <= AL_FREQUENCY_SHIFTER_MAX_FREQUENCY)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Frequency shifter frequency out of range"); + props->Fshifter.Frequency = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid frequency shifter float property 0x%04x", param); + } +} +void Fshifter_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Fshifter_setParamf(props, context, param, vals[0]); } + +void Fshifter_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_FREQUENCY_SHIFTER_LEFT_DIRECTION: + if(!(val >= AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION && val <= AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Frequency shifter left direction out of range"); + props->Fshifter.LeftDirection = val; + break; + + case AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION: + if(!(val >= AL_FREQUENCY_SHIFTER_MIN_RIGHT_DIRECTION && val <= AL_FREQUENCY_SHIFTER_MAX_RIGHT_DIRECTION)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Frequency shifter right direction out of range"); + props->Fshifter.RightDirection = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid frequency shifter integer property 0x%04x", param); + } +} +void Fshifter_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ Fshifter_setParami(props, context, param, vals[0]); } + +void Fshifter_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_FREQUENCY_SHIFTER_LEFT_DIRECTION: + *val = props->Fshifter.LeftDirection; + break; + case AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION: + *val = props->Fshifter.RightDirection; + break; + default: + alSetError(context, AL_INVALID_ENUM, "Invalid frequency shifter integer property 0x%04x", param); + } +} +void Fshifter_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ Fshifter_getParami(props, context, param, vals); } + +void Fshifter_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_FREQUENCY_SHIFTER_FREQUENCY: + *val = props->Fshifter.Frequency; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid frequency shifter float property 0x%04x", param); + } +} +void Fshifter_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Fshifter_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Fshifter); + + +struct FshifterStateFactory final : public EffectStateFactory { + EffectState *create() override { return new FshifterState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Fshifter_vtable; } +}; + +EffectProps FshifterStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Fshifter.Frequency = AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY; + props.Fshifter.LeftDirection = AL_FREQUENCY_SHIFTER_DEFAULT_LEFT_DIRECTION; + props.Fshifter.RightDirection = AL_FREQUENCY_SHIFTER_DEFAULT_RIGHT_DIRECTION; + return props; +} + +} // namespace + +EffectStateFactory *FshifterStateFactory_getFactory() +{ + static FshifterStateFactory FshifterFactory{}; + return &FshifterFactory; +} diff --git a/alc/effects/modulator.cpp b/alc/effects/modulator.cpp new file mode 100644 index 00000000..086482d7 --- /dev/null +++ b/alc/effects/modulator.cpp @@ -0,0 +1,279 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2009 by Chris Robinson. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cmath> +#include <cstdlib> + +#include <cmath> +#include <algorithm> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" +#include "filters/biquad.h" +#include "vecmat.h" + + +namespace { + +#define MAX_UPDATE_SAMPLES 128 + +#define WAVEFORM_FRACBITS 24 +#define WAVEFORM_FRACONE (1<<WAVEFORM_FRACBITS) +#define WAVEFORM_FRACMASK (WAVEFORM_FRACONE-1) + +inline ALfloat Sin(ALsizei index) +{ + return std::sin(static_cast<ALfloat>(index) * + (al::MathDefs<float>::Tau() / ALfloat{WAVEFORM_FRACONE})); +} + +inline ALfloat Saw(ALsizei index) +{ + return static_cast<ALfloat>(index)*(2.0f/WAVEFORM_FRACONE) - 1.0f; +} + +inline ALfloat Square(ALsizei index) +{ + return static_cast<ALfloat>(((index>>(WAVEFORM_FRACBITS-2))&2) - 1); +} + +inline ALfloat One(ALsizei) +{ + return 1.0f; +} + +template<ALfloat func(ALsizei)> +void Modulate(ALfloat *RESTRICT dst, ALsizei index, const ALsizei step, ALsizei todo) +{ + ALsizei i; + for(i = 0;i < todo;i++) + { + index += step; + index &= WAVEFORM_FRACMASK; + dst[i] = func(index); + } +} + + +struct ModulatorState final : public EffectState { + void (*mGetSamples)(ALfloat*RESTRICT, ALsizei, const ALsizei, ALsizei){}; + + ALsizei mIndex{0}; + ALsizei mStep{1}; + + struct { + BiquadFilter Filter; + + ALfloat CurrentGains[MAX_OUTPUT_CHANNELS]{}; + ALfloat TargetGains[MAX_OUTPUT_CHANNELS]{}; + } mChans[MAX_AMBI_CHANNELS]; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(ModulatorState) +}; + +ALboolean ModulatorState::deviceUpdate(const ALCdevice*) +{ + for(auto &e : mChans) + { + e.Filter.clear(); + std::fill(std::begin(e.CurrentGains), std::end(e.CurrentGains), 0.0f); + } + return AL_TRUE; +} + +void ModulatorState::update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *device{context->Device}; + + const float step{props->Modulator.Frequency / static_cast<ALfloat>(device->Frequency)}; + mStep = fastf2i(clampf(step*WAVEFORM_FRACONE, 0.0f, ALfloat{WAVEFORM_FRACONE-1})); + + if(mStep == 0) + mGetSamples = Modulate<One>; + else if(props->Modulator.Waveform == AL_RING_MODULATOR_SINUSOID) + mGetSamples = Modulate<Sin>; + else if(props->Modulator.Waveform == AL_RING_MODULATOR_SAWTOOTH) + mGetSamples = Modulate<Saw>; + else /*if(props->Modulator.Waveform == AL_RING_MODULATOR_SQUARE)*/ + mGetSamples = Modulate<Square>; + + ALfloat f0norm{props->Modulator.HighPassCutoff / static_cast<ALfloat>(device->Frequency)}; + f0norm = clampf(f0norm, 1.0f/512.0f, 0.49f); + /* Bandwidth value is constant in octaves. */ + mChans[0].Filter.setParams(BiquadType::HighPass, 1.0f, f0norm, + BiquadFilter::rcpQFromBandwidth(f0norm, 0.75f)); + for(size_t i{1u};i < slot->Wet.Buffer.size();++i) + mChans[i].Filter.copyParamsFrom(mChans[0].Filter); + + mOutTarget = target.Main->Buffer; + for(size_t i{0u};i < slot->Wet.Buffer.size();++i) + { + auto coeffs = GetAmbiIdentityRow(i); + ComputePanGains(target.Main, coeffs.data(), slot->Params.Gain, mChans[i].TargetGains); + } +} + +void ModulatorState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) +{ + for(ALsizei base{0};base < samplesToDo;) + { + alignas(16) ALfloat modsamples[MAX_UPDATE_SAMPLES]; + ALsizei td = mini(MAX_UPDATE_SAMPLES, samplesToDo-base); + ALsizei c, i; + + mGetSamples(modsamples, mIndex, mStep, td); + mIndex += (mStep*td) & WAVEFORM_FRACMASK; + mIndex &= WAVEFORM_FRACMASK; + + ASSUME(numInput > 0); + for(c = 0;c < numInput;c++) + { + alignas(16) ALfloat temps[MAX_UPDATE_SAMPLES]; + + mChans[c].Filter.process(temps, &samplesIn[c][base], td); + for(i = 0;i < td;i++) + temps[i] *= modsamples[i]; + + MixSamples(temps, samplesOut, mChans[c].CurrentGains, mChans[c].TargetGains, + samplesToDo-base, base, td); + } + + base += td; + } +} + + +void Modulator_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_RING_MODULATOR_FREQUENCY: + if(!(val >= AL_RING_MODULATOR_MIN_FREQUENCY && val <= AL_RING_MODULATOR_MAX_FREQUENCY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Modulator frequency out of range"); + props->Modulator.Frequency = val; + break; + + case AL_RING_MODULATOR_HIGHPASS_CUTOFF: + if(!(val >= AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF && val <= AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Modulator high-pass cutoff out of range"); + props->Modulator.HighPassCutoff = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid modulator float property 0x%04x", param); + } +} +void Modulator_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Modulator_setParamf(props, context, param, vals[0]); } +void Modulator_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_RING_MODULATOR_FREQUENCY: + case AL_RING_MODULATOR_HIGHPASS_CUTOFF: + Modulator_setParamf(props, context, param, static_cast<ALfloat>(val)); + break; + + case AL_RING_MODULATOR_WAVEFORM: + if(!(val >= AL_RING_MODULATOR_MIN_WAVEFORM && val <= AL_RING_MODULATOR_MAX_WAVEFORM)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Invalid modulator waveform"); + props->Modulator.Waveform = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid modulator integer property 0x%04x", param); + } +} +void Modulator_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ Modulator_setParami(props, context, param, vals[0]); } + +void Modulator_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_RING_MODULATOR_FREQUENCY: + *val = static_cast<ALint>(props->Modulator.Frequency); + break; + case AL_RING_MODULATOR_HIGHPASS_CUTOFF: + *val = static_cast<ALint>(props->Modulator.HighPassCutoff); + break; + case AL_RING_MODULATOR_WAVEFORM: + *val = props->Modulator.Waveform; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid modulator integer property 0x%04x", param); + } +} +void Modulator_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ Modulator_getParami(props, context, param, vals); } +void Modulator_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_RING_MODULATOR_FREQUENCY: + *val = props->Modulator.Frequency; + break; + case AL_RING_MODULATOR_HIGHPASS_CUTOFF: + *val = props->Modulator.HighPassCutoff; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid modulator float property 0x%04x", param); + } +} +void Modulator_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Modulator_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Modulator); + + +struct ModulatorStateFactory final : public EffectStateFactory { + EffectState *create() override { return new ModulatorState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Modulator_vtable; } +}; + +EffectProps ModulatorStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Modulator.Frequency = AL_RING_MODULATOR_DEFAULT_FREQUENCY; + props.Modulator.HighPassCutoff = AL_RING_MODULATOR_DEFAULT_HIGHPASS_CUTOFF; + props.Modulator.Waveform = AL_RING_MODULATOR_DEFAULT_WAVEFORM; + return props; +} + +} // namespace + +EffectStateFactory *ModulatorStateFactory_getFactory() +{ + static ModulatorStateFactory ModulatorFactory{}; + return &ModulatorFactory; +} diff --git a/alc/effects/null.cpp b/alc/effects/null.cpp new file mode 100644 index 00000000..e55c8699 --- /dev/null +++ b/alc/effects/null.cpp @@ -0,0 +1,164 @@ +#include "config.h" + +#include <cstdlib> + +#include "AL/al.h" +#include "AL/alc.h" + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" + + +namespace { + +struct NullState final : public EffectState { + NullState(); + ~NullState() override; + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(NullState) +}; + +/* This constructs the effect state. It's called when the object is first + * created. + */ +NullState::NullState() = default; + +/* This destructs the effect state. It's called only when the effect instance + * is no longer used. + */ +NullState::~NullState() = default; + +/* This updates the device-dependant effect state. This is called on state + * initialization and any time the device parameters (e.g. playback frequency, + * format) have been changed. Will always be followed by a call to the update + * method, if successful. + */ +ALboolean NullState::deviceUpdate(const ALCdevice* /*device*/) +{ + return AL_TRUE; +} + +/* This updates the effect state with new properties. This is called any time + * the effect is (re)loaded into a slot. + */ +void NullState::update(const ALCcontext* /*context*/, const ALeffectslot* /*slot*/, + const EffectProps* /*props*/, const EffectTarget /*target*/) +{ +} + +/* This processes the effect state, for the given number of samples from the + * input to the output buffer. The result should be added to the output buffer, + * not replace it. + */ +void NullState::process(const ALsizei /*samplesToDo*/, + const FloatBufferLine *RESTRICT /*samplesIn*/, const ALsizei /*numInput*/, + const al::span<FloatBufferLine> /*samplesOut*/) +{ +} + + +void NullEffect_setParami(EffectProps* /*props*/, ALCcontext *context, ALenum param, ALint /*val*/) +{ + switch(param) + { + default: + alSetError(context, AL_INVALID_ENUM, "Invalid null effect integer property 0x%04x", param); + } +} +void NullEffect_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ + switch(param) + { + default: + NullEffect_setParami(props, context, param, vals[0]); + } +} +void NullEffect_setParamf(EffectProps* /*props*/, ALCcontext *context, ALenum param, ALfloat /*val*/) +{ + switch(param) + { + default: + alSetError(context, AL_INVALID_ENUM, "Invalid null effect float property 0x%04x", param); + } +} +void NullEffect_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ + switch(param) + { + default: + NullEffect_setParamf(props, context, param, vals[0]); + } +} + +void NullEffect_getParami(const EffectProps* /*props*/, ALCcontext *context, ALenum param, ALint* /*val*/) +{ + switch(param) + { + default: + alSetError(context, AL_INVALID_ENUM, "Invalid null effect integer property 0x%04x", param); + } +} +void NullEffect_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ + switch(param) + { + default: + NullEffect_getParami(props, context, param, vals); + } +} +void NullEffect_getParamf(const EffectProps* /*props*/, ALCcontext *context, ALenum param, ALfloat* /*val*/) +{ + switch(param) + { + default: + alSetError(context, AL_INVALID_ENUM, "Invalid null effect float property 0x%04x", param); + } +} +void NullEffect_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ + switch(param) + { + default: + NullEffect_getParamf(props, context, param, vals); + } +} + +DEFINE_ALEFFECT_VTABLE(NullEffect); + + +struct NullStateFactory final : public EffectStateFactory { + EffectState *create() override; + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override; +}; + +/* Creates EffectState objects of the appropriate type. */ +EffectState *NullStateFactory::create() +{ return new NullState{}; } + +/* Returns an ALeffectProps initialized with this effect type's default + * property values. + */ +EffectProps NullStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + return props; +} + +/* Returns a pointer to this effect type's global set/get vtable. */ +const EffectVtable *NullStateFactory::getEffectVtable() const noexcept +{ return &NullEffect_vtable; } + +} // namespace + +EffectStateFactory *NullStateFactory_getFactory() +{ + static NullStateFactory NullFactory{}; + return &NullFactory; +} diff --git a/alc/effects/pshifter.cpp b/alc/effects/pshifter.cpp new file mode 100644 index 00000000..39d3cf1a --- /dev/null +++ b/alc/effects/pshifter.cpp @@ -0,0 +1,405 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2018 by Raul Herraiz. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#ifdef HAVE_SSE_INTRINSICS +#include <emmintrin.h> +#endif + +#include <cmath> +#include <cstdlib> +#include <array> +#include <complex> +#include <algorithm> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" + +#include "alcomplex.h" + + +namespace { + +using complex_d = std::complex<double>; + +#define STFT_SIZE 1024 +#define STFT_HALF_SIZE (STFT_SIZE>>1) +#define OVERSAMP (1<<2) + +#define STFT_STEP (STFT_SIZE / OVERSAMP) +#define FIFO_LATENCY (STFT_STEP * (OVERSAMP-1)) + +inline int double2int(double d) +{ +#if defined(HAVE_SSE_INTRINSICS) + return _mm_cvttsd_si32(_mm_set_sd(d)); + +#elif ((defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__)) && \ + !defined(__SSE2_MATH__)) || (defined(_MSC_VER) && defined(_M_IX86_FP) && _M_IX86_FP < 2) + + int sign, shift; + int64_t mant; + union { + double d; + int64_t i64; + } conv; + + conv.d = d; + sign = (conv.i64>>63) | 1; + shift = ((conv.i64>>52)&0x7ff) - (1023+52); + + /* Over/underflow */ + if(UNLIKELY(shift >= 63 || shift < -52)) + return 0; + + mant = (conv.i64&0xfffffffffffff_i64) | 0x10000000000000_i64; + if(LIKELY(shift < 0)) + return (int)(mant >> -shift) * sign; + return (int)(mant << shift) * sign; + +#else + + return static_cast<int>(d); +#endif +} + +/* Define a Hann window, used to filter the STFT input and output. */ +/* Making this constexpr seems to require C++14. */ +std::array<ALdouble,STFT_SIZE> InitHannWindow() +{ + std::array<ALdouble,STFT_SIZE> ret; + /* Create lookup table of the Hann window for the desired size, i.e. HIL_SIZE */ + for(ALsizei i{0};i < STFT_SIZE>>1;i++) + { + ALdouble val = std::sin(al::MathDefs<double>::Pi() * i / ALdouble{STFT_SIZE-1}); + ret[i] = ret[STFT_SIZE-1-i] = val * val; + } + return ret; +} +alignas(16) const std::array<ALdouble,STFT_SIZE> HannWindow = InitHannWindow(); + + +struct ALphasor { + ALdouble Amplitude; + ALdouble Phase; +}; + +struct ALfrequencyDomain { + ALdouble Amplitude; + ALdouble Frequency; +}; + + +/* Converts complex to ALphasor */ +inline ALphasor rect2polar(const complex_d &number) +{ + ALphasor polar; + polar.Amplitude = std::abs(number); + polar.Phase = std::arg(number); + return polar; +} + +/* Converts ALphasor to complex */ +inline complex_d polar2rect(const ALphasor &number) +{ return std::polar<double>(number.Amplitude, number.Phase); } + + +struct PshifterState final : public EffectState { + /* Effect parameters */ + ALsizei mCount; + ALsizei mPitchShiftI; + ALfloat mPitchShift; + ALfloat mFreqPerBin; + + /* Effects buffers */ + ALfloat mInFIFO[STFT_SIZE]; + ALfloat mOutFIFO[STFT_STEP]; + ALdouble mLastPhase[STFT_HALF_SIZE+1]; + ALdouble mSumPhase[STFT_HALF_SIZE+1]; + ALdouble mOutputAccum[STFT_SIZE]; + + complex_d mFFTbuffer[STFT_SIZE]; + + ALfrequencyDomain mAnalysis_buffer[STFT_HALF_SIZE+1]; + ALfrequencyDomain mSyntesis_buffer[STFT_HALF_SIZE+1]; + + alignas(16) ALfloat mBufferOut[BUFFERSIZE]; + + /* Effect gains for each output channel */ + ALfloat mCurrentGains[MAX_OUTPUT_CHANNELS]; + ALfloat mTargetGains[MAX_OUTPUT_CHANNELS]; + + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(PshifterState) +}; + +ALboolean PshifterState::deviceUpdate(const ALCdevice *device) +{ + /* (Re-)initializing parameters and clear the buffers. */ + mCount = FIFO_LATENCY; + mPitchShiftI = FRACTIONONE; + mPitchShift = 1.0f; + mFreqPerBin = device->Frequency / static_cast<ALfloat>(STFT_SIZE); + + std::fill(std::begin(mInFIFO), std::end(mInFIFO), 0.0f); + std::fill(std::begin(mOutFIFO), std::end(mOutFIFO), 0.0f); + std::fill(std::begin(mLastPhase), std::end(mLastPhase), 0.0); + std::fill(std::begin(mSumPhase), std::end(mSumPhase), 0.0); + std::fill(std::begin(mOutputAccum), std::end(mOutputAccum), 0.0); + std::fill(std::begin(mFFTbuffer), std::end(mFFTbuffer), complex_d{}); + std::fill(std::begin(mAnalysis_buffer), std::end(mAnalysis_buffer), ALfrequencyDomain{}); + std::fill(std::begin(mSyntesis_buffer), std::end(mSyntesis_buffer), ALfrequencyDomain{}); + + std::fill(std::begin(mCurrentGains), std::end(mCurrentGains), 0.0f); + std::fill(std::begin(mTargetGains), std::end(mTargetGains), 0.0f); + + return AL_TRUE; +} + +void PshifterState::update(const ALCcontext*, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const float pitch{std::pow(2.0f, + static_cast<ALfloat>(props->Pshifter.CoarseTune*100 + props->Pshifter.FineTune) / 1200.0f + )}; + mPitchShiftI = fastf2i(pitch*FRACTIONONE); + mPitchShift = mPitchShiftI * (1.0f/FRACTIONONE); + + ALfloat coeffs[MAX_AMBI_CHANNELS]; + CalcDirectionCoeffs({0.0f, 0.0f, -1.0f}, 0.0f, coeffs); + + mOutTarget = target.Main->Buffer; + ComputePanGains(target.Main, coeffs, slot->Params.Gain, mTargetGains); +} + +void PshifterState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei /*numInput*/, const al::span<FloatBufferLine> samplesOut) +{ + /* Pitch shifter engine based on the work of Stephan Bernsee. + * http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ + */ + + static constexpr ALdouble expected{al::MathDefs<double>::Tau() / OVERSAMP}; + const ALdouble freq_per_bin{mFreqPerBin}; + ALfloat *RESTRICT bufferOut{mBufferOut}; + ALsizei count{mCount}; + + for(ALsizei i{0};i < samplesToDo;) + { + do { + /* Fill FIFO buffer with samples data */ + mInFIFO[count] = samplesIn[0][i]; + bufferOut[i] = mOutFIFO[count - FIFO_LATENCY]; + + count++; + } while(++i < samplesToDo && count < STFT_SIZE); + + /* Check whether FIFO buffer is filled */ + if(count < STFT_SIZE) break; + count = FIFO_LATENCY; + + /* Real signal windowing and store in FFTbuffer */ + for(ALsizei k{0};k < STFT_SIZE;k++) + { + mFFTbuffer[k].real(mInFIFO[k] * HannWindow[k]); + mFFTbuffer[k].imag(0.0); + } + + /* ANALYSIS */ + /* Apply FFT to FFTbuffer data */ + complex_fft(mFFTbuffer, -1.0); + + /* Analyze the obtained data. Since the real FFT is symmetric, only + * STFT_HALF_SIZE+1 samples are needed. + */ + for(ALsizei k{0};k < STFT_HALF_SIZE+1;k++) + { + /* Compute amplitude and phase */ + ALphasor component{rect2polar(mFFTbuffer[k])}; + + /* Compute phase difference and subtract expected phase difference */ + double tmp{(component.Phase - mLastPhase[k]) - k*expected}; + + /* Map delta phase into +/- Pi interval */ + int qpd{double2int(tmp / al::MathDefs<double>::Pi())}; + tmp -= al::MathDefs<double>::Pi() * (qpd + (qpd%2)); + + /* Get deviation from bin frequency from the +/- Pi interval */ + tmp /= expected; + + /* Compute the k-th partials' true frequency, twice the amplitude + * for maintain the gain (because half of bins are used) and store + * amplitude and true frequency in analysis buffer. + */ + mAnalysis_buffer[k].Amplitude = 2.0 * component.Amplitude; + mAnalysis_buffer[k].Frequency = (k + tmp) * freq_per_bin; + + /* Store actual phase[k] for the calculations in the next frame*/ + mLastPhase[k] = component.Phase; + } + + /* PROCESSING */ + /* pitch shifting */ + for(ALsizei k{0};k < STFT_HALF_SIZE+1;k++) + { + mSyntesis_buffer[k].Amplitude = 0.0; + mSyntesis_buffer[k].Frequency = 0.0; + } + + for(ALsizei k{0};k < STFT_HALF_SIZE+1;k++) + { + ALsizei j{(k*mPitchShiftI) >> FRACTIONBITS}; + if(j >= STFT_HALF_SIZE+1) break; + + mSyntesis_buffer[j].Amplitude += mAnalysis_buffer[k].Amplitude; + mSyntesis_buffer[j].Frequency = mAnalysis_buffer[k].Frequency * mPitchShift; + } + + /* SYNTHESIS */ + /* Synthesis the processing data */ + for(ALsizei k{0};k < STFT_HALF_SIZE+1;k++) + { + ALphasor component; + ALdouble tmp; + + /* Compute bin deviation from scaled freq */ + tmp = mSyntesis_buffer[k].Frequency/freq_per_bin - k; + + /* Calculate actual delta phase and accumulate it to get bin phase */ + mSumPhase[k] += (k + tmp) * expected; + + component.Amplitude = mSyntesis_buffer[k].Amplitude; + component.Phase = mSumPhase[k]; + + /* Compute phasor component to cartesian complex number and storage it into FFTbuffer*/ + mFFTbuffer[k] = polar2rect(component); + } + /* zero negative frequencies for recontruct a real signal */ + for(ALsizei k{STFT_HALF_SIZE+1};k < STFT_SIZE;k++) + mFFTbuffer[k] = complex_d{}; + + /* Apply iFFT to buffer data */ + complex_fft(mFFTbuffer, 1.0); + + /* Windowing and add to output */ + for(ALsizei k{0};k < STFT_SIZE;k++) + mOutputAccum[k] += HannWindow[k] * mFFTbuffer[k].real() / + (0.5 * STFT_HALF_SIZE * OVERSAMP); + + /* Shift accumulator, input & output FIFO */ + ALsizei j, k; + for(k = 0;k < STFT_STEP;k++) mOutFIFO[k] = static_cast<ALfloat>(mOutputAccum[k]); + for(j = 0;k < STFT_SIZE;k++,j++) mOutputAccum[j] = mOutputAccum[k]; + for(;j < STFT_SIZE;j++) mOutputAccum[j] = 0.0; + for(k = 0;k < FIFO_LATENCY;k++) + mInFIFO[k] = mInFIFO[k+STFT_STEP]; + } + mCount = count; + + /* Now, mix the processed sound data to the output. */ + MixSamples(bufferOut, samplesOut, mCurrentGains, mTargetGains, maxi(samplesToDo, 512), 0, + samplesToDo); +} + + +void Pshifter_setParamf(EffectProps*, ALCcontext *context, ALenum param, ALfloat) +{ alSetError(context, AL_INVALID_ENUM, "Invalid pitch shifter float property 0x%04x", param); } +void Pshifter_setParamfv(EffectProps*, ALCcontext *context, ALenum param, const ALfloat*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid pitch shifter float-vector property 0x%04x", param); } + +void Pshifter_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_PITCH_SHIFTER_COARSE_TUNE: + if(!(val >= AL_PITCH_SHIFTER_MIN_COARSE_TUNE && val <= AL_PITCH_SHIFTER_MAX_COARSE_TUNE)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Pitch shifter coarse tune out of range"); + props->Pshifter.CoarseTune = val; + break; + + case AL_PITCH_SHIFTER_FINE_TUNE: + if(!(val >= AL_PITCH_SHIFTER_MIN_FINE_TUNE && val <= AL_PITCH_SHIFTER_MAX_FINE_TUNE)) + SETERR_RETURN(context, AL_INVALID_VALUE,,"Pitch shifter fine tune out of range"); + props->Pshifter.FineTune = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid pitch shifter integer property 0x%04x", param); + } +} +void Pshifter_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ Pshifter_setParami(props, context, param, vals[0]); } + +void Pshifter_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_PITCH_SHIFTER_COARSE_TUNE: + *val = props->Pshifter.CoarseTune; + break; + case AL_PITCH_SHIFTER_FINE_TUNE: + *val = props->Pshifter.FineTune; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid pitch shifter integer property 0x%04x", param); + } +} +void Pshifter_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ Pshifter_getParami(props, context, param, vals); } + +void Pshifter_getParamf(const EffectProps*, ALCcontext *context, ALenum param, ALfloat*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid pitch shifter float property 0x%04x", param); } +void Pshifter_getParamfv(const EffectProps*, ALCcontext *context, ALenum param, ALfloat*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid pitch shifter float vector-property 0x%04x", param); } + +DEFINE_ALEFFECT_VTABLE(Pshifter); + + +struct PshifterStateFactory final : public EffectStateFactory { + EffectState *create() override; + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Pshifter_vtable; } +}; + +EffectState *PshifterStateFactory::create() +{ return new PshifterState{}; } + +EffectProps PshifterStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Pshifter.CoarseTune = AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE; + props.Pshifter.FineTune = AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE; + return props; +} + +} // namespace + +EffectStateFactory *PshifterStateFactory_getFactory() +{ + static PshifterStateFactory PshifterFactory{}; + return &PshifterFactory; +} diff --git a/alc/effects/reverb.cpp b/alc/effects/reverb.cpp new file mode 100644 index 00000000..ac996b3f --- /dev/null +++ b/alc/effects/reverb.cpp @@ -0,0 +1,2102 @@ +/** + * Ambisonic reverb engine for the OpenAL cross platform audio library + * Copyright (C) 2008-2017 by Chris Robinson and Christopher Fitzgerald. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cstdio> +#include <cstdlib> +#include <cmath> + +#include <array> +#include <numeric> +#include <algorithm> +#include <functional> + +#include "alcmain.h" +#include "alcontext.h" +#include "alu.h" +#include "alAuxEffectSlot.h" +#include "alListener.h" +#include "alError.h" +#include "bformatdec.h" +#include "filters/biquad.h" +#include "vector.h" +#include "vecmat.h" + +/* This is a user config option for modifying the overall output of the reverb + * effect. + */ +ALfloat ReverbBoost = 1.0f; + +namespace { + +using namespace std::placeholders; + +/* The number of samples used for cross-faded delay lines. This can be used + * to balance the compensation for abrupt line changes and attenuation due to + * minimally lengthed recursive lines. Try to keep this below the device + * update size. + */ +constexpr int FADE_SAMPLES{128}; + +/* The number of spatialized lines or channels to process. Four channels allows + * for a 3D A-Format response. NOTE: This can't be changed without taking care + * of the conversion matrices, and a few places where the length arrays are + * assumed to have 4 elements. + */ +constexpr int NUM_LINES{4}; + + +/* The B-Format to A-Format conversion matrix. The arrangement of rows is + * deliberately chosen to align the resulting lines to their spatial opposites + * (0:above front left <-> 3:above back right, 1:below front right <-> 2:below + * back left). It's not quite opposite, since the A-Format results in a + * tetrahedron, but it's close enough. Should the model be extended to 8-lines + * in the future, true opposites can be used. + */ +alignas(16) constexpr ALfloat B2A[NUM_LINES][MAX_AMBI_CHANNELS]{ + { 0.288675134595f, 0.288675134595f, 0.288675134595f, 0.288675134595f }, + { 0.288675134595f, -0.288675134595f, -0.288675134595f, 0.288675134595f }, + { 0.288675134595f, 0.288675134595f, -0.288675134595f, -0.288675134595f }, + { 0.288675134595f, -0.288675134595f, 0.288675134595f, -0.288675134595f } +}; + +/* Converts A-Format to B-Format. */ +alignas(16) constexpr ALfloat A2B[NUM_LINES][NUM_LINES]{ + { 0.866025403785f, 0.866025403785f, 0.866025403785f, 0.866025403785f }, + { 0.866025403785f, -0.866025403785f, 0.866025403785f, -0.866025403785f }, + { 0.866025403785f, -0.866025403785f, -0.866025403785f, 0.866025403785f }, + { 0.866025403785f, 0.866025403785f, -0.866025403785f, -0.866025403785f } +}; + + +constexpr ALfloat FadeStep{1.0f / FADE_SAMPLES}; + +/* The all-pass and delay lines have a variable length dependent on the + * effect's density parameter, which helps alter the perceived environment + * size. The size-to-density conversion is a cubed scale: + * + * density = min(1.0, pow(size, 3.0) / DENSITY_SCALE); + * + * The line lengths scale linearly with room size, so the inverse density + * conversion is needed, taking the cube root of the re-scaled density to + * calculate the line length multiplier: + * + * length_mult = max(5.0, cbrt(density*DENSITY_SCALE)); + * + * The density scale below will result in a max line multiplier of 50, for an + * effective size range of 5m to 50m. + */ +constexpr ALfloat DENSITY_SCALE{125000.0f}; + +/* All delay line lengths are specified in seconds. + * + * To approximate early reflections, we break them up into primary (those + * arriving from the same direction as the source) and secondary (those + * arriving from the opposite direction). + * + * The early taps decorrelate the 4-channel signal to approximate an average + * room response for the primary reflections after the initial early delay. + * + * Given an average room dimension (d_a) and the speed of sound (c) we can + * calculate the average reflection delay (r_a) regardless of listener and + * source positions as: + * + * r_a = d_a / c + * c = 343.3 + * + * This can extended to finding the average difference (r_d) between the + * maximum (r_1) and minimum (r_0) reflection delays: + * + * r_0 = 2 / 3 r_a + * = r_a - r_d / 2 + * = r_d + * r_1 = 4 / 3 r_a + * = r_a + r_d / 2 + * = 2 r_d + * r_d = 2 / 3 r_a + * = r_1 - r_0 + * + * As can be determined by integrating the 1D model with a source (s) and + * listener (l) positioned across the dimension of length (d_a): + * + * r_d = int_(l=0)^d_a (int_(s=0)^d_a |2 d_a - 2 (l + s)| ds) dl / c + * + * The initial taps (T_(i=0)^N) are then specified by taking a power series + * that ranges between r_0 and half of r_1 less r_0: + * + * R_i = 2^(i / (2 N - 1)) r_d + * = r_0 + (2^(i / (2 N - 1)) - 1) r_d + * = r_0 + T_i + * T_i = R_i - r_0 + * = (2^(i / (2 N - 1)) - 1) r_d + * + * Assuming an average of 1m, we get the following taps: + */ +constexpr std::array<ALfloat,NUM_LINES> EARLY_TAP_LENGTHS{{ + 0.0000000e+0f, 2.0213520e-4f, 4.2531060e-4f, 6.7171600e-4f +}}; + +/* The early all-pass filter lengths are based on the early tap lengths: + * + * A_i = R_i / a + * + * Where a is the approximate maximum all-pass cycle limit (20). + */ +constexpr std::array<ALfloat,NUM_LINES> EARLY_ALLPASS_LENGTHS{{ + 9.7096800e-5f, 1.0720356e-4f, 1.1836234e-4f, 1.3068260e-4f +}}; + +/* The early delay lines are used to transform the primary reflections into + * the secondary reflections. The A-format is arranged in such a way that + * the channels/lines are spatially opposite: + * + * C_i is opposite C_(N-i-1) + * + * The delays of the two opposing reflections (R_i and O_i) from a source + * anywhere along a particular dimension always sum to twice its full delay: + * + * 2 r_a = R_i + O_i + * + * With that in mind we can determine the delay between the two reflections + * and thus specify our early line lengths (L_(i=0)^N) using: + * + * O_i = 2 r_a - R_(N-i-1) + * L_i = O_i - R_(N-i-1) + * = 2 (r_a - R_(N-i-1)) + * = 2 (r_a - T_(N-i-1) - r_0) + * = 2 r_a (1 - (2 / 3) 2^((N - i - 1) / (2 N - 1))) + * + * Using an average dimension of 1m, we get: + */ +constexpr std::array<ALfloat,NUM_LINES> EARLY_LINE_LENGTHS{{ + 5.9850400e-4f, 1.0913150e-3f, 1.5376658e-3f, 1.9419362e-3f +}}; + +/* The late all-pass filter lengths are based on the late line lengths: + * + * A_i = (5 / 3) L_i / r_1 + */ +constexpr std::array<ALfloat,NUM_LINES> LATE_ALLPASS_LENGTHS{{ + 1.6182800e-4f, 2.0389060e-4f, 2.8159360e-4f, 3.2365600e-4f +}}; +constexpr auto LATE_ALLPASS_LENGTHS_size = LATE_ALLPASS_LENGTHS.size(); + +/* The late lines are used to approximate the decaying cycle of recursive + * late reflections. + * + * Splitting the lines in half, we start with the shortest reflection paths + * (L_(i=0)^(N/2)): + * + * L_i = 2^(i / (N - 1)) r_d + * + * Then for the opposite (longest) reflection paths (L_(i=N/2)^N): + * + * L_i = 2 r_a - L_(i-N/2) + * = 2 r_a - 2^((i - N / 2) / (N - 1)) r_d + * + * For our 1m average room, we get: + */ +constexpr std::array<ALfloat,NUM_LINES> LATE_LINE_LENGTHS{{ + 1.9419362e-3f, 2.4466860e-3f, 3.3791220e-3f, 3.8838720e-3f +}}; +constexpr auto LATE_LINE_LENGTHS_size = LATE_LINE_LENGTHS.size(); + + +struct DelayLineI { + /* The delay lines use interleaved samples, with the lengths being powers + * of 2 to allow the use of bit-masking instead of a modulus for wrapping. + */ + ALsizei Mask{0}; + ALfloat (*Line)[NUM_LINES]{nullptr}; + + + void write(ALsizei offset, const ALsizei c, const ALfloat *RESTRICT in, const ALsizei count) const noexcept + { + ASSUME(count > 0); + for(ALsizei i{0};i < count;) + { + offset &= Mask; + ALsizei td{mini(Mask+1 - offset, count - i)}; + do { + Line[offset++][c] = in[i++]; + } while(--td); + } + } +}; + +struct VecAllpass { + DelayLineI Delay; + ALfloat Coeff{0.0f}; + ALsizei Offset[NUM_LINES][2]{}; + + void processFaded(const al::span<FloatBufferLine,NUM_LINES> samples, ALsizei offset, + const ALfloat xCoeff, const ALfloat yCoeff, ALfloat fade, const ALsizei todo); + void processUnfaded(const al::span<FloatBufferLine,NUM_LINES> samples, ALsizei offset, + const ALfloat xCoeff, const ALfloat yCoeff, const ALsizei todo); +}; + +struct T60Filter { + /* Two filters are used to adjust the signal. One to control the low + * frequencies, and one to control the high frequencies. + */ + ALfloat MidGain[2]{0.0f, 0.0f}; + BiquadFilter HFFilter, LFFilter; + + void calcCoeffs(const ALfloat length, const ALfloat lfDecayTime, const ALfloat mfDecayTime, + const ALfloat hfDecayTime, const ALfloat lf0norm, const ALfloat hf0norm); + + /* Applies the two T60 damping filter sections. */ + void process(ALfloat *samples, const ALsizei todo) + { + HFFilter.process(samples, samples, todo); + LFFilter.process(samples, samples, todo); + } +}; + +struct EarlyReflections { + /* A Gerzon vector all-pass filter is used to simulate initial diffusion. + * The spread from this filter also helps smooth out the reverb tail. + */ + VecAllpass VecAp; + + /* An echo line is used to complete the second half of the early + * reflections. + */ + DelayLineI Delay; + ALsizei Offset[NUM_LINES][2]{}; + ALfloat Coeff[NUM_LINES][2]{}; + + /* The gain for each output channel based on 3D panning. */ + ALfloat CurrentGain[NUM_LINES][MAX_OUTPUT_CHANNELS]{}; + ALfloat PanGain[NUM_LINES][MAX_OUTPUT_CHANNELS]{}; + + void updateLines(const ALfloat density, const ALfloat diffusion, const ALfloat decayTime, + const ALfloat frequency); +}; + +struct LateReverb { + /* A recursive delay line is used fill in the reverb tail. */ + DelayLineI Delay; + ALsizei Offset[NUM_LINES][2]{}; + + /* Attenuation to compensate for the modal density and decay rate of the + * late lines. + */ + ALfloat DensityGain[2]{0.0f, 0.0f}; + + /* T60 decay filters are used to simulate absorption. */ + T60Filter T60[NUM_LINES]; + + /* A Gerzon vector all-pass filter is used to simulate diffusion. */ + VecAllpass VecAp; + + /* The gain for each output channel based on 3D panning. */ + ALfloat CurrentGain[NUM_LINES][MAX_OUTPUT_CHANNELS]{}; + ALfloat PanGain[NUM_LINES][MAX_OUTPUT_CHANNELS]{}; + + void updateLines(const ALfloat density, const ALfloat diffusion, const ALfloat lfDecayTime, + const ALfloat mfDecayTime, const ALfloat hfDecayTime, const ALfloat lf0norm, + const ALfloat hf0norm, const ALfloat frequency); +}; + +struct ReverbState final : public EffectState { + /* All delay lines are allocated as a single buffer to reduce memory + * fragmentation and management code. + */ + al::vector<ALfloat,16> mSampleBuffer; + + struct { + /* Calculated parameters which indicate if cross-fading is needed after + * an update. + */ + ALfloat Density{AL_EAXREVERB_DEFAULT_DENSITY}; + ALfloat Diffusion{AL_EAXREVERB_DEFAULT_DIFFUSION}; + ALfloat DecayTime{AL_EAXREVERB_DEFAULT_DECAY_TIME}; + ALfloat HFDecayTime{AL_EAXREVERB_DEFAULT_DECAY_HFRATIO * AL_EAXREVERB_DEFAULT_DECAY_TIME}; + ALfloat LFDecayTime{AL_EAXREVERB_DEFAULT_DECAY_LFRATIO * AL_EAXREVERB_DEFAULT_DECAY_TIME}; + ALfloat HFReference{AL_EAXREVERB_DEFAULT_HFREFERENCE}; + ALfloat LFReference{AL_EAXREVERB_DEFAULT_LFREFERENCE}; + } mParams; + + /* Master effect filters */ + struct { + BiquadFilter Lp; + BiquadFilter Hp; + } mFilter[NUM_LINES]; + + /* Core delay line (early reflections and late reverb tap from this). */ + DelayLineI mDelay; + + /* Tap points for early reflection delay. */ + ALsizei mEarlyDelayTap[NUM_LINES][2]{}; + ALfloat mEarlyDelayCoeff[NUM_LINES][2]{}; + + /* Tap points for late reverb feed and delay. */ + ALsizei mLateFeedTap{}; + ALsizei mLateDelayTap[NUM_LINES][2]{}; + + /* Coefficients for the all-pass and line scattering matrices. */ + ALfloat mMixX{0.0f}; + ALfloat mMixY{0.0f}; + + EarlyReflections mEarly; + + LateReverb mLate; + + /* Indicates the cross-fade point for delay line reads [0,FADE_SAMPLES]. */ + ALsizei mFadeCount{0}; + + /* Maximum number of samples to process at once. */ + ALsizei mMaxUpdate[2]{BUFFERSIZE, BUFFERSIZE}; + + /* The current write offset for all delay lines. */ + ALsizei mOffset{0}; + + /* Temporary storage used when processing. */ + alignas(16) std::array<FloatBufferLine,NUM_LINES> mTempSamples{}; + alignas(16) std::array<FloatBufferLine,NUM_LINES> mEarlyBuffer{}; + alignas(16) std::array<FloatBufferLine,NUM_LINES> mLateBuffer{}; + + using MixOutT = void (ReverbState::*)(const al::span<FloatBufferLine> samplesOut, + const ALsizei todo); + + MixOutT mMixOut{&ReverbState::MixOutPlain}; + std::array<ALfloat,MAX_AMBI_ORDER+1> mOrderScales{}; + std::array<std::array<BandSplitter,NUM_LINES>,2> mAmbiSplitter; + + + void MixOutPlain(const al::span<FloatBufferLine> samplesOut, const ALsizei todo) + { + ASSUME(todo > 0); + + /* Convert back to B-Format, and mix the results to output. */ + for(ALsizei c{0};c < NUM_LINES;c++) + { + std::fill_n(mTempSamples[0].begin(), todo, 0.0f); + MixRowSamples(mTempSamples[0], A2B[c], mEarlyBuffer, 0, todo); + MixSamples(mTempSamples[0].data(), samplesOut, mEarly.CurrentGain[c], + mEarly.PanGain[c], todo, 0, todo); + } + + for(ALsizei c{0};c < NUM_LINES;c++) + { + std::fill_n(mTempSamples[0].begin(), todo, 0.0f); + MixRowSamples(mTempSamples[0], A2B[c], mLateBuffer, 0, todo); + MixSamples(mTempSamples[0].data(), samplesOut, mLate.CurrentGain[c], mLate.PanGain[c], + todo, 0, todo); + } + } + + void MixOutAmbiUp(const al::span<FloatBufferLine> samplesOut, const ALsizei todo) + { + ASSUME(todo > 0); + + for(ALsizei c{0};c < NUM_LINES;c++) + { + std::fill_n(mTempSamples[0].begin(), todo, 0.0f); + MixRowSamples(mTempSamples[0], A2B[c], mEarlyBuffer, 0, todo); + + /* Apply scaling to the B-Format's HF response to "upsample" it to + * higher-order output. + */ + const ALfloat hfscale{(c==0) ? mOrderScales[0] : mOrderScales[1]}; + mAmbiSplitter[0][c].applyHfScale(mTempSamples[0].data(), hfscale, todo); + + MixSamples(mTempSamples[0].data(), samplesOut, mEarly.CurrentGain[c], + mEarly.PanGain[c], todo, 0, todo); + } + + for(ALsizei c{0};c < NUM_LINES;c++) + { + std::fill_n(mTempSamples[0].begin(), todo, 0.0f); + MixRowSamples(mTempSamples[0], A2B[c], mLateBuffer, 0, todo); + + const ALfloat hfscale{(c==0) ? mOrderScales[0] : mOrderScales[1]}; + mAmbiSplitter[1][c].applyHfScale(mTempSamples[0].data(), hfscale, todo); + + MixSamples(mTempSamples[0].data(), samplesOut, mLate.CurrentGain[c], mLate.PanGain[c], + todo, 0, todo); + } + } + + bool allocLines(const ALfloat frequency); + + void updateDelayLine(const ALfloat earlyDelay, const ALfloat lateDelay, const ALfloat density, + const ALfloat decayTime, const ALfloat frequency); + void update3DPanning(const ALfloat *ReflectionsPan, const ALfloat *LateReverbPan, + const ALfloat earlyGain, const ALfloat lateGain, const EffectTarget &target); + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + DEF_NEWDEL(ReverbState) +}; + +/************************************** + * Device Update * + **************************************/ + +inline ALfloat CalcDelayLengthMult(ALfloat density) +{ return maxf(5.0f, std::cbrt(density*DENSITY_SCALE)); } + +/* Given the allocated sample buffer, this function updates each delay line + * offset. + */ +inline ALvoid RealizeLineOffset(ALfloat *sampleBuffer, DelayLineI *Delay) +{ + union { + ALfloat *f; + ALfloat (*f4)[NUM_LINES]; + } u; + u.f = &sampleBuffer[reinterpret_cast<ptrdiff_t>(Delay->Line) * NUM_LINES]; + Delay->Line = u.f4; +} + +/* Calculate the length of a delay line and store its mask and offset. */ +ALuint CalcLineLength(const ALfloat length, const ptrdiff_t offset, const ALfloat frequency, + const ALuint extra, DelayLineI *Delay) +{ + /* All line lengths are powers of 2, calculated from their lengths in + * seconds, rounded up. + */ + auto samples = static_cast<ALuint>(float2int(std::ceil(length*frequency))); + samples = NextPowerOf2(samples + extra); + + /* All lines share a single sample buffer. */ + Delay->Mask = samples - 1; + Delay->Line = reinterpret_cast<ALfloat(*)[NUM_LINES]>(offset); + + /* Return the sample count for accumulation. */ + return samples; +} + +/* Calculates the delay line metrics and allocates the shared sample buffer + * for all lines given the sample rate (frequency). If an allocation failure + * occurs, it returns AL_FALSE. + */ +bool ReverbState::allocLines(const ALfloat frequency) +{ + /* All delay line lengths are calculated to accomodate the full range of + * lengths given their respective paramters. + */ + ALuint totalSamples{0u}; + + /* Multiplier for the maximum density value, i.e. density=1, which is + * actually the least density... + */ + ALfloat multiplier{CalcDelayLengthMult(AL_EAXREVERB_MAX_DENSITY)}; + + /* The main delay length includes the maximum early reflection delay, the + * largest early tap width, the maximum late reverb delay, and the + * largest late tap width. Finally, it must also be extended by the + * update size (BUFFERSIZE) for block processing. + */ + ALfloat length{AL_EAXREVERB_MAX_REFLECTIONS_DELAY + EARLY_TAP_LENGTHS.back()*multiplier + + AL_EAXREVERB_MAX_LATE_REVERB_DELAY + + (LATE_LINE_LENGTHS.back() - LATE_LINE_LENGTHS.front())/float{LATE_LINE_LENGTHS_size}*multiplier}; + totalSamples += CalcLineLength(length, totalSamples, frequency, BUFFERSIZE, &mDelay); + + /* The early vector all-pass line. */ + length = EARLY_ALLPASS_LENGTHS.back() * multiplier; + totalSamples += CalcLineLength(length, totalSamples, frequency, 0, &mEarly.VecAp.Delay); + + /* The early reflection line. */ + length = EARLY_LINE_LENGTHS.back() * multiplier; + totalSamples += CalcLineLength(length, totalSamples, frequency, 0, &mEarly.Delay); + + /* The late vector all-pass line. */ + length = LATE_ALLPASS_LENGTHS.back() * multiplier; + totalSamples += CalcLineLength(length, totalSamples, frequency, 0, &mLate.VecAp.Delay); + + /* The late delay lines are calculated from the largest maximum density + * line length. + */ + length = LATE_LINE_LENGTHS.back() * multiplier; + totalSamples += CalcLineLength(length, totalSamples, frequency, 0, &mLate.Delay); + + totalSamples *= NUM_LINES; + if(totalSamples != mSampleBuffer.size()) + { + mSampleBuffer.resize(totalSamples); + mSampleBuffer.shrink_to_fit(); + } + + /* Clear the sample buffer. */ + std::fill(mSampleBuffer.begin(), mSampleBuffer.end(), 0.0f); + + /* Update all delays to reflect the new sample buffer. */ + RealizeLineOffset(mSampleBuffer.data(), &mDelay); + RealizeLineOffset(mSampleBuffer.data(), &mEarly.VecAp.Delay); + RealizeLineOffset(mSampleBuffer.data(), &mEarly.Delay); + RealizeLineOffset(mSampleBuffer.data(), &mLate.VecAp.Delay); + RealizeLineOffset(mSampleBuffer.data(), &mLate.Delay); + + return true; +} + +ALboolean ReverbState::deviceUpdate(const ALCdevice *device) +{ + const auto frequency = static_cast<ALfloat>(device->Frequency); + + /* Allocate the delay lines. */ + if(!allocLines(frequency)) + return AL_FALSE; + + const ALfloat multiplier{CalcDelayLengthMult(AL_EAXREVERB_MAX_DENSITY)}; + + /* The late feed taps are set a fixed position past the latest delay tap. */ + mLateFeedTap = float2int( + (AL_EAXREVERB_MAX_REFLECTIONS_DELAY + EARLY_TAP_LENGTHS.back()*multiplier) * frequency); + + /* Clear filters and gain coefficients since the delay lines were all just + * cleared (if not reallocated). + */ + for(auto &filter : mFilter) + { + filter.Lp.clear(); + filter.Hp.clear(); + } + + for(auto &coeff : mEarlyDelayCoeff) + std::fill(std::begin(coeff), std::end(coeff), 0.0f); + for(auto &coeff : mEarly.Coeff) + std::fill(std::begin(coeff), std::end(coeff), 0.0f); + + mLate.DensityGain[0] = 0.0f; + mLate.DensityGain[1] = 0.0f; + for(auto &t60 : mLate.T60) + { + t60.MidGain[0] = 0.0f; + t60.MidGain[1] = 0.0f; + t60.HFFilter.clear(); + t60.LFFilter.clear(); + } + + for(auto &gains : mEarly.CurrentGain) + std::fill(std::begin(gains), std::end(gains), 0.0f); + for(auto &gains : mEarly.PanGain) + std::fill(std::begin(gains), std::end(gains), 0.0f); + for(auto &gains : mLate.CurrentGain) + std::fill(std::begin(gains), std::end(gains), 0.0f); + for(auto &gains : mLate.PanGain) + std::fill(std::begin(gains), std::end(gains), 0.0f); + + /* Reset counters and offset base. */ + mFadeCount = 0; + std::fill(std::begin(mMaxUpdate), std::end(mMaxUpdate), BUFFERSIZE); + mOffset = 0; + + if(device->mAmbiOrder > 1) + { + mMixOut = &ReverbState::MixOutAmbiUp; + mOrderScales = BFormatDec::GetHFOrderScales(1, device->mAmbiOrder); + } + else + { + mMixOut = &ReverbState::MixOutPlain; + mOrderScales.fill(1.0f); + } + mAmbiSplitter[0][0].init(400.0f / frequency); + std::fill(mAmbiSplitter[0].begin()+1, mAmbiSplitter[0].end(), mAmbiSplitter[0][0]); + std::fill(mAmbiSplitter[1].begin(), mAmbiSplitter[1].end(), mAmbiSplitter[0][0]); + + return AL_TRUE; +} + +/************************************** + * Effect Update * + **************************************/ + +/* Calculate a decay coefficient given the length of each cycle and the time + * until the decay reaches -60 dB. + */ +inline ALfloat CalcDecayCoeff(const ALfloat length, const ALfloat decayTime) +{ return std::pow(REVERB_DECAY_GAIN, length/decayTime); } + +/* Calculate a decay length from a coefficient and the time until the decay + * reaches -60 dB. + */ +inline ALfloat CalcDecayLength(const ALfloat coeff, const ALfloat decayTime) +{ return std::log10(coeff) * decayTime / std::log10(REVERB_DECAY_GAIN); } + +/* Calculate an attenuation to be applied to the input of any echo models to + * compensate for modal density and decay time. + */ +inline ALfloat CalcDensityGain(const ALfloat a) +{ + /* The energy of a signal can be obtained by finding the area under the + * squared signal. This takes the form of Sum(x_n^2), where x is the + * amplitude for the sample n. + * + * Decaying feedback matches exponential decay of the form Sum(a^n), + * where a is the attenuation coefficient, and n is the sample. The area + * under this decay curve can be calculated as: 1 / (1 - a). + * + * Modifying the above equation to find the area under the squared curve + * (for energy) yields: 1 / (1 - a^2). Input attenuation can then be + * calculated by inverting the square root of this approximation, + * yielding: 1 / sqrt(1 / (1 - a^2)), simplified to: sqrt(1 - a^2). + */ + return std::sqrt(1.0f - a*a); +} + +/* Calculate the scattering matrix coefficients given a diffusion factor. */ +inline ALvoid CalcMatrixCoeffs(const ALfloat diffusion, ALfloat *x, ALfloat *y) +{ + /* The matrix is of order 4, so n is sqrt(4 - 1). */ + ALfloat n{std::sqrt(3.0f)}; + ALfloat t{diffusion * std::atan(n)}; + + /* Calculate the first mixing matrix coefficient. */ + *x = std::cos(t); + /* Calculate the second mixing matrix coefficient. */ + *y = std::sin(t) / n; +} + +/* Calculate the limited HF ratio for use with the late reverb low-pass + * filters. + */ +ALfloat CalcLimitedHfRatio(const ALfloat hfRatio, const ALfloat airAbsorptionGainHF, + const ALfloat decayTime, const ALfloat SpeedOfSound) +{ + /* Find the attenuation due to air absorption in dB (converting delay + * time to meters using the speed of sound). Then reversing the decay + * equation, solve for HF ratio. The delay length is cancelled out of + * the equation, so it can be calculated once for all lines. + */ + ALfloat limitRatio{1.0f / (CalcDecayLength(airAbsorptionGainHF, decayTime) * SpeedOfSound)}; + + /* Using the limit calculated above, apply the upper bound to the HF ratio. + */ + return minf(limitRatio, hfRatio); +} + + +/* Calculates the 3-band T60 damping coefficients for a particular delay line + * of specified length, using a combination of two shelf filter sections given + * decay times for each band split at two reference frequencies. + */ +void T60Filter::calcCoeffs(const ALfloat length, const ALfloat lfDecayTime, + const ALfloat mfDecayTime, const ALfloat hfDecayTime, const ALfloat lf0norm, + const ALfloat hf0norm) +{ + const ALfloat mfGain{CalcDecayCoeff(length, mfDecayTime)}; + const ALfloat lfGain{maxf(CalcDecayCoeff(length, lfDecayTime)/mfGain, 0.001f)}; + const ALfloat hfGain{maxf(CalcDecayCoeff(length, hfDecayTime)/mfGain, 0.001f)}; + + MidGain[1] = mfGain; + LFFilter.setParams(BiquadType::LowShelf, lfGain, lf0norm, + LFFilter.rcpQFromSlope(lfGain, 1.0f)); + HFFilter.setParams(BiquadType::HighShelf, hfGain, hf0norm, + HFFilter.rcpQFromSlope(hfGain, 1.0f)); +} + +/* Update the early reflection line lengths and gain coefficients. */ +void EarlyReflections::updateLines(const ALfloat density, const ALfloat diffusion, + const ALfloat decayTime, const ALfloat frequency) +{ + const ALfloat multiplier{CalcDelayLengthMult(density)}; + + /* Calculate the all-pass feed-back/forward coefficient. */ + VecAp.Coeff = std::sqrt(0.5f) * std::pow(diffusion, 2.0f); + + for(ALsizei i{0};i < NUM_LINES;i++) + { + /* Calculate the length (in seconds) of each all-pass line. */ + ALfloat length{EARLY_ALLPASS_LENGTHS[i] * multiplier}; + + /* Calculate the delay offset for each all-pass line. */ + VecAp.Offset[i][1] = float2int(length * frequency); + + /* Calculate the length (in seconds) of each delay line. */ + length = EARLY_LINE_LENGTHS[i] * multiplier; + + /* Calculate the delay offset for each delay line. */ + Offset[i][1] = float2int(length * frequency); + + /* Calculate the gain (coefficient) for each line. */ + Coeff[i][1] = CalcDecayCoeff(length, decayTime); + } +} + +/* Update the late reverb line lengths and T60 coefficients. */ +void LateReverb::updateLines(const ALfloat density, const ALfloat diffusion, + const ALfloat lfDecayTime, const ALfloat mfDecayTime, const ALfloat hfDecayTime, + const ALfloat lf0norm, const ALfloat hf0norm, const ALfloat frequency) +{ + /* Scaling factor to convert the normalized reference frequencies from + * representing 0...freq to 0...max_reference. + */ + const ALfloat norm_weight_factor{frequency / AL_EAXREVERB_MAX_HFREFERENCE}; + + const ALfloat late_allpass_avg{ + std::accumulate(LATE_ALLPASS_LENGTHS.begin(), LATE_ALLPASS_LENGTHS.end(), 0.0f) / + float{LATE_ALLPASS_LENGTHS_size}}; + + /* To compensate for changes in modal density and decay time of the late + * reverb signal, the input is attenuated based on the maximal energy of + * the outgoing signal. This approximation is used to keep the apparent + * energy of the signal equal for all ranges of density and decay time. + * + * The average length of the delay lines is used to calculate the + * attenuation coefficient. + */ + const ALfloat multiplier{CalcDelayLengthMult(density)}; + ALfloat length{std::accumulate(LATE_LINE_LENGTHS.begin(), LATE_LINE_LENGTHS.end(), 0.0f) / + float{LATE_LINE_LENGTHS_size} * multiplier}; + length += late_allpass_avg * multiplier; + /* The density gain calculation uses an average decay time weighted by + * approximate bandwidth. This attempts to compensate for losses of energy + * that reduce decay time due to scattering into highly attenuated bands. + */ + const ALfloat bandWeights[3]{ + lf0norm*norm_weight_factor, + hf0norm*norm_weight_factor - lf0norm*norm_weight_factor, + 1.0f - hf0norm*norm_weight_factor}; + DensityGain[1] = CalcDensityGain( + CalcDecayCoeff(length, + bandWeights[0]*lfDecayTime + bandWeights[1]*mfDecayTime + bandWeights[2]*hfDecayTime + ) + ); + + /* Calculate the all-pass feed-back/forward coefficient. */ + VecAp.Coeff = std::sqrt(0.5f) * std::pow(diffusion, 2.0f); + + for(ALsizei i{0};i < NUM_LINES;i++) + { + /* Calculate the length (in seconds) of each all-pass line. */ + length = LATE_ALLPASS_LENGTHS[i] * multiplier; + + /* Calculate the delay offset for each all-pass line. */ + VecAp.Offset[i][1] = float2int(length * frequency); + + /* Calculate the length (in seconds) of each delay line. */ + length = LATE_LINE_LENGTHS[i] * multiplier; + + /* Calculate the delay offset for each delay line. */ + Offset[i][1] = float2int(length*frequency + 0.5f); + + /* Approximate the absorption that the vector all-pass would exhibit + * given the current diffusion so we don't have to process a full T60 + * filter for each of its four lines. + */ + length += lerp(LATE_ALLPASS_LENGTHS[i], late_allpass_avg, diffusion) * multiplier; + + /* Calculate the T60 damping coefficients for each line. */ + T60[i].calcCoeffs(length, lfDecayTime, mfDecayTime, hfDecayTime, lf0norm, hf0norm); + } +} + + +/* Update the offsets for the main effect delay line. */ +void ReverbState::updateDelayLine(const ALfloat earlyDelay, const ALfloat lateDelay, + const ALfloat density, const ALfloat decayTime, const ALfloat frequency) +{ + const ALfloat multiplier{CalcDelayLengthMult(density)}; + + /* Early reflection taps are decorrelated by means of an average room + * reflection approximation described above the definition of the taps. + * This approximation is linear and so the above density multiplier can + * be applied to adjust the width of the taps. A single-band decay + * coefficient is applied to simulate initial attenuation and absorption. + * + * Late reverb taps are based on the late line lengths to allow a zero- + * delay path and offsets that would continue the propagation naturally + * into the late lines. + */ + for(ALsizei i{0};i < NUM_LINES;i++) + { + ALfloat length{earlyDelay + EARLY_TAP_LENGTHS[i]*multiplier}; + mEarlyDelayTap[i][1] = float2int(length * frequency); + + length = EARLY_TAP_LENGTHS[i]*multiplier; + mEarlyDelayCoeff[i][1] = CalcDecayCoeff(length, decayTime); + + length = lateDelay + (LATE_LINE_LENGTHS[i] - LATE_LINE_LENGTHS.front()) / + float{LATE_LINE_LENGTHS_size} * multiplier; + mLateDelayTap[i][1] = mLateFeedTap + float2int(length * frequency); + } +} + +/* Creates a transform matrix given a reverb vector. The vector pans the reverb + * reflections toward the given direction, using its magnitude (up to 1) as a + * focal strength. This function results in a B-Format transformation matrix + * that spatially focuses the signal in the desired direction. + */ +alu::Matrix GetTransformFromVector(const ALfloat *vec) +{ + /* Normalize the panning vector according to the N3D scale, which has an + * extra sqrt(3) term on the directional components. Converting from OpenAL + * to B-Format also requires negating X (ACN 1) and Z (ACN 3). Note however + * that the reverb panning vectors use left-handed coordinates, unlike the + * rest of OpenAL which use right-handed. This is fixed by negating Z, + * which cancels out with the B-Format Z negation. + */ + ALfloat norm[3]; + ALfloat mag{std::sqrt(vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2])}; + if(mag > 1.0f) + { + norm[0] = vec[0] / mag * -al::MathDefs<float>::Sqrt3(); + norm[1] = vec[1] / mag * al::MathDefs<float>::Sqrt3(); + norm[2] = vec[2] / mag * al::MathDefs<float>::Sqrt3(); + mag = 1.0f; + } + else + { + /* If the magnitude is less than or equal to 1, just apply the sqrt(3) + * term. There's no need to renormalize the magnitude since it would + * just be reapplied in the matrix. + */ + norm[0] = vec[0] * -al::MathDefs<float>::Sqrt3(); + norm[1] = vec[1] * al::MathDefs<float>::Sqrt3(); + norm[2] = vec[2] * al::MathDefs<float>::Sqrt3(); + } + + return alu::Matrix{ + 1.0f, 0.0f, 0.0f, 0.0f, + norm[0], 1.0f-mag, 0.0f, 0.0f, + norm[1], 0.0f, 1.0f-mag, 0.0f, + norm[2], 0.0f, 0.0f, 1.0f-mag + }; +} + +/* Update the early and late 3D panning gains. */ +void ReverbState::update3DPanning(const ALfloat *ReflectionsPan, const ALfloat *LateReverbPan, + const ALfloat earlyGain, const ALfloat lateGain, const EffectTarget &target) +{ + /* Create matrices that transform a B-Format signal according to the + * panning vectors. + */ + const alu::Matrix earlymat{GetTransformFromVector(ReflectionsPan)}; + const alu::Matrix latemat{GetTransformFromVector(LateReverbPan)}; + + mOutTarget = target.Main->Buffer; + for(ALsizei i{0};i < NUM_LINES;i++) + { + const ALfloat coeffs[MAX_AMBI_CHANNELS]{earlymat[0][i], earlymat[1][i], earlymat[2][i], + earlymat[3][i]}; + ComputePanGains(target.Main, coeffs, earlyGain, mEarly.PanGain[i]); + } + for(ALsizei i{0};i < NUM_LINES;i++) + { + const ALfloat coeffs[MAX_AMBI_CHANNELS]{latemat[0][i], latemat[1][i], latemat[2][i], + latemat[3][i]}; + ComputePanGains(target.Main, coeffs, lateGain, mLate.PanGain[i]); + } +} + +void ReverbState::update(const ALCcontext *Context, const ALeffectslot *Slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *Device{Context->Device}; + const ALlistener &Listener = Context->Listener; + const auto frequency = static_cast<ALfloat>(Device->Frequency); + + /* Calculate the master filters */ + ALfloat hf0norm{minf(props->Reverb.HFReference / frequency, 0.49f)}; + /* Restrict the filter gains from going below -60dB to keep the filter from + * killing most of the signal. + */ + ALfloat gainhf{maxf(props->Reverb.GainHF, 0.001f)}; + mFilter[0].Lp.setParams(BiquadType::HighShelf, gainhf, hf0norm, + mFilter[0].Lp.rcpQFromSlope(gainhf, 1.0f)); + ALfloat lf0norm{minf(props->Reverb.LFReference / frequency, 0.49f)}; + ALfloat gainlf{maxf(props->Reverb.GainLF, 0.001f)}; + mFilter[0].Hp.setParams(BiquadType::LowShelf, gainlf, lf0norm, + mFilter[0].Hp.rcpQFromSlope(gainlf, 1.0f)); + for(ALsizei i{1};i < NUM_LINES;i++) + { + mFilter[i].Lp.copyParamsFrom(mFilter[0].Lp); + mFilter[i].Hp.copyParamsFrom(mFilter[0].Hp); + } + + /* Update the main effect delay and associated taps. */ + updateDelayLine(props->Reverb.ReflectionsDelay, props->Reverb.LateReverbDelay, + props->Reverb.Density, props->Reverb.DecayTime, frequency); + + /* Update the early lines. */ + mEarly.updateLines(props->Reverb.Density, props->Reverb.Diffusion, props->Reverb.DecayTime, + frequency); + + /* Get the mixing matrix coefficients. */ + CalcMatrixCoeffs(props->Reverb.Diffusion, &mMixX, &mMixY); + + /* If the HF limit parameter is flagged, calculate an appropriate limit + * based on the air absorption parameter. + */ + ALfloat hfRatio{props->Reverb.DecayHFRatio}; + if(props->Reverb.DecayHFLimit && props->Reverb.AirAbsorptionGainHF < 1.0f) + hfRatio = CalcLimitedHfRatio(hfRatio, props->Reverb.AirAbsorptionGainHF, + props->Reverb.DecayTime, Listener.Params.ReverbSpeedOfSound + ); + + /* Calculate the LF/HF decay times. */ + const ALfloat lfDecayTime{clampf(props->Reverb.DecayTime * props->Reverb.DecayLFRatio, + AL_EAXREVERB_MIN_DECAY_TIME, AL_EAXREVERB_MAX_DECAY_TIME)}; + const ALfloat hfDecayTime{clampf(props->Reverb.DecayTime * hfRatio, + AL_EAXREVERB_MIN_DECAY_TIME, AL_EAXREVERB_MAX_DECAY_TIME)}; + + /* Update the late lines. */ + mLate.updateLines(props->Reverb.Density, props->Reverb.Diffusion, lfDecayTime, + props->Reverb.DecayTime, hfDecayTime, lf0norm, hf0norm, frequency); + + /* Update early and late 3D panning. */ + const ALfloat gain{props->Reverb.Gain * Slot->Params.Gain * ReverbBoost}; + update3DPanning(props->Reverb.ReflectionsPan, props->Reverb.LateReverbPan, + props->Reverb.ReflectionsGain*gain, props->Reverb.LateReverbGain*gain, target); + + /* Calculate the max update size from the smallest relevant delay. */ + mMaxUpdate[1] = mini(BUFFERSIZE, mini(mEarly.Offset[0][1], mLate.Offset[0][1])); + + /* Determine if delay-line cross-fading is required. Density is essentially + * a master control for the feedback delays, so changes the offsets of many + * delay lines. + */ + if(mParams.Density != props->Reverb.Density || + /* Diffusion and decay times influences the decay rate (gain) of the + * late reverb T60 filter. + */ + mParams.Diffusion != props->Reverb.Diffusion || + mParams.DecayTime != props->Reverb.DecayTime || + mParams.HFDecayTime != hfDecayTime || + mParams.LFDecayTime != lfDecayTime || + /* HF/LF References control the weighting used to calculate the density + * gain. + */ + mParams.HFReference != props->Reverb.HFReference || + mParams.LFReference != props->Reverb.LFReference) + mFadeCount = 0; + mParams.Density = props->Reverb.Density; + mParams.Diffusion = props->Reverb.Diffusion; + mParams.DecayTime = props->Reverb.DecayTime; + mParams.HFDecayTime = hfDecayTime; + mParams.LFDecayTime = lfDecayTime; + mParams.HFReference = props->Reverb.HFReference; + mParams.LFReference = props->Reverb.LFReference; +} + + +/************************************** + * Effect Processing * + **************************************/ + +/* Applies a scattering matrix to the 4-line (vector) input. This is used + * for both the below vector all-pass model and to perform modal feed-back + * delay network (FDN) mixing. + * + * The matrix is derived from a skew-symmetric matrix to form a 4D rotation + * matrix with a single unitary rotational parameter: + * + * [ d, a, b, c ] 1 = a^2 + b^2 + c^2 + d^2 + * [ -a, d, c, -b ] + * [ -b, -c, d, a ] + * [ -c, b, -a, d ] + * + * The rotation is constructed from the effect's diffusion parameter, + * yielding: + * + * 1 = x^2 + 3 y^2 + * + * Where a, b, and c are the coefficient y with differing signs, and d is the + * coefficient x. The final matrix is thus: + * + * [ x, y, -y, y ] n = sqrt(matrix_order - 1) + * [ -y, x, y, y ] t = diffusion_parameter * atan(n) + * [ y, -y, x, y ] x = cos(t) + * [ -y, -y, -y, x ] y = sin(t) / n + * + * Any square orthogonal matrix with an order that is a power of two will + * work (where ^T is transpose, ^-1 is inverse): + * + * M^T = M^-1 + * + * Using that knowledge, finding an appropriate matrix can be accomplished + * naively by searching all combinations of: + * + * M = D + S - S^T + * + * Where D is a diagonal matrix (of x), and S is a triangular matrix (of y) + * whose combination of signs are being iterated. + */ +inline void VectorPartialScatter(ALfloat *RESTRICT out, const ALfloat *RESTRICT in, + const ALfloat xCoeff, const ALfloat yCoeff) +{ + out[0] = xCoeff*in[0] + yCoeff*( in[1] + -in[2] + in[3]); + out[1] = xCoeff*in[1] + yCoeff*(-in[0] + in[2] + in[3]); + out[2] = xCoeff*in[2] + yCoeff*( in[0] + -in[1] + in[3]); + out[3] = xCoeff*in[3] + yCoeff*(-in[0] + -in[1] + -in[2] ); +} + +/* Utilizes the above, but reverses the input channels. */ +void VectorScatterRevDelayIn(const DelayLineI delay, ALint offset, const ALfloat xCoeff, + const ALfloat yCoeff, const ALsizei base, const al::span<const FloatBufferLine,NUM_LINES> in, + const ALsizei count) +{ + ASSUME(base >= 0); + ASSUME(count > 0); + + for(ALsizei i{0};i < count;) + { + offset &= delay.Mask; + ALsizei td{mini(delay.Mask+1 - offset, count-i)}; + do { + ALfloat f[NUM_LINES]; + for(ALsizei j{0};j < NUM_LINES;j++) + f[NUM_LINES-1-j] = in[j][base+i]; + ++i; + + VectorPartialScatter(delay.Line[offset++], f, xCoeff, yCoeff); + } while(--td); + } +} + +/* This applies a Gerzon multiple-in/multiple-out (MIMO) vector all-pass + * filter to the 4-line input. + * + * It works by vectorizing a regular all-pass filter and replacing the delay + * element with a scattering matrix (like the one above) and a diagonal + * matrix of delay elements. + * + * Two static specializations are used for transitional (cross-faded) delay + * line processing and non-transitional processing. + */ +void VecAllpass::processUnfaded(const al::span<FloatBufferLine,NUM_LINES> samples, ALsizei offset, + const ALfloat xCoeff, const ALfloat yCoeff, const ALsizei todo) +{ + const DelayLineI delay{Delay}; + const ALfloat feedCoeff{Coeff}; + + ASSUME(todo > 0); + + ALsizei vap_offset[NUM_LINES]; + for(ALsizei j{0};j < NUM_LINES;j++) + vap_offset[j] = offset - Offset[j][0]; + for(ALsizei i{0};i < todo;) + { + for(ALsizei j{0};j < NUM_LINES;j++) + vap_offset[j] &= delay.Mask; + offset &= delay.Mask; + + ALsizei maxoff{offset}; + for(ALsizei j{0};j < NUM_LINES;j++) + maxoff = maxi(maxoff, vap_offset[j]); + ALsizei td{mini(delay.Mask+1 - maxoff, todo - i)}; + + do { + ALfloat f[NUM_LINES]; + for(ALsizei j{0};j < NUM_LINES;j++) + { + const ALfloat input{samples[j][i]}; + const ALfloat out{delay.Line[vap_offset[j]++][j] - feedCoeff*input}; + f[j] = input + feedCoeff*out; + + samples[j][i] = out; + } + ++i; + + VectorPartialScatter(delay.Line[offset++], f, xCoeff, yCoeff); + } while(--td); + } +} +void VecAllpass::processFaded(const al::span<FloatBufferLine,NUM_LINES> samples, ALsizei offset, + const ALfloat xCoeff, const ALfloat yCoeff, ALfloat fade, const ALsizei todo) +{ + const DelayLineI delay{Delay}; + const ALfloat feedCoeff{Coeff}; + + ASSUME(todo > 0); + + fade *= 1.0f/FADE_SAMPLES; + ALsizei vap_offset[NUM_LINES][2]; + for(ALsizei j{0};j < NUM_LINES;j++) + { + vap_offset[j][0] = offset - Offset[j][0]; + vap_offset[j][1] = offset - Offset[j][1]; + } + for(ALsizei i{0};i < todo;) + { + for(ALsizei j{0};j < NUM_LINES;j++) + { + vap_offset[j][0] &= delay.Mask; + vap_offset[j][1] &= delay.Mask; + } + offset &= delay.Mask; + + ALsizei maxoff{offset}; + for(ALsizei j{0};j < NUM_LINES;j++) + maxoff = maxi(maxoff, maxi(vap_offset[j][0], vap_offset[j][1])); + ALsizei td{mini(delay.Mask+1 - maxoff, todo - i)}; + + do { + fade += FadeStep; + ALfloat f[NUM_LINES]; + for(ALsizei j{0};j < NUM_LINES;j++) + f[j] = delay.Line[vap_offset[j][0]++][j]*(1.0f-fade) + + delay.Line[vap_offset[j][1]++][j]*fade; + + for(ALsizei j{0};j < NUM_LINES;j++) + { + const ALfloat input{samples[j][i]}; + const ALfloat out{f[j] - feedCoeff*input}; + f[j] = input + feedCoeff*out; + + samples[j][i] = out; + } + ++i; + + VectorPartialScatter(delay.Line[offset++], f, xCoeff, yCoeff); + } while(--td); + } +} + +/* This generates early reflections. + * + * This is done by obtaining the primary reflections (those arriving from the + * same direction as the source) from the main delay line. These are + * attenuated and all-pass filtered (based on the diffusion parameter). + * + * The early lines are then fed in reverse (according to the approximately + * opposite spatial location of the A-Format lines) to create the secondary + * reflections (those arriving from the opposite direction as the source). + * + * The early response is then completed by combining the primary reflections + * with the delayed and attenuated output from the early lines. + * + * Finally, the early response is reversed, scattered (based on diffusion), + * and fed into the late reverb section of the main delay line. + * + * Two static specializations are used for transitional (cross-faded) delay + * line processing and non-transitional processing. + */ +void EarlyReflection_Unfaded(ReverbState *State, const ALsizei offset, const ALsizei todo, + const ALsizei base, const al::span<FloatBufferLine,NUM_LINES> out) +{ + const al::span<FloatBufferLine,NUM_LINES> temps{State->mTempSamples}; + const DelayLineI early_delay{State->mEarly.Delay}; + const DelayLineI main_delay{State->mDelay}; + const ALfloat mixX{State->mMixX}; + const ALfloat mixY{State->mMixY}; + + ASSUME(todo > 0); + + /* First, load decorrelated samples from the main delay line as the primary + * reflections. + */ + for(ALsizei j{0};j < NUM_LINES;j++) + { + ALsizei early_delay_tap{offset - State->mEarlyDelayTap[j][0]}; + const ALfloat coeff{State->mEarlyDelayCoeff[j][0]}; + for(ALsizei i{0};i < todo;) + { + early_delay_tap &= main_delay.Mask; + ALsizei td{mini(main_delay.Mask+1 - early_delay_tap, todo - i)}; + do { + temps[j][i++] = main_delay.Line[early_delay_tap++][j] * coeff; + } while(--td); + } + } + + /* Apply a vector all-pass, to help color the initial reflections based on + * the diffusion strength. + */ + State->mEarly.VecAp.processUnfaded(temps, offset, mixX, mixY, todo); + + /* Apply a delay and bounce to generate secondary reflections, combine with + * the primary reflections and write out the result for mixing. + */ + for(ALsizei j{0};j < NUM_LINES;j++) + { + ALint feedb_tap{offset - State->mEarly.Offset[j][0]}; + const ALfloat feedb_coeff{State->mEarly.Coeff[j][0]}; + + ASSUME(base >= 0); + for(ALsizei i{0};i < todo;) + { + feedb_tap &= early_delay.Mask; + ALsizei td{mini(early_delay.Mask+1 - feedb_tap, todo - i)}; + do { + out[j][base+i] = temps[j][i] + early_delay.Line[feedb_tap++][j]*feedb_coeff; + ++i; + } while(--td); + } + } + for(ALsizei j{0};j < NUM_LINES;j++) + early_delay.write(offset, NUM_LINES-1-j, temps[j].data(), todo); + + /* Also write the result back to the main delay line for the late reverb + * stage to pick up at the appropriate time, appplying a scatter and + * bounce to improve the initial diffusion in the late reverb. + */ + const ALsizei late_feed_tap{offset - State->mLateFeedTap}; + VectorScatterRevDelayIn(main_delay, late_feed_tap, mixX, mixY, base, + {out.cbegin(), out.cend()}, todo); +} +void EarlyReflection_Faded(ReverbState *State, const ALsizei offset, const ALsizei todo, + const ALfloat fade, const ALsizei base, const al::span<FloatBufferLine,NUM_LINES> out) +{ + const al::span<FloatBufferLine,NUM_LINES> temps{State->mTempSamples}; + const DelayLineI early_delay{State->mEarly.Delay}; + const DelayLineI main_delay{State->mDelay}; + const ALfloat mixX{State->mMixX}; + const ALfloat mixY{State->mMixY}; + + ASSUME(todo > 0); + + for(ALsizei j{0};j < NUM_LINES;j++) + { + ALsizei early_delay_tap0{offset - State->mEarlyDelayTap[j][0]}; + ALsizei early_delay_tap1{offset - State->mEarlyDelayTap[j][1]}; + const ALfloat oldCoeff{State->mEarlyDelayCoeff[j][0]}; + const ALfloat oldCoeffStep{-oldCoeff / FADE_SAMPLES}; + const ALfloat newCoeffStep{State->mEarlyDelayCoeff[j][1] / FADE_SAMPLES}; + ALfloat fadeCount{fade}; + + for(ALsizei i{0};i < todo;) + { + early_delay_tap0 &= main_delay.Mask; + early_delay_tap1 &= main_delay.Mask; + ALsizei td{mini(main_delay.Mask+1 - maxi(early_delay_tap0, early_delay_tap1), todo-i)}; + do { + fadeCount += 1.0f; + const ALfloat fade0{oldCoeff + oldCoeffStep*fadeCount}; + const ALfloat fade1{newCoeffStep*fadeCount}; + temps[j][i++] = + main_delay.Line[early_delay_tap0++][j]*fade0 + + main_delay.Line[early_delay_tap1++][j]*fade1; + } while(--td); + } + } + + State->mEarly.VecAp.processFaded(temps, offset, mixX, mixY, fade, todo); + + for(ALsizei j{0};j < NUM_LINES;j++) + { + ALint feedb_tap0{offset - State->mEarly.Offset[j][0]}; + ALint feedb_tap1{offset - State->mEarly.Offset[j][1]}; + const ALfloat feedb_oldCoeff{State->mEarly.Coeff[j][0]}; + const ALfloat feedb_oldCoeffStep{-feedb_oldCoeff / FADE_SAMPLES}; + const ALfloat feedb_newCoeffStep{State->mEarly.Coeff[j][1] / FADE_SAMPLES}; + ALfloat fadeCount{fade}; + + ASSUME(base >= 0); + for(ALsizei i{0};i < todo;) + { + feedb_tap0 &= early_delay.Mask; + feedb_tap1 &= early_delay.Mask; + ALsizei td{mini(early_delay.Mask+1 - maxi(feedb_tap0, feedb_tap1), todo - i)}; + + do { + fadeCount += 1.0f; + const ALfloat fade0{feedb_oldCoeff + feedb_oldCoeffStep*fadeCount}; + const ALfloat fade1{feedb_newCoeffStep*fadeCount}; + out[j][base+i] = temps[j][i] + + early_delay.Line[feedb_tap0++][j]*fade0 + + early_delay.Line[feedb_tap1++][j]*fade1; + ++i; + } while(--td); + } + } + for(ALsizei j{0};j < NUM_LINES;j++) + early_delay.write(offset, NUM_LINES-1-j, temps[j].data(), todo); + + const ALsizei late_feed_tap{offset - State->mLateFeedTap}; + VectorScatterRevDelayIn(main_delay, late_feed_tap, mixX, mixY, base, + {out.cbegin(), out.cend()}, todo); +} + +/* This generates the reverb tail using a modified feed-back delay network + * (FDN). + * + * Results from the early reflections are mixed with the output from the late + * delay lines. + * + * The late response is then completed by T60 and all-pass filtering the mix. + * + * Finally, the lines are reversed (so they feed their opposite directions) + * and scattered with the FDN matrix before re-feeding the delay lines. + * + * Two variations are made, one for for transitional (cross-faded) delay line + * processing and one for non-transitional processing. + */ +void LateReverb_Unfaded(ReverbState *State, const ALsizei offset, const ALsizei todo, + const ALsizei base, const al::span<FloatBufferLine,NUM_LINES> out) +{ + const al::span<FloatBufferLine,NUM_LINES> temps{State->mTempSamples}; + const DelayLineI late_delay{State->mLate.Delay}; + const DelayLineI main_delay{State->mDelay}; + const ALfloat mixX{State->mMixX}; + const ALfloat mixY{State->mMixY}; + + ASSUME(todo > 0); + + /* First, load decorrelated samples from the main and feedback delay lines. + * Filter the signal to apply its frequency-dependent decay. + */ + for(ALsizei j{0};j < NUM_LINES;j++) + { + ALsizei late_delay_tap{offset - State->mLateDelayTap[j][0]}; + ALsizei late_feedb_tap{offset - State->mLate.Offset[j][0]}; + const ALfloat midGain{State->mLate.T60[j].MidGain[0]}; + const ALfloat densityGain{State->mLate.DensityGain[0] * midGain}; + for(ALsizei i{0};i < todo;) + { + late_delay_tap &= main_delay.Mask; + late_feedb_tap &= late_delay.Mask; + ALsizei td{mini( + mini(main_delay.Mask+1 - late_delay_tap, late_delay.Mask+1 - late_feedb_tap), + todo - i)}; + do { + temps[j][i++] = + main_delay.Line[late_delay_tap++][j]*densityGain + + late_delay.Line[late_feedb_tap++][j]*midGain; + } while(--td); + } + State->mLate.T60[j].process(temps[j].data(), todo); + } + + /* Apply a vector all-pass to improve micro-surface diffusion, and write + * out the results for mixing. + */ + State->mLate.VecAp.processUnfaded(temps, offset, mixX, mixY, todo); + + for(ALsizei j{0};j < NUM_LINES;j++) + std::copy_n(temps[j].begin(), todo, out[j].begin()+base); + + /* Finally, scatter and bounce the results to refeed the feedback buffer. */ + VectorScatterRevDelayIn(late_delay, offset, mixX, mixY, base, + {out.cbegin(), out.cend()}, todo); +} +void LateReverb_Faded(ReverbState *State, const ALsizei offset, const ALsizei todo, + const ALfloat fade, const ALsizei base, const al::span<FloatBufferLine,NUM_LINES> out) +{ + const al::span<FloatBufferLine,NUM_LINES> temps{State->mTempSamples}; + const DelayLineI late_delay{State->mLate.Delay}; + const DelayLineI main_delay{State->mDelay}; + const ALfloat mixX{State->mMixX}; + const ALfloat mixY{State->mMixY}; + + ASSUME(todo > 0); + + for(ALsizei j{0};j < NUM_LINES;j++) + { + const ALfloat oldMidGain{State->mLate.T60[j].MidGain[0]}; + const ALfloat midGain{State->mLate.T60[j].MidGain[1]}; + const ALfloat oldMidStep{-oldMidGain / FADE_SAMPLES}; + const ALfloat midStep{midGain / FADE_SAMPLES}; + const ALfloat oldDensityGain{State->mLate.DensityGain[0] * oldMidGain}; + const ALfloat densityGain{State->mLate.DensityGain[1] * midGain}; + const ALfloat oldDensityStep{-oldDensityGain / FADE_SAMPLES}; + const ALfloat densityStep{densityGain / FADE_SAMPLES}; + ALsizei late_delay_tap0{offset - State->mLateDelayTap[j][0]}; + ALsizei late_delay_tap1{offset - State->mLateDelayTap[j][1]}; + ALsizei late_feedb_tap0{offset - State->mLate.Offset[j][0]}; + ALsizei late_feedb_tap1{offset - State->mLate.Offset[j][1]}; + ALfloat fadeCount{fade}; + + for(ALsizei i{0};i < todo;) + { + late_delay_tap0 &= main_delay.Mask; + late_delay_tap1 &= main_delay.Mask; + late_feedb_tap0 &= late_delay.Mask; + late_feedb_tap1 &= late_delay.Mask; + ALsizei td{mini( + mini(main_delay.Mask+1 - maxi(late_delay_tap0, late_delay_tap1), + late_delay.Mask+1 - maxi(late_feedb_tap0, late_feedb_tap1)), + todo - i)}; + do { + fadeCount += 1.0f; + const ALfloat fade0{oldDensityGain + oldDensityStep*fadeCount}; + const ALfloat fade1{densityStep*fadeCount}; + const ALfloat gfade0{oldMidGain + oldMidStep*fadeCount}; + const ALfloat gfade1{midStep*fadeCount}; + temps[j][i++] = + main_delay.Line[late_delay_tap0++][j]*fade0 + + main_delay.Line[late_delay_tap1++][j]*fade1 + + late_delay.Line[late_feedb_tap0++][j]*gfade0 + + late_delay.Line[late_feedb_tap1++][j]*gfade1; + } while(--td); + } + State->mLate.T60[j].process(temps[j].data(), todo); + } + + State->mLate.VecAp.processFaded(temps, offset, mixX, mixY, fade, todo); + + for(ALsizei j{0};j < NUM_LINES;j++) + std::copy_n(temps[j].begin(), todo, out[j].begin()+base); + + VectorScatterRevDelayIn(late_delay, offset, mixX, mixY, base, + {out.cbegin(), out.cend()}, todo); +} + +void ReverbState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) +{ + ALsizei fadeCount{mFadeCount}; + + ASSUME(samplesToDo > 0); + + /* Convert B-Format to A-Format for processing. */ + const al::span<FloatBufferLine,NUM_LINES> afmt{mTempSamples}; + for(ALsizei c{0};c < NUM_LINES;c++) + { + std::fill_n(afmt[c].begin(), samplesToDo, 0.0f); + MixRowSamples(afmt[c], B2A[c], {samplesIn, samplesIn+numInput}, 0, samplesToDo); + + /* Band-pass the incoming samples. */ + mFilter[c].Lp.process(afmt[c].data(), afmt[c].data(), samplesToDo); + mFilter[c].Hp.process(afmt[c].data(), afmt[c].data(), samplesToDo); + } + + /* Process reverb for these samples. */ + for(ALsizei base{0};base < samplesToDo;) + { + ALsizei todo{samplesToDo - base}; + /* If cross-fading, don't do more samples than there are to fade. */ + if(FADE_SAMPLES-fadeCount > 0) + { + todo = mini(todo, FADE_SAMPLES-fadeCount); + todo = mini(todo, mMaxUpdate[0]); + } + todo = mini(todo, mMaxUpdate[1]); + ASSUME(todo > 0 && todo <= BUFFERSIZE); + + const ALsizei offset{mOffset + base}; + ASSUME(offset >= 0); + + /* Feed the initial delay line. */ + for(ALsizei c{0};c < NUM_LINES;c++) + mDelay.write(offset, c, afmt[c].data()+base, todo); + + /* Process the samples for reverb. */ + if(UNLIKELY(fadeCount < FADE_SAMPLES)) + { + auto fade = static_cast<ALfloat>(fadeCount); + + /* Generate early reflections and late reverb. */ + EarlyReflection_Faded(this, offset, todo, fade, base, mEarlyBuffer); + + LateReverb_Faded(this, offset, todo, fade, base, mLateBuffer); + + /* Step fading forward. */ + fadeCount += todo; + if(fadeCount >= FADE_SAMPLES) + { + /* Update the cross-fading delay line taps. */ + fadeCount = FADE_SAMPLES; + for(ALsizei c{0};c < NUM_LINES;c++) + { + mEarlyDelayTap[c][0] = mEarlyDelayTap[c][1]; + mEarlyDelayCoeff[c][0] = mEarlyDelayCoeff[c][1]; + mEarly.VecAp.Offset[c][0] = mEarly.VecAp.Offset[c][1]; + mEarly.Offset[c][0] = mEarly.Offset[c][1]; + mEarly.Coeff[c][0] = mEarly.Coeff[c][1]; + mLateDelayTap[c][0] = mLateDelayTap[c][1]; + mLate.VecAp.Offset[c][0] = mLate.VecAp.Offset[c][1]; + mLate.Offset[c][0] = mLate.Offset[c][1]; + mLate.T60[c].MidGain[0] = mLate.T60[c].MidGain[1]; + } + mLate.DensityGain[0] = mLate.DensityGain[1]; + mMaxUpdate[0] = mMaxUpdate[1]; + } + } + else + { + /* Generate early reflections and late reverb. */ + EarlyReflection_Unfaded(this, offset, todo, base, mEarlyBuffer); + + LateReverb_Unfaded(this, offset, todo, base, mLateBuffer); + } + + base += todo; + } + mOffset = (mOffset+samplesToDo) & 0x3fffffff; + mFadeCount = fadeCount; + + /* Finally, mix early reflections and late reverb. */ + (this->*mMixOut)(samplesOut, samplesToDo); +} + + +void EAXReverb_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_EAXREVERB_DECAY_HFLIMIT: + if(!(val >= AL_EAXREVERB_MIN_DECAY_HFLIMIT && val <= AL_EAXREVERB_MAX_DECAY_HFLIMIT)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb decay hflimit out of range"); + props->Reverb.DecayHFLimit = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid EAX reverb integer property 0x%04x", + param); + } +} +void EAXReverb_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ EAXReverb_setParami(props, context, param, vals[0]); } +void EAXReverb_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_EAXREVERB_DENSITY: + if(!(val >= AL_EAXREVERB_MIN_DENSITY && val <= AL_EAXREVERB_MAX_DENSITY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb density out of range"); + props->Reverb.Density = val; + break; + + case AL_EAXREVERB_DIFFUSION: + if(!(val >= AL_EAXREVERB_MIN_DIFFUSION && val <= AL_EAXREVERB_MAX_DIFFUSION)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb diffusion out of range"); + props->Reverb.Diffusion = val; + break; + + case AL_EAXREVERB_GAIN: + if(!(val >= AL_EAXREVERB_MIN_GAIN && val <= AL_EAXREVERB_MAX_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb gain out of range"); + props->Reverb.Gain = val; + break; + + case AL_EAXREVERB_GAINHF: + if(!(val >= AL_EAXREVERB_MIN_GAINHF && val <= AL_EAXREVERB_MAX_GAINHF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb gainhf out of range"); + props->Reverb.GainHF = val; + break; + + case AL_EAXREVERB_GAINLF: + if(!(val >= AL_EAXREVERB_MIN_GAINLF && val <= AL_EAXREVERB_MAX_GAINLF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb gainlf out of range"); + props->Reverb.GainLF = val; + break; + + case AL_EAXREVERB_DECAY_TIME: + if(!(val >= AL_EAXREVERB_MIN_DECAY_TIME && val <= AL_EAXREVERB_MAX_DECAY_TIME)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb decay time out of range"); + props->Reverb.DecayTime = val; + break; + + case AL_EAXREVERB_DECAY_HFRATIO: + if(!(val >= AL_EAXREVERB_MIN_DECAY_HFRATIO && val <= AL_EAXREVERB_MAX_DECAY_HFRATIO)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb decay hfratio out of range"); + props->Reverb.DecayHFRatio = val; + break; + + case AL_EAXREVERB_DECAY_LFRATIO: + if(!(val >= AL_EAXREVERB_MIN_DECAY_LFRATIO && val <= AL_EAXREVERB_MAX_DECAY_LFRATIO)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb decay lfratio out of range"); + props->Reverb.DecayLFRatio = val; + break; + + case AL_EAXREVERB_REFLECTIONS_GAIN: + if(!(val >= AL_EAXREVERB_MIN_REFLECTIONS_GAIN && val <= AL_EAXREVERB_MAX_REFLECTIONS_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb reflections gain out of range"); + props->Reverb.ReflectionsGain = val; + break; + + case AL_EAXREVERB_REFLECTIONS_DELAY: + if(!(val >= AL_EAXREVERB_MIN_REFLECTIONS_DELAY && val <= AL_EAXREVERB_MAX_REFLECTIONS_DELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb reflections delay out of range"); + props->Reverb.ReflectionsDelay = val; + break; + + case AL_EAXREVERB_LATE_REVERB_GAIN: + if(!(val >= AL_EAXREVERB_MIN_LATE_REVERB_GAIN && val <= AL_EAXREVERB_MAX_LATE_REVERB_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb late reverb gain out of range"); + props->Reverb.LateReverbGain = val; + break; + + case AL_EAXREVERB_LATE_REVERB_DELAY: + if(!(val >= AL_EAXREVERB_MIN_LATE_REVERB_DELAY && val <= AL_EAXREVERB_MAX_LATE_REVERB_DELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb late reverb delay out of range"); + props->Reverb.LateReverbDelay = val; + break; + + case AL_EAXREVERB_AIR_ABSORPTION_GAINHF: + if(!(val >= AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF && val <= AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb air absorption gainhf out of range"); + props->Reverb.AirAbsorptionGainHF = val; + break; + + case AL_EAXREVERB_ECHO_TIME: + if(!(val >= AL_EAXREVERB_MIN_ECHO_TIME && val <= AL_EAXREVERB_MAX_ECHO_TIME)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb echo time out of range"); + props->Reverb.EchoTime = val; + break; + + case AL_EAXREVERB_ECHO_DEPTH: + if(!(val >= AL_EAXREVERB_MIN_ECHO_DEPTH && val <= AL_EAXREVERB_MAX_ECHO_DEPTH)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb echo depth out of range"); + props->Reverb.EchoDepth = val; + break; + + case AL_EAXREVERB_MODULATION_TIME: + if(!(val >= AL_EAXREVERB_MIN_MODULATION_TIME && val <= AL_EAXREVERB_MAX_MODULATION_TIME)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb modulation time out of range"); + props->Reverb.ModulationTime = val; + break; + + case AL_EAXREVERB_MODULATION_DEPTH: + if(!(val >= AL_EAXREVERB_MIN_MODULATION_DEPTH && val <= AL_EAXREVERB_MAX_MODULATION_DEPTH)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb modulation depth out of range"); + props->Reverb.ModulationDepth = val; + break; + + case AL_EAXREVERB_HFREFERENCE: + if(!(val >= AL_EAXREVERB_MIN_HFREFERENCE && val <= AL_EAXREVERB_MAX_HFREFERENCE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb hfreference out of range"); + props->Reverb.HFReference = val; + break; + + case AL_EAXREVERB_LFREFERENCE: + if(!(val >= AL_EAXREVERB_MIN_LFREFERENCE && val <= AL_EAXREVERB_MAX_LFREFERENCE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb lfreference out of range"); + props->Reverb.LFReference = val; + break; + + case AL_EAXREVERB_ROOM_ROLLOFF_FACTOR: + if(!(val >= AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR && val <= AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb room rolloff factor out of range"); + props->Reverb.RoomRolloffFactor = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid EAX reverb float property 0x%04x", + param); + } +} +void EAXReverb_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ + switch(param) + { + case AL_EAXREVERB_REFLECTIONS_PAN: + if(!(std::isfinite(vals[0]) && std::isfinite(vals[1]) && std::isfinite(vals[2]))) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb reflections pan out of range"); + props->Reverb.ReflectionsPan[0] = vals[0]; + props->Reverb.ReflectionsPan[1] = vals[1]; + props->Reverb.ReflectionsPan[2] = vals[2]; + break; + case AL_EAXREVERB_LATE_REVERB_PAN: + if(!(std::isfinite(vals[0]) && std::isfinite(vals[1]) && std::isfinite(vals[2]))) + SETERR_RETURN(context, AL_INVALID_VALUE,, "EAX Reverb late reverb pan out of range"); + props->Reverb.LateReverbPan[0] = vals[0]; + props->Reverb.LateReverbPan[1] = vals[1]; + props->Reverb.LateReverbPan[2] = vals[2]; + break; + + default: + EAXReverb_setParamf(props, context, param, vals[0]); + break; + } +} + +void EAXReverb_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_EAXREVERB_DECAY_HFLIMIT: + *val = props->Reverb.DecayHFLimit; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid EAX reverb integer property 0x%04x", + param); + } +} +void EAXReverb_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ EAXReverb_getParami(props, context, param, vals); } +void EAXReverb_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_EAXREVERB_DENSITY: + *val = props->Reverb.Density; + break; + + case AL_EAXREVERB_DIFFUSION: + *val = props->Reverb.Diffusion; + break; + + case AL_EAXREVERB_GAIN: + *val = props->Reverb.Gain; + break; + + case AL_EAXREVERB_GAINHF: + *val = props->Reverb.GainHF; + break; + + case AL_EAXREVERB_GAINLF: + *val = props->Reverb.GainLF; + break; + + case AL_EAXREVERB_DECAY_TIME: + *val = props->Reverb.DecayTime; + break; + + case AL_EAXREVERB_DECAY_HFRATIO: + *val = props->Reverb.DecayHFRatio; + break; + + case AL_EAXREVERB_DECAY_LFRATIO: + *val = props->Reverb.DecayLFRatio; + break; + + case AL_EAXREVERB_REFLECTIONS_GAIN: + *val = props->Reverb.ReflectionsGain; + break; + + case AL_EAXREVERB_REFLECTIONS_DELAY: + *val = props->Reverb.ReflectionsDelay; + break; + + case AL_EAXREVERB_LATE_REVERB_GAIN: + *val = props->Reverb.LateReverbGain; + break; + + case AL_EAXREVERB_LATE_REVERB_DELAY: + *val = props->Reverb.LateReverbDelay; + break; + + case AL_EAXREVERB_AIR_ABSORPTION_GAINHF: + *val = props->Reverb.AirAbsorptionGainHF; + break; + + case AL_EAXREVERB_ECHO_TIME: + *val = props->Reverb.EchoTime; + break; + + case AL_EAXREVERB_ECHO_DEPTH: + *val = props->Reverb.EchoDepth; + break; + + case AL_EAXREVERB_MODULATION_TIME: + *val = props->Reverb.ModulationTime; + break; + + case AL_EAXREVERB_MODULATION_DEPTH: + *val = props->Reverb.ModulationDepth; + break; + + case AL_EAXREVERB_HFREFERENCE: + *val = props->Reverb.HFReference; + break; + + case AL_EAXREVERB_LFREFERENCE: + *val = props->Reverb.LFReference; + break; + + case AL_EAXREVERB_ROOM_ROLLOFF_FACTOR: + *val = props->Reverb.RoomRolloffFactor; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid EAX reverb float property 0x%04x", + param); + } +} +void EAXReverb_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ + switch(param) + { + case AL_EAXREVERB_REFLECTIONS_PAN: + vals[0] = props->Reverb.ReflectionsPan[0]; + vals[1] = props->Reverb.ReflectionsPan[1]; + vals[2] = props->Reverb.ReflectionsPan[2]; + break; + case AL_EAXREVERB_LATE_REVERB_PAN: + vals[0] = props->Reverb.LateReverbPan[0]; + vals[1] = props->Reverb.LateReverbPan[1]; + vals[2] = props->Reverb.LateReverbPan[2]; + break; + + default: + EAXReverb_getParamf(props, context, param, vals); + break; + } +} + +DEFINE_ALEFFECT_VTABLE(EAXReverb); + + +struct ReverbStateFactory final : public EffectStateFactory { + EffectState *create() override { return new ReverbState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &EAXReverb_vtable; } +}; + +EffectProps ReverbStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Reverb.Density = AL_EAXREVERB_DEFAULT_DENSITY; + props.Reverb.Diffusion = AL_EAXREVERB_DEFAULT_DIFFUSION; + props.Reverb.Gain = AL_EAXREVERB_DEFAULT_GAIN; + props.Reverb.GainHF = AL_EAXREVERB_DEFAULT_GAINHF; + props.Reverb.GainLF = AL_EAXREVERB_DEFAULT_GAINLF; + props.Reverb.DecayTime = AL_EAXREVERB_DEFAULT_DECAY_TIME; + props.Reverb.DecayHFRatio = AL_EAXREVERB_DEFAULT_DECAY_HFRATIO; + props.Reverb.DecayLFRatio = AL_EAXREVERB_DEFAULT_DECAY_LFRATIO; + props.Reverb.ReflectionsGain = AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN; + props.Reverb.ReflectionsDelay = AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY; + props.Reverb.ReflectionsPan[0] = AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ; + props.Reverb.ReflectionsPan[1] = AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ; + props.Reverb.ReflectionsPan[2] = AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ; + props.Reverb.LateReverbGain = AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN; + props.Reverb.LateReverbDelay = AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY; + props.Reverb.LateReverbPan[0] = AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ; + props.Reverb.LateReverbPan[1] = AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ; + props.Reverb.LateReverbPan[2] = AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ; + props.Reverb.EchoTime = AL_EAXREVERB_DEFAULT_ECHO_TIME; + props.Reverb.EchoDepth = AL_EAXREVERB_DEFAULT_ECHO_DEPTH; + props.Reverb.ModulationTime = AL_EAXREVERB_DEFAULT_MODULATION_TIME; + props.Reverb.ModulationDepth = AL_EAXREVERB_DEFAULT_MODULATION_DEPTH; + props.Reverb.AirAbsorptionGainHF = AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF; + props.Reverb.HFReference = AL_EAXREVERB_DEFAULT_HFREFERENCE; + props.Reverb.LFReference = AL_EAXREVERB_DEFAULT_LFREFERENCE; + props.Reverb.RoomRolloffFactor = AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR; + props.Reverb.DecayHFLimit = AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT; + return props; +} + + +void StdReverb_setParami(EffectProps *props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_REVERB_DECAY_HFLIMIT: + if(!(val >= AL_REVERB_MIN_DECAY_HFLIMIT && val <= AL_REVERB_MAX_DECAY_HFLIMIT)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb decay hflimit out of range"); + props->Reverb.DecayHFLimit = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid reverb integer property 0x%04x", param); + } +} +void StdReverb_setParamiv(EffectProps *props, ALCcontext *context, ALenum param, const ALint *vals) +{ StdReverb_setParami(props, context, param, vals[0]); } +void StdReverb_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_REVERB_DENSITY: + if(!(val >= AL_REVERB_MIN_DENSITY && val <= AL_REVERB_MAX_DENSITY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb density out of range"); + props->Reverb.Density = val; + break; + + case AL_REVERB_DIFFUSION: + if(!(val >= AL_REVERB_MIN_DIFFUSION && val <= AL_REVERB_MAX_DIFFUSION)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb diffusion out of range"); + props->Reverb.Diffusion = val; + break; + + case AL_REVERB_GAIN: + if(!(val >= AL_REVERB_MIN_GAIN && val <= AL_REVERB_MAX_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb gain out of range"); + props->Reverb.Gain = val; + break; + + case AL_REVERB_GAINHF: + if(!(val >= AL_REVERB_MIN_GAINHF && val <= AL_REVERB_MAX_GAINHF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb gainhf out of range"); + props->Reverb.GainHF = val; + break; + + case AL_REVERB_DECAY_TIME: + if(!(val >= AL_REVERB_MIN_DECAY_TIME && val <= AL_REVERB_MAX_DECAY_TIME)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb decay time out of range"); + props->Reverb.DecayTime = val; + break; + + case AL_REVERB_DECAY_HFRATIO: + if(!(val >= AL_REVERB_MIN_DECAY_HFRATIO && val <= AL_REVERB_MAX_DECAY_HFRATIO)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb decay hfratio out of range"); + props->Reverb.DecayHFRatio = val; + break; + + case AL_REVERB_REFLECTIONS_GAIN: + if(!(val >= AL_REVERB_MIN_REFLECTIONS_GAIN && val <= AL_REVERB_MAX_REFLECTIONS_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb reflections gain out of range"); + props->Reverb.ReflectionsGain = val; + break; + + case AL_REVERB_REFLECTIONS_DELAY: + if(!(val >= AL_REVERB_MIN_REFLECTIONS_DELAY && val <= AL_REVERB_MAX_REFLECTIONS_DELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb reflections delay out of range"); + props->Reverb.ReflectionsDelay = val; + break; + + case AL_REVERB_LATE_REVERB_GAIN: + if(!(val >= AL_REVERB_MIN_LATE_REVERB_GAIN && val <= AL_REVERB_MAX_LATE_REVERB_GAIN)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb late reverb gain out of range"); + props->Reverb.LateReverbGain = val; + break; + + case AL_REVERB_LATE_REVERB_DELAY: + if(!(val >= AL_REVERB_MIN_LATE_REVERB_DELAY && val <= AL_REVERB_MAX_LATE_REVERB_DELAY)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb late reverb delay out of range"); + props->Reverb.LateReverbDelay = val; + break; + + case AL_REVERB_AIR_ABSORPTION_GAINHF: + if(!(val >= AL_REVERB_MIN_AIR_ABSORPTION_GAINHF && val <= AL_REVERB_MAX_AIR_ABSORPTION_GAINHF)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb air absorption gainhf out of range"); + props->Reverb.AirAbsorptionGainHF = val; + break; + + case AL_REVERB_ROOM_ROLLOFF_FACTOR: + if(!(val >= AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR && val <= AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Reverb room rolloff factor out of range"); + props->Reverb.RoomRolloffFactor = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid reverb float property 0x%04x", param); + } +} +void StdReverb_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ StdReverb_setParamf(props, context, param, vals[0]); } + +void StdReverb_getParami(const EffectProps *props, ALCcontext *context, ALenum param, ALint *val) +{ + switch(param) + { + case AL_REVERB_DECAY_HFLIMIT: + *val = props->Reverb.DecayHFLimit; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid reverb integer property 0x%04x", param); + } +} +void StdReverb_getParamiv(const EffectProps *props, ALCcontext *context, ALenum param, ALint *vals) +{ StdReverb_getParami(props, context, param, vals); } +void StdReverb_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_REVERB_DENSITY: + *val = props->Reverb.Density; + break; + + case AL_REVERB_DIFFUSION: + *val = props->Reverb.Diffusion; + break; + + case AL_REVERB_GAIN: + *val = props->Reverb.Gain; + break; + + case AL_REVERB_GAINHF: + *val = props->Reverb.GainHF; + break; + + case AL_REVERB_DECAY_TIME: + *val = props->Reverb.DecayTime; + break; + + case AL_REVERB_DECAY_HFRATIO: + *val = props->Reverb.DecayHFRatio; + break; + + case AL_REVERB_REFLECTIONS_GAIN: + *val = props->Reverb.ReflectionsGain; + break; + + case AL_REVERB_REFLECTIONS_DELAY: + *val = props->Reverb.ReflectionsDelay; + break; + + case AL_REVERB_LATE_REVERB_GAIN: + *val = props->Reverb.LateReverbGain; + break; + + case AL_REVERB_LATE_REVERB_DELAY: + *val = props->Reverb.LateReverbDelay; + break; + + case AL_REVERB_AIR_ABSORPTION_GAINHF: + *val = props->Reverb.AirAbsorptionGainHF; + break; + + case AL_REVERB_ROOM_ROLLOFF_FACTOR: + *val = props->Reverb.RoomRolloffFactor; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid reverb float property 0x%04x", param); + } +} +void StdReverb_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ StdReverb_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(StdReverb); + + +struct StdReverbStateFactory final : public EffectStateFactory { + EffectState *create() override { return new ReverbState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &StdReverb_vtable; } +}; + +EffectProps StdReverbStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Reverb.Density = AL_REVERB_DEFAULT_DENSITY; + props.Reverb.Diffusion = AL_REVERB_DEFAULT_DIFFUSION; + props.Reverb.Gain = AL_REVERB_DEFAULT_GAIN; + props.Reverb.GainHF = AL_REVERB_DEFAULT_GAINHF; + props.Reverb.GainLF = 1.0f; + props.Reverb.DecayTime = AL_REVERB_DEFAULT_DECAY_TIME; + props.Reverb.DecayHFRatio = AL_REVERB_DEFAULT_DECAY_HFRATIO; + props.Reverb.DecayLFRatio = 1.0f; + props.Reverb.ReflectionsGain = AL_REVERB_DEFAULT_REFLECTIONS_GAIN; + props.Reverb.ReflectionsDelay = AL_REVERB_DEFAULT_REFLECTIONS_DELAY; + props.Reverb.ReflectionsPan[0] = 0.0f; + props.Reverb.ReflectionsPan[1] = 0.0f; + props.Reverb.ReflectionsPan[2] = 0.0f; + props.Reverb.LateReverbGain = AL_REVERB_DEFAULT_LATE_REVERB_GAIN; + props.Reverb.LateReverbDelay = AL_REVERB_DEFAULT_LATE_REVERB_DELAY; + props.Reverb.LateReverbPan[0] = 0.0f; + props.Reverb.LateReverbPan[1] = 0.0f; + props.Reverb.LateReverbPan[2] = 0.0f; + props.Reverb.EchoTime = 0.25f; + props.Reverb.EchoDepth = 0.0f; + props.Reverb.ModulationTime = 0.25f; + props.Reverb.ModulationDepth = 0.0f; + props.Reverb.AirAbsorptionGainHF = AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF; + props.Reverb.HFReference = 5000.0f; + props.Reverb.LFReference = 250.0f; + props.Reverb.RoomRolloffFactor = AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR; + props.Reverb.DecayHFLimit = AL_REVERB_DEFAULT_DECAY_HFLIMIT; + return props; +} + +} // namespace + +EffectStateFactory *ReverbStateFactory_getFactory() +{ + static ReverbStateFactory ReverbFactory{}; + return &ReverbFactory; +} + +EffectStateFactory *StdReverbStateFactory_getFactory() +{ + static StdReverbStateFactory ReverbFactory{}; + return &ReverbFactory; +} diff --git a/alc/effects/vmorpher.cpp b/alc/effects/vmorpher.cpp new file mode 100644 index 00000000..eebba3f1 --- /dev/null +++ b/alc/effects/vmorpher.cpp @@ -0,0 +1,430 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2019 by Anis A. Hireche + * 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 <cmath> +#include <cstdlib> +#include <algorithm> +#include <functional> + +#include "alcmain.h" +#include "alcontext.h" +#include "alAuxEffectSlot.h" +#include "alError.h" +#include "alu.h" + +namespace { + +#define MAX_UPDATE_SAMPLES 128 +#define NUM_FORMANTS 4 +#define NUM_FILTERS 2 +#define Q_FACTOR 5.0f + +#define VOWEL_A_INDEX 0 +#define VOWEL_B_INDEX 1 + +#define WAVEFORM_FRACBITS 24 +#define WAVEFORM_FRACONE (1<<WAVEFORM_FRACBITS) +#define WAVEFORM_FRACMASK (WAVEFORM_FRACONE-1) + +inline ALfloat Sin(ALsizei index) +{ + constexpr ALfloat scale{al::MathDefs<float>::Tau() / ALfloat{WAVEFORM_FRACONE}}; + return std::sin(static_cast<ALfloat>(index) * scale)*0.5f + 0.5f; +} + +inline ALfloat Saw(ALsizei index) +{ + return static_cast<ALfloat>(index) / ALfloat{WAVEFORM_FRACONE}; +} + +inline ALfloat Triangle(ALsizei index) +{ + return std::fabs(static_cast<ALfloat>(index)*(2.0f/WAVEFORM_FRACONE) - 1.0f); +} + +inline ALfloat Half(ALsizei) +{ + return 0.5f; +} + +template<ALfloat func(ALsizei)> +void Oscillate(ALfloat *RESTRICT dst, ALsizei index, const ALsizei step, ALsizei todo) +{ + for(ALsizei i{0};i < todo;i++) + { + index += step; + index &= WAVEFORM_FRACMASK; + dst[i] = func(index); + } +} + +struct FormantFilter +{ + ALfloat f0norm{0.0f}; + ALfloat fGain{1.0f}; + ALfloat s1{0.0f}; + ALfloat s2{0.0f}; + + FormantFilter() = default; + FormantFilter(ALfloat f0norm_, ALfloat gain) : f0norm{f0norm_}, fGain{gain} { } + + inline void process(const ALfloat* samplesIn, ALfloat* samplesOut, const ALsizei numInput) + { + /* A state variable filter from a topology-preserving transform. + * Based on a talk given by Ivan Cohen: https://www.youtube.com/watch?v=esjHXGPyrhg + */ + const ALfloat g = std::tan(al::MathDefs<float>::Pi() * f0norm); + const ALfloat h = 1.0f / (1 + (g / Q_FACTOR) + (g * g)); + + for (ALsizei i{0};i < numInput;i++) + { + const ALfloat H = h * (samplesIn[i] - (1.0f / Q_FACTOR + g) * s1 - s2); + const ALfloat B = g * H + s1; + const ALfloat L = g * B + s2; + + s1 = g * H + B; + s2 = g * B + L; + + // Apply peak and accumulate samples. + samplesOut[i] += B * fGain; + } + } + + inline void clear() + { + s1 = 0.0f; + s2 = 0.0f; + } +}; + + +struct VmorpherState final : public EffectState { + struct { + /* Effect parameters */ + FormantFilter Formants[NUM_FILTERS][NUM_FORMANTS]; + + /* Effect gains for each channel */ + ALfloat CurrentGains[MAX_OUTPUT_CHANNELS]{}; + ALfloat TargetGains[MAX_OUTPUT_CHANNELS]{}; + } mChans[MAX_AMBI_CHANNELS]; + + void (*mGetSamples)(ALfloat* RESTRICT, ALsizei, const ALsizei, ALsizei) {}; + + ALsizei mIndex{0}; + ALsizei mStep{1}; + + /* Effects buffers */ + ALfloat mSampleBufferA[MAX_UPDATE_SAMPLES]{}; + ALfloat mSampleBufferB[MAX_UPDATE_SAMPLES]{}; + + ALboolean deviceUpdate(const ALCdevice *device) override; + void update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) override; + void process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) override; + + static std::array<FormantFilter,4> getFiltersByPhoneme(ALenum phoneme, ALfloat frequency, ALfloat pitch); + + DEF_NEWDEL(VmorpherState) +}; + +std::array<FormantFilter,4> VmorpherState::getFiltersByPhoneme(ALenum phoneme, ALfloat frequency, ALfloat pitch) +{ + /* Using soprano formant set of values to + * better match mid-range frequency space. + * + * See: https://www.classes.cs.uchicago.edu/archive/1999/spring/CS295/Computing_Resources/Csound/CsManual3.48b1.HTML/Appendices/table3.html + */ + switch(phoneme) + { + case AL_VOCAL_MORPHER_PHONEME_A: + return {{ + {( 800 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f, 0 / 20.0f); */ + {(1150 * pitch) / frequency, 0.501187f}, /* std::pow(10.0f, -6 / 20.0f); */ + {(2900 * pitch) / frequency, 0.025118f}, /* std::pow(10.0f, -32 / 20.0f); */ + {(3900 * pitch) / frequency, 0.100000f} /* std::pow(10.0f, -20 / 20.0f); */ + }}; + case AL_VOCAL_MORPHER_PHONEME_E: + return {{ + {( 350 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f, 0 / 20.0f); */ + {(2000 * pitch) / frequency, 0.100000f}, /* std::pow(10.0f, -20 / 20.0f); */ + {(2800 * pitch) / frequency, 0.177827f}, /* std::pow(10.0f, -15 / 20.0f); */ + {(3600 * pitch) / frequency, 0.009999f} /* std::pow(10.0f, -40 / 20.0f); */ + }}; + case AL_VOCAL_MORPHER_PHONEME_I: + return {{ + {( 270 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f, 0 / 20.0f); */ + {(2140 * pitch) / frequency, 0.251188f}, /* std::pow(10.0f, -12 / 20.0f); */ + {(2950 * pitch) / frequency, 0.050118f}, /* std::pow(10.0f, -26 / 20.0f); */ + {(3900 * pitch) / frequency, 0.050118f} /* std::pow(10.0f, -26 / 20.0f); */ + }}; + case AL_VOCAL_MORPHER_PHONEME_O: + return {{ + {( 450 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f, 0 / 20.0f); */ + {( 800 * pitch) / frequency, 0.281838f}, /* std::pow(10.0f, -11 / 20.0f); */ + {(2830 * pitch) / frequency, 0.079432f}, /* std::pow(10.0f, -22 / 20.0f); */ + {(3800 * pitch) / frequency, 0.079432f} /* std::pow(10.0f, -22 / 20.0f); */ + }}; + case AL_VOCAL_MORPHER_PHONEME_U: + return {{ + {( 325 * pitch) / frequency, 1.000000f}, /* std::pow(10.0f, 0 / 20.0f); */ + {( 700 * pitch) / frequency, 0.158489f}, /* std::pow(10.0f, -16 / 20.0f); */ + {(2700 * pitch) / frequency, 0.017782f}, /* std::pow(10.0f, -35 / 20.0f); */ + {(3800 * pitch) / frequency, 0.009999f} /* std::pow(10.0f, -40 / 20.0f); */ + }}; + } + return {}; +} + + +ALboolean VmorpherState::deviceUpdate(const ALCdevice* /*device*/) +{ + for(auto &e : mChans) + { + std::for_each(std::begin(e.Formants[VOWEL_A_INDEX]), std::end(e.Formants[VOWEL_A_INDEX]), + std::mem_fn(&FormantFilter::clear)); + std::for_each(std::begin(e.Formants[VOWEL_B_INDEX]), std::end(e.Formants[VOWEL_B_INDEX]), + std::mem_fn(&FormantFilter::clear)); + std::fill(std::begin(e.CurrentGains), std::end(e.CurrentGains), 0.0f); + } + + return AL_TRUE; +} + +void VmorpherState::update(const ALCcontext *context, const ALeffectslot *slot, const EffectProps *props, const EffectTarget target) +{ + const ALCdevice *device{context->Device}; + const ALfloat frequency{static_cast<ALfloat>(device->Frequency)}; + const ALfloat step{props->Vmorpher.Rate / static_cast<ALfloat>(device->Frequency)}; + mStep = fastf2i(clampf(step*WAVEFORM_FRACONE, 0.0f, ALfloat{WAVEFORM_FRACONE-1})); + + if(mStep == 0) + mGetSamples = Oscillate<Half>; + else if(props->Vmorpher.Waveform == AL_VOCAL_MORPHER_WAVEFORM_SINUSOID) + mGetSamples = Oscillate<Sin>; + else if(props->Vmorpher.Waveform == AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH) + mGetSamples = Oscillate<Saw>; + else /*if(props->Vmorpher.Waveform == AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE)*/ + mGetSamples = Oscillate<Triangle>; + + const ALfloat pitchA{fastf2i(std::pow(2.0f, props->Vmorpher.PhonemeACoarseTuning*100.0f / 2400.0f)*FRACTIONONE) * (1.0f/FRACTIONONE)}; + const ALfloat pitchB{fastf2i(std::pow(2.0f, props->Vmorpher.PhonemeBCoarseTuning*100.0f / 2400.0f)*FRACTIONONE) * (1.0f/FRACTIONONE)}; + + auto vowelA = getFiltersByPhoneme(props->Vmorpher.PhonemeA, frequency, pitchA); + auto vowelB = getFiltersByPhoneme(props->Vmorpher.PhonemeB, frequency, pitchB); + + /* Copy the filter coefficients to the input channels. */ + for(size_t i{0u};i < slot->Wet.Buffer.size();++i) + { + std::copy(vowelA.begin(), vowelA.end(), std::begin(mChans[i].Formants[VOWEL_A_INDEX])); + std::copy(vowelB.begin(), vowelB.end(), std::begin(mChans[i].Formants[VOWEL_B_INDEX])); + } + + mOutTarget = target.Main->Buffer; + for(size_t i{0u};i < slot->Wet.Buffer.size();++i) + { + auto coeffs = GetAmbiIdentityRow(i); + ComputePanGains(target.Main, coeffs.data(), slot->Params.Gain, mChans[i].TargetGains); + } +} + +void VmorpherState::process(const ALsizei samplesToDo, const FloatBufferLine *RESTRICT samplesIn, const ALsizei numInput, const al::span<FloatBufferLine> samplesOut) +{ + /* Following the EFX specification for a conformant implementation which describes + * the effect as a pair of 4-band formant filters blended together using an LFO. + */ + for(ALsizei base{0};base < samplesToDo;) + { + alignas(16) ALfloat lfo[MAX_UPDATE_SAMPLES]; + const ALsizei td = mini(MAX_UPDATE_SAMPLES, samplesToDo-base); + + mGetSamples(lfo, mIndex, mStep, td); + mIndex += (mStep * td) & WAVEFORM_FRACMASK; + mIndex &= WAVEFORM_FRACMASK; + + ASSUME(numInput > 0); + for(ALsizei c{0};c < numInput;c++) + { + for (ALsizei i{0};i < td;i++) + { + mSampleBufferA[i] = 0.0f; + mSampleBufferB[i] = 0.0f; + } + + auto& vowelA = mChans[c].Formants[VOWEL_A_INDEX]; + auto& vowelB = mChans[c].Formants[VOWEL_B_INDEX]; + + /* Process first vowel. */ + vowelA[0].process(&samplesIn[c][base], mSampleBufferA, td); + vowelA[1].process(&samplesIn[c][base], mSampleBufferA, td); + vowelA[2].process(&samplesIn[c][base], mSampleBufferA, td); + vowelA[3].process(&samplesIn[c][base], mSampleBufferA, td); + + /* Process second vowel. */ + vowelB[0].process(&samplesIn[c][base], mSampleBufferB, td); + vowelB[1].process(&samplesIn[c][base], mSampleBufferB, td); + vowelB[2].process(&samplesIn[c][base], mSampleBufferB, td); + vowelB[3].process(&samplesIn[c][base], mSampleBufferB, td); + + alignas(16) ALfloat samplesBlended[MAX_UPDATE_SAMPLES]; + + for (ALsizei i{0};i < td;i++) + samplesBlended[i] = lerp(mSampleBufferA[i], mSampleBufferB[i], lfo[i]); + + /* Now, mix the processed sound data to the output. */ + MixSamples(samplesBlended, samplesOut, mChans[c].CurrentGains, mChans[c].TargetGains, + samplesToDo-base, base, td); + } + + base += td; + } +} + + +void Vmorpher_setParami(EffectProps* props, ALCcontext *context, ALenum param, ALint val) +{ + switch(param) + { + case AL_VOCAL_MORPHER_WAVEFORM: + if(!(val >= AL_VOCAL_MORPHER_MIN_WAVEFORM && val <= AL_VOCAL_MORPHER_MAX_WAVEFORM)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Vocal morpher waveform out of range"); + props->Vmorpher.Waveform = val; + break; + + case AL_VOCAL_MORPHER_PHONEMEA: + if(!(val >= AL_VOCAL_MORPHER_MIN_PHONEMEA && val <= AL_VOCAL_MORPHER_MAX_PHONEMEA)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Vocal morpher phoneme-a out of range"); + props->Vmorpher.PhonemeA = val; + break; + + case AL_VOCAL_MORPHER_PHONEMEB: + if(!(val >= AL_VOCAL_MORPHER_MIN_PHONEMEB && val <= AL_VOCAL_MORPHER_MAX_PHONEMEB)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Vocal morpher phoneme-b out of range"); + props->Vmorpher.PhonemeB = val; + break; + + case AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING: + if(!(val >= AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING && val <= AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Vocal morpher phoneme-a coarse tuning out of range"); + props->Vmorpher.PhonemeACoarseTuning = val; + break; + + case AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING: + if(!(val >= AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING && val <= AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Vocal morpher phoneme-b coarse tuning out of range"); + props->Vmorpher.PhonemeBCoarseTuning = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid vocal morpher integer property 0x%04x", param); + } +} +void Vmorpher_setParamiv(EffectProps*, ALCcontext *context, ALenum param, const ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid vocal morpher integer-vector property 0x%04x", param); } +void Vmorpher_setParamf(EffectProps *props, ALCcontext *context, ALenum param, ALfloat val) +{ + switch(param) + { + case AL_VOCAL_MORPHER_RATE: + if(!(val >= AL_VOCAL_MORPHER_MIN_RATE && val <= AL_VOCAL_MORPHER_MAX_RATE)) + SETERR_RETURN(context, AL_INVALID_VALUE,, "Vocal morpher rate out of range"); + props->Vmorpher.Rate = val; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid vocal morpher float property 0x%04x", param); + } +} +void Vmorpher_setParamfv(EffectProps *props, ALCcontext *context, ALenum param, const ALfloat *vals) +{ Vmorpher_setParamf(props, context, param, vals[0]); } + +void Vmorpher_getParami(const EffectProps* props, ALCcontext *context, ALenum param, ALint* val) +{ + switch(param) + { + case AL_VOCAL_MORPHER_PHONEMEA: + *val = props->Vmorpher.PhonemeA; + break; + + case AL_VOCAL_MORPHER_PHONEMEB: + *val = props->Vmorpher.PhonemeB; + break; + + case AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING: + *val = props->Vmorpher.PhonemeACoarseTuning; + break; + + case AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING: + *val = props->Vmorpher.PhonemeBCoarseTuning; + break; + + case AL_VOCAL_MORPHER_WAVEFORM: + *val = props->Vmorpher.Waveform; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid vocal morpher integer property 0x%04x", param); + } +} +void Vmorpher_getParamiv(const EffectProps*, ALCcontext *context, ALenum param, ALint*) +{ alSetError(context, AL_INVALID_ENUM, "Invalid vocal morpher integer-vector property 0x%04x", param); } +void Vmorpher_getParamf(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *val) +{ + switch(param) + { + case AL_VOCAL_MORPHER_RATE: + *val = props->Vmorpher.Rate; + break; + + default: + alSetError(context, AL_INVALID_ENUM, "Invalid vocal morpher float property 0x%04x", param); + } +} +void Vmorpher_getParamfv(const EffectProps *props, ALCcontext *context, ALenum param, ALfloat *vals) +{ Vmorpher_getParamf(props, context, param, vals); } + +DEFINE_ALEFFECT_VTABLE(Vmorpher); + + +struct VmorpherStateFactory final : public EffectStateFactory { + EffectState *create() override { return new VmorpherState{}; } + EffectProps getDefaultProps() const noexcept override; + const EffectVtable *getEffectVtable() const noexcept override { return &Vmorpher_vtable; } +}; + +EffectProps VmorpherStateFactory::getDefaultProps() const noexcept +{ + EffectProps props{}; + props.Vmorpher.Rate = AL_VOCAL_MORPHER_DEFAULT_RATE; + props.Vmorpher.PhonemeA = AL_VOCAL_MORPHER_DEFAULT_PHONEMEA; + props.Vmorpher.PhonemeB = AL_VOCAL_MORPHER_DEFAULT_PHONEMEB; + props.Vmorpher.PhonemeACoarseTuning = AL_VOCAL_MORPHER_DEFAULT_PHONEMEA_COARSE_TUNING; + props.Vmorpher.PhonemeBCoarseTuning = AL_VOCAL_MORPHER_DEFAULT_PHONEMEB_COARSE_TUNING; + props.Vmorpher.Waveform = AL_VOCAL_MORPHER_DEFAULT_WAVEFORM; + return props; +} + +} // namespace + +EffectStateFactory *VmorpherStateFactory_getFactory() +{ + static VmorpherStateFactory VmorpherFactory{}; + return &VmorpherFactory; +} diff --git a/alc/filters/biquad.cpp b/alc/filters/biquad.cpp new file mode 100644 index 00000000..6a3cef64 --- /dev/null +++ b/alc/filters/biquad.cpp @@ -0,0 +1,127 @@ + +#include "config.h" + +#include "biquad.h" + +#include <algorithm> +#include <cassert> +#include <cmath> + +#include "opthelpers.h" + + +template<typename Real> +void BiquadFilterR<Real>::setParams(BiquadType type, Real gain, Real f0norm, Real rcpQ) +{ + // Limit gain to -100dB + assert(gain > 0.00001f); + + const Real w0{al::MathDefs<Real>::Tau() * f0norm}; + const Real sin_w0{std::sin(w0)}; + const Real cos_w0{std::cos(w0)}; + const Real alpha{sin_w0/2.0f * rcpQ}; + + Real sqrtgain_alpha_2; + Real a[3]{ 1.0f, 0.0f, 0.0f }; + Real b[3]{ 1.0f, 0.0f, 0.0f }; + + /* Calculate filter coefficients depending on filter type */ + switch(type) + { + case BiquadType::HighShelf: + sqrtgain_alpha_2 = 2.0f * std::sqrt(gain) * alpha; + b[0] = gain*((gain+1.0f) + (gain-1.0f)*cos_w0 + sqrtgain_alpha_2); + b[1] = -2.0f*gain*((gain-1.0f) + (gain+1.0f)*cos_w0 ); + b[2] = gain*((gain+1.0f) + (gain-1.0f)*cos_w0 - sqrtgain_alpha_2); + a[0] = (gain+1.0f) - (gain-1.0f)*cos_w0 + sqrtgain_alpha_2; + a[1] = 2.0f* ((gain-1.0f) - (gain+1.0f)*cos_w0 ); + a[2] = (gain+1.0f) - (gain-1.0f)*cos_w0 - sqrtgain_alpha_2; + break; + case BiquadType::LowShelf: + sqrtgain_alpha_2 = 2.0f * std::sqrt(gain) * alpha; + b[0] = gain*((gain+1.0f) - (gain-1.0f)*cos_w0 + sqrtgain_alpha_2); + b[1] = 2.0f*gain*((gain-1.0f) - (gain+1.0f)*cos_w0 ); + b[2] = gain*((gain+1.0f) - (gain-1.0f)*cos_w0 - sqrtgain_alpha_2); + a[0] = (gain+1.0f) + (gain-1.0f)*cos_w0 + sqrtgain_alpha_2; + a[1] = -2.0f* ((gain-1.0f) + (gain+1.0f)*cos_w0 ); + a[2] = (gain+1.0f) + (gain-1.0f)*cos_w0 - sqrtgain_alpha_2; + break; + case BiquadType::Peaking: + gain = std::sqrt(gain); + b[0] = 1.0f + alpha * gain; + b[1] = -2.0f * cos_w0; + b[2] = 1.0f - alpha * gain; + a[0] = 1.0f + alpha / gain; + a[1] = -2.0f * cos_w0; + a[2] = 1.0f - alpha / gain; + break; + + case BiquadType::LowPass: + b[0] = (1.0f - cos_w0) / 2.0f; + b[1] = 1.0f - cos_w0; + b[2] = (1.0f - cos_w0) / 2.0f; + a[0] = 1.0f + alpha; + a[1] = -2.0f * cos_w0; + a[2] = 1.0f - alpha; + break; + case BiquadType::HighPass: + b[0] = (1.0f + cos_w0) / 2.0f; + b[1] = -(1.0f + cos_w0); + b[2] = (1.0f + cos_w0) / 2.0f; + a[0] = 1.0f + alpha; + a[1] = -2.0f * cos_w0; + a[2] = 1.0f - alpha; + break; + case BiquadType::BandPass: + b[0] = alpha; + b[1] = 0.0f; + b[2] = -alpha; + a[0] = 1.0f + alpha; + a[1] = -2.0f * cos_w0; + a[2] = 1.0f - alpha; + break; + } + + a1 = a[1] / a[0]; + a2 = a[2] / a[0]; + b0 = b[0] / a[0]; + b1 = b[1] / a[0]; + b2 = b[2] / a[0]; +} + +template<typename Real> +void BiquadFilterR<Real>::process(Real *dst, const Real *src, int numsamples) +{ + ASSUME(numsamples > 0); + + const Real b0{this->b0}; + const Real b1{this->b1}; + const Real b2{this->b2}; + const Real a1{this->a1}; + const Real a2{this->a2}; + Real z1{this->z1}; + Real z2{this->z2}; + + /* Processing loop is Transposed Direct Form II. This requires less storage + * compared to Direct Form I (only two delay components, instead of a four- + * sample history; the last two inputs and outputs), and works better for + * floating-point which favors summing similarly-sized values while being + * less bothered by overflow. + * + * See: http://www.earlevel.com/main/2003/02/28/biquads/ + */ + auto proc_sample = [b0,b1,b2,a1,a2,&z1,&z2](Real input) noexcept -> Real + { + Real output = input*b0 + z1; + z1 = input*b1 - output*a1 + z2; + z2 = input*b2 - output*a2; + return output; + }; + std::transform(src, src+numsamples, dst, proc_sample); + + this->z1 = z1; + this->z2 = z2; +} + +template class BiquadFilterR<float>; +template class BiquadFilterR<double>; diff --git a/alc/filters/biquad.h b/alc/filters/biquad.h new file mode 100644 index 00000000..893a69a9 --- /dev/null +++ b/alc/filters/biquad.h @@ -0,0 +1,113 @@ +#ifndef FILTERS_BIQUAD_H +#define FILTERS_BIQUAD_H + +#include <cmath> +#include <utility> + +#include "math_defs.h" + + +/* Filters implementation is based on the "Cookbook formulae for audio + * EQ biquad filter coefficients" by Robert Bristow-Johnson + * http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt + */ +/* Implementation note: For the shelf filters, the specified gain is for the + * reference frequency, which is the centerpoint of the transition band. This + * better matches EFX filter design. To set the gain for the shelf itself, use + * the square root of the desired linear gain (or halve the dB gain). + */ + +enum class BiquadType { + /** EFX-style low-pass filter, specifying a gain and reference frequency. */ + HighShelf, + /** EFX-style high-pass filter, specifying a gain and reference frequency. */ + LowShelf, + /** Peaking filter, specifying a gain and reference frequency. */ + Peaking, + + /** Low-pass cut-off filter, specifying a cut-off frequency. */ + LowPass, + /** High-pass cut-off filter, specifying a cut-off frequency. */ + HighPass, + /** Band-pass filter, specifying a center frequency. */ + BandPass, +}; + +template<typename Real> +class BiquadFilterR { + /* Last two delayed components for direct form II. */ + Real z1{0.0f}, z2{0.0f}; + /* Transfer function coefficients "b" (numerator) */ + Real b0{1.0f}, b1{0.0f}, b2{0.0f}; + /* Transfer function coefficients "a" (denominator; a0 is pre-applied). */ + Real a1{0.0f}, a2{0.0f}; + +public: + void clear() noexcept { z1 = z2 = 0.0f; } + + /** + * Sets the filter state for the specified filter type and its parameters. + * + * \param type The type of filter to apply. + * \param gain The gain for the reference frequency response. Only used by + * the Shelf and Peaking filter types. + * \param f0norm The reference frequency normal (ref_freq / sample_rate). + * This is the center point for the Shelf, Peaking, and + * BandPass filter types, or the cutoff frequency for the + * LowPass and HighPass filter types. + * \param rcpQ The reciprocal of the Q coefficient for the filter's + * transition band. Can be generated from rcpQFromSlope or + * rcpQFromBandwidth as needed. + */ + void setParams(BiquadType type, Real gain, Real f0norm, Real rcpQ); + + void copyParamsFrom(const BiquadFilterR &other) + { + b0 = other.b0; + b1 = other.b1; + b2 = other.b2; + a1 = other.a1; + a2 = other.a2; + } + + + void process(Real *dst, const Real *src, int numsamples); + + /* Rather hacky. It's just here to support "manual" processing. */ + std::pair<Real,Real> getComponents() const noexcept + { return {z1, z2}; } + void setComponents(Real z1_, Real z2_) noexcept + { z1 = z1_; z2 = z2_; } + Real processOne(const Real in, Real &z1_, Real &z2_) const noexcept + { + Real out{in*b0 + z1_}; + z1_ = in*b1 - out*a1 + z2_; + z2_ = in*b2 - out*a2; + return out; + } + + /** + * Calculates the rcpQ (i.e. 1/Q) coefficient for shelving filters, using + * the reference gain and shelf slope parameter. + * \param gain 0 < gain + * \param slope 0 < slope <= 1 + */ + static Real rcpQFromSlope(Real gain, Real slope) + { return std::sqrt((gain + 1.0f/gain)*(1.0f/slope - 1.0f) + 2.0f); } + + /** + * Calculates the rcpQ (i.e. 1/Q) coefficient for filters, using the + * normalized reference frequency and bandwidth. + * \param f0norm 0 < f0norm < 0.5. + * \param bandwidth 0 < bandwidth + */ + static Real rcpQFromBandwidth(Real f0norm, Real bandwidth) + { + const Real w0{al::MathDefs<Real>::Tau() * f0norm}; + return 2.0f*std::sinh(std::log(Real{2.0f})/2.0f*bandwidth*w0/std::sin(w0)); + } +}; + +using BiquadFilter = BiquadFilterR<float>; + +#endif /* FILTERS_BIQUAD_H */ diff --git a/alc/filters/nfc.cpp b/alc/filters/nfc.cpp new file mode 100644 index 00000000..1a567f2c --- /dev/null +++ b/alc/filters/nfc.cpp @@ -0,0 +1,391 @@ + +#include "config.h" + +#include "nfc.h" + +#include <algorithm> + +#include "alcmain.h" + + +/* Near-field control filters are the basis for handling the near-field effect. + * The near-field effect is a bass-boost present in the directional components + * of a recorded signal, created as a result of the wavefront curvature (itself + * a function of sound distance). Proper reproduction dictates this be + * compensated for using a bass-cut given the playback speaker distance, to + * avoid excessive bass in the playback. + * + * For real-time rendered audio, emulating the near-field effect based on the + * sound source's distance, and subsequently compensating for it at output + * based on the speaker distances, can create a more realistic perception of + * sound distance beyond a simple 1/r attenuation. + * + * These filters do just that. Each one applies a low-shelf filter, created as + * the combination of a bass-boost for a given sound source distance (near- + * field emulation) along with a bass-cut for a given control/speaker distance + * (near-field compensation). + * + * Note that it is necessary to apply a cut along with the boost, since the + * boost alone is unstable in higher-order ambisonics as it causes an infinite + * DC gain (even first-order ambisonics requires there to be no DC offset for + * the boost to work). Consequently, ambisonics requires a control parameter to + * be used to avoid an unstable boost-only filter. NFC-HOA defines this control + * as a reference delay, calculated with: + * + * reference_delay = control_distance / speed_of_sound + * + * This means w0 (for input) or w1 (for output) should be set to: + * + * wN = 1 / (reference_delay * sample_rate) + * + * when dealing with NFC-HOA content. For FOA input content, which does not + * specify a reference_delay variable, w0 should be set to 0 to apply only + * near-field compensation for output. It's important that w1 be a finite, + * positive, non-0 value or else the bass-boost will become unstable again. + * Also, w0 should not be too large compared to w1, to avoid excessively loud + * low frequencies. + */ + +namespace { + +constexpr float B[5][4] = { + { 0.0f }, + { 1.0f }, + { 3.0f, 3.0f }, + { 3.6778f, 6.4595f, 2.3222f }, + { 4.2076f, 11.4877f, 5.7924f, 9.1401f } +}; + +NfcFilter1 NfcFilterCreate1(const float w0, const float w1) noexcept +{ + NfcFilter1 nfc{}; + float b_00, g_0; + float r; + + nfc.base_gain = 1.0f; + nfc.gain = 1.0f; + + /* Calculate bass-boost coefficients. */ + r = 0.5f * w0; + b_00 = B[1][0] * r; + g_0 = 1.0f + b_00; + + nfc.gain *= g_0; + nfc.b1 = 2.0f * b_00 / g_0; + + /* Calculate bass-cut coefficients. */ + r = 0.5f * w1; + b_00 = B[1][0] * r; + g_0 = 1.0f + b_00; + + nfc.base_gain /= g_0; + nfc.gain /= g_0; + nfc.a1 = 2.0f * b_00 / g_0; + + return nfc; +} + +void NfcFilterAdjust1(NfcFilter1 *nfc, const float w0) noexcept +{ + const float r{0.5f * w0}; + const float b_00{B[1][0] * r}; + const float g_0{1.0f + b_00}; + + nfc->gain = nfc->base_gain * g_0; + nfc->b1 = 2.0f * b_00 / g_0; +} + + +NfcFilter2 NfcFilterCreate2(const float w0, const float w1) noexcept +{ + NfcFilter2 nfc{}; + float b_10, b_11, g_1; + float r; + + nfc.base_gain = 1.0f; + nfc.gain = 1.0f; + + /* Calculate bass-boost coefficients. */ + r = 0.5f * w0; + b_10 = B[2][0] * r; + b_11 = B[2][1] * r * r; + g_1 = 1.0f + b_10 + b_11; + + nfc.gain *= g_1; + nfc.b1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc.b2 = 4.0f * b_11 / g_1; + + /* Calculate bass-cut coefficients. */ + r = 0.5f * w1; + b_10 = B[2][0] * r; + b_11 = B[2][1] * r * r; + g_1 = 1.0f + b_10 + b_11; + + nfc.base_gain /= g_1; + nfc.gain /= g_1; + nfc.a1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc.a2 = 4.0f * b_11 / g_1; + + return nfc; +} + +void NfcFilterAdjust2(NfcFilter2 *nfc, const float w0) noexcept +{ + const float r{0.5f * w0}; + const float b_10{B[2][0] * r}; + const float b_11{B[2][1] * r * r}; + const float g_1{1.0f + b_10 + b_11}; + + nfc->gain = nfc->base_gain * g_1; + nfc->b1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc->b2 = 4.0f * b_11 / g_1; +} + + +NfcFilter3 NfcFilterCreate3(const float w0, const float w1) noexcept +{ + NfcFilter3 nfc{}; + float b_10, b_11, g_1; + float b_00, g_0; + float r; + + nfc.base_gain = 1.0f; + nfc.gain = 1.0f; + + /* Calculate bass-boost coefficients. */ + r = 0.5f * w0; + b_10 = B[3][0] * r; + b_11 = B[3][1] * r * r; + b_00 = B[3][2] * r; + g_1 = 1.0f + b_10 + b_11; + g_0 = 1.0f + b_00; + + nfc.gain *= g_1 * g_0; + nfc.b1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc.b2 = 4.0f * b_11 / g_1; + nfc.b3 = 2.0f * b_00 / g_0; + + /* Calculate bass-cut coefficients. */ + r = 0.5f * w1; + b_10 = B[3][0] * r; + b_11 = B[3][1] * r * r; + b_00 = B[3][2] * r; + g_1 = 1.0f + b_10 + b_11; + g_0 = 1.0f + b_00; + + nfc.base_gain /= g_1 * g_0; + nfc.gain /= g_1 * g_0; + nfc.a1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc.a2 = 4.0f * b_11 / g_1; + nfc.a3 = 2.0f * b_00 / g_0; + + return nfc; +} + +void NfcFilterAdjust3(NfcFilter3 *nfc, const float w0) noexcept +{ + const float r{0.5f * w0}; + const float b_10{B[3][0] * r}; + const float b_11{B[3][1] * r * r}; + const float b_00{B[3][2] * r}; + const float g_1{1.0f + b_10 + b_11}; + const float g_0{1.0f + b_00}; + + nfc->gain = nfc->base_gain * g_1 * g_0; + nfc->b1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc->b2 = 4.0f * b_11 / g_1; + nfc->b3 = 2.0f * b_00 / g_0; +} + + +NfcFilter4 NfcFilterCreate4(const float w0, const float w1) noexcept +{ + NfcFilter4 nfc{}; + float b_10, b_11, g_1; + float b_00, b_01, g_0; + float r; + + nfc.base_gain = 1.0f; + nfc.gain = 1.0f; + + /* Calculate bass-boost coefficients. */ + r = 0.5f * w0; + b_10 = B[4][0] * r; + b_11 = B[4][1] * r * r; + b_00 = B[4][2] * r; + b_01 = B[4][3] * r * r; + g_1 = 1.0f + b_10 + b_11; + g_0 = 1.0f + b_00 + b_01; + + nfc.gain *= g_1 * g_0; + nfc.b1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc.b2 = 4.0f * b_11 / g_1; + nfc.b3 = (2.0f*b_00 + 4.0f*b_01) / g_0; + nfc.b4 = 4.0f * b_01 / g_0; + + /* Calculate bass-cut coefficients. */ + r = 0.5f * w1; + b_10 = B[4][0] * r; + b_11 = B[4][1] * r * r; + b_00 = B[4][2] * r; + b_01 = B[4][3] * r * r; + g_1 = 1.0f + b_10 + b_11; + g_0 = 1.0f + b_00 + b_01; + + nfc.base_gain /= g_1 * g_0; + nfc.gain /= g_1 * g_0; + nfc.a1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc.a2 = 4.0f * b_11 / g_1; + nfc.a3 = (2.0f*b_00 + 4.0f*b_01) / g_0; + nfc.a4 = 4.0f * b_01 / g_0; + + return nfc; +} + +void NfcFilterAdjust4(NfcFilter4 *nfc, const float w0) noexcept +{ + const float r{0.5f * w0}; + const float b_10{B[4][0] * r}; + const float b_11{B[4][1] * r * r}; + const float b_00{B[4][2] * r}; + const float b_01{B[4][3] * r * r}; + const float g_1{1.0f + b_10 + b_11}; + const float g_0{1.0f + b_00 + b_01}; + + nfc->gain = nfc->base_gain * g_1 * g_0; + nfc->b1 = (2.0f*b_10 + 4.0f*b_11) / g_1; + nfc->b2 = 4.0f * b_11 / g_1; + nfc->b3 = (2.0f*b_00 + 4.0f*b_01) / g_0; + nfc->b4 = 4.0f * b_01 / g_0; +} + +} // namespace + +void NfcFilter::init(const float w1) noexcept +{ + first = NfcFilterCreate1(0.0f, w1); + second = NfcFilterCreate2(0.0f, w1); + third = NfcFilterCreate3(0.0f, w1); + fourth = NfcFilterCreate4(0.0f, w1); +} + +void NfcFilter::adjust(const float w0) noexcept +{ + NfcFilterAdjust1(&first, w0); + NfcFilterAdjust2(&second, w0); + NfcFilterAdjust3(&third, w0); + NfcFilterAdjust4(&fourth, w0); +} + + +void NfcFilter::process1(float *RESTRICT dst, const float *RESTRICT src, const int count) +{ + ASSUME(count > 0); + + const float gain{first.gain}; + const float b1{first.b1}; + const float a1{first.a1}; + float z1{first.z[0]}; + auto proc_sample = [gain,b1,a1,&z1](const float in) noexcept -> float + { + const float y{in*gain - a1*z1}; + const float out{y + b1*z1}; + z1 += y; + return out; + }; + std::transform(src, src+count, dst, proc_sample); + first.z[0] = z1; +} + +void NfcFilter::process2(float *RESTRICT dst, const float *RESTRICT src, const int count) +{ + ASSUME(count > 0); + + const float gain{second.gain}; + const float b1{second.b1}; + const float b2{second.b2}; + const float a1{second.a1}; + const float a2{second.a2}; + float z1{second.z[0]}; + float z2{second.z[1]}; + auto proc_sample = [gain,b1,b2,a1,a2,&z1,&z2](const float in) noexcept -> float + { + const float y{in*gain - a1*z1 - a2*z2}; + const float out{y + b1*z1 + b2*z2}; + z2 += z1; + z1 += y; + return out; + }; + std::transform(src, src+count, dst, proc_sample); + second.z[0] = z1; + second.z[1] = z2; +} + +void NfcFilter::process3(float *RESTRICT dst, const float *RESTRICT src, const int count) +{ + ASSUME(count > 0); + + const float gain{third.gain}; + const float b1{third.b1}; + const float b2{third.b2}; + const float b3{third.b3}; + const float a1{third.a1}; + const float a2{third.a2}; + const float a3{third.a3}; + float z1{third.z[0]}; + float z2{third.z[1]}; + float z3{third.z[2]}; + auto proc_sample = [gain,b1,b2,b3,a1,a2,a3,&z1,&z2,&z3](const float in) noexcept -> float + { + float y{in*gain - a1*z1 - a2*z2}; + float out{y + b1*z1 + b2*z2}; + z2 += z1; + z1 += y; + + y = out - a3*z3; + out = y + b3*z3; + z3 += y; + return out; + }; + std::transform(src, src+count, dst, proc_sample); + third.z[0] = z1; + third.z[1] = z2; + third.z[2] = z3; +} + +void NfcFilter::process4(float *RESTRICT dst, const float *RESTRICT src, const int count) +{ + ASSUME(count > 0); + + const float gain{fourth.gain}; + const float b1{fourth.b1}; + const float b2{fourth.b2}; + const float b3{fourth.b3}; + const float b4{fourth.b4}; + const float a1{fourth.a1}; + const float a2{fourth.a2}; + const float a3{fourth.a3}; + const float a4{fourth.a4}; + float z1{fourth.z[0]}; + float z2{fourth.z[1]}; + float z3{fourth.z[2]}; + float z4{fourth.z[3]}; + auto proc_sample = [gain,b1,b2,b3,b4,a1,a2,a3,a4,&z1,&z2,&z3,&z4](const float in) noexcept -> float + { + float y{in*gain - a1*z1 - a2*z2}; + float out{y + b1*z1 + b2*z2}; + z2 += z1; + z1 += y; + + y = out - a3*z3 - a4*z4; + out = y + b3*z3 + b4*z4; + z4 += z3; + z3 += y; + return out; + }; + std::transform(src, src+count, dst, proc_sample); + fourth.z[0] = z1; + fourth.z[1] = z2; + fourth.z[2] = z3; + fourth.z[3] = z4; +} diff --git a/alc/filters/nfc.h b/alc/filters/nfc.h new file mode 100644 index 00000000..b656850a --- /dev/null +++ b/alc/filters/nfc.h @@ -0,0 +1,58 @@ +#ifndef FILTER_NFC_H +#define FILTER_NFC_H + +struct NfcFilter1 { + float base_gain, gain; + float b1, a1; + float z[1]; +}; +struct NfcFilter2 { + float base_gain, gain; + float b1, b2, a1, a2; + float z[2]; +}; +struct NfcFilter3 { + float base_gain, gain; + float b1, b2, b3, a1, a2, a3; + float z[3]; +}; +struct NfcFilter4 { + float base_gain, gain; + float b1, b2, b3, b4, a1, a2, a3, a4; + float z[4]; +}; + +class NfcFilter { + NfcFilter1 first; + NfcFilter2 second; + NfcFilter3 third; + NfcFilter4 fourth; + +public: + /* NOTE: + * w0 = speed_of_sound / (source_distance * sample_rate); + * w1 = speed_of_sound / (control_distance * sample_rate); + * + * Generally speaking, the control distance should be approximately the + * average speaker distance, or based on the reference delay if outputing + * NFC-HOA. It must not be negative, 0, or infinite. The source distance + * should not be too small relative to the control distance. + */ + + void init(const float w1) noexcept; + void adjust(const float w0) noexcept; + + /* Near-field control filter for first-order ambisonic channels (1-3). */ + void process1(float *RESTRICT dst, const float *RESTRICT src, const int count); + + /* Near-field control filter for second-order ambisonic channels (4-8). */ + void process2(float *RESTRICT dst, const float *RESTRICT src, const int count); + + /* Near-field control filter for third-order ambisonic channels (9-15). */ + void process3(float *RESTRICT dst, const float *RESTRICT src, const int count); + + /* Near-field control filter for fourth-order ambisonic channels (16-24). */ + void process4(float *RESTRICT dst, const float *RESTRICT src, const int count); +}; + +#endif /* FILTER_NFC_H */ diff --git a/alc/filters/splitter.cpp b/alc/filters/splitter.cpp new file mode 100644 index 00000000..09e7bfe8 --- /dev/null +++ b/alc/filters/splitter.cpp @@ -0,0 +1,115 @@ + +#include "config.h" + +#include "splitter.h" + +#include <cmath> +#include <limits> +#include <algorithm> + +#include "math_defs.h" + +template<typename Real> +void BandSplitterR<Real>::init(Real f0norm) +{ + const Real w{f0norm * al::MathDefs<Real>::Tau()}; + const Real cw{std::cos(w)}; + if(cw > std::numeric_limits<float>::epsilon()) + coeff = (std::sin(w) - 1.0f) / cw; + else + coeff = cw * -0.5f; + + lp_z1 = 0.0f; + lp_z2 = 0.0f; + ap_z1 = 0.0f; +} + +template<typename Real> +void BandSplitterR<Real>::process(Real *hpout, Real *lpout, const Real *input, const int count) +{ + ASSUME(count > 0); + + const Real ap_coeff{this->coeff}; + const Real lp_coeff{this->coeff*0.5f + 0.5f}; + Real lp_z1{this->lp_z1}; + Real lp_z2{this->lp_z2}; + Real ap_z1{this->ap_z1}; + auto proc_sample = [ap_coeff,lp_coeff,&lp_z1,&lp_z2,&ap_z1,&lpout](const Real in) noexcept -> Real + { + /* Low-pass sample processing. */ + Real d{(in - lp_z1) * lp_coeff}; + Real lp_y{lp_z1 + d}; + lp_z1 = lp_y + d; + + d = (lp_y - lp_z2) * lp_coeff; + lp_y = lp_z2 + d; + lp_z2 = lp_y + d; + + *(lpout++) = lp_y; + + /* All-pass sample processing. */ + Real ap_y{in*ap_coeff + ap_z1}; + ap_z1 = in - ap_y*ap_coeff; + + /* High-pass generated from removing low-passed output. */ + return ap_y - lp_y; + }; + std::transform(input, input+count, hpout, proc_sample); + this->lp_z1 = lp_z1; + this->lp_z2 = lp_z2; + this->ap_z1 = ap_z1; +} + +template<typename Real> +void BandSplitterR<Real>::applyHfScale(Real *samples, const Real hfscale, const int count) +{ + ASSUME(count > 0); + + const Real ap_coeff{this->coeff}; + const Real lp_coeff{this->coeff*0.5f + 0.5f}; + Real lp_z1{this->lp_z1}; + Real lp_z2{this->lp_z2}; + Real ap_z1{this->ap_z1}; + auto proc_sample = [hfscale,ap_coeff,lp_coeff,&lp_z1,&lp_z2,&ap_z1](const Real in) noexcept -> Real + { + /* Low-pass sample processing. */ + Real d{(in - lp_z1) * lp_coeff}; + Real lp_y{lp_z1 + d}; + lp_z1 = lp_y + d; + + d = (lp_y - lp_z2) * lp_coeff; + lp_y = lp_z2 + d; + lp_z2 = lp_y + d; + + /* All-pass sample processing. */ + Real ap_y{in*ap_coeff + ap_z1}; + ap_z1 = in - ap_y*ap_coeff; + + /* High-pass generated from removing low-passed output. */ + return (ap_y-lp_y)*hfscale + lp_y; + }; + std::transform(samples, samples+count, samples, proc_sample); + this->lp_z1 = lp_z1; + this->lp_z2 = lp_z2; + this->ap_z1 = ap_z1; +} + +template<typename Real> +void BandSplitterR<Real>::applyAllpass(Real *samples, const int count) const +{ + ASSUME(count > 0); + + const Real coeff{this->coeff}; + Real z1{0.0f}; + auto proc_sample = [coeff,&z1](const Real in) noexcept -> Real + { + const Real out{in*coeff + z1}; + z1 = in - out*coeff; + return out; + }; + std::transform(samples, samples+count, samples, proc_sample); +} + + +template class BandSplitterR<float>; +template class BandSplitterR<double>; diff --git a/alc/filters/splitter.h b/alc/filters/splitter.h new file mode 100644 index 00000000..927c4d17 --- /dev/null +++ b/alc/filters/splitter.h @@ -0,0 +1,50 @@ +#ifndef FILTER_SPLITTER_H +#define FILTER_SPLITTER_H + +#include "alcmain.h" +#include "almalloc.h" + + +/* Band splitter. Splits a signal into two phase-matching frequency bands. */ +template<typename Real> +class BandSplitterR { + Real coeff{0.0f}; + Real lp_z1{0.0f}; + Real lp_z2{0.0f}; + Real ap_z1{0.0f}; + +public: + BandSplitterR() = default; + BandSplitterR(const BandSplitterR&) = default; + BandSplitterR(Real f0norm) { init(f0norm); } + + void init(Real f0norm); + void clear() noexcept { lp_z1 = lp_z2 = ap_z1 = 0.0f; } + void process(Real *hpout, Real *lpout, const Real *input, const int count); + + void applyHfScale(Real *samples, const Real hfscale, const int count); + + /* The all-pass portion of the band splitter. Applies the same phase shift + * without splitting the signal. Note that each use of this method is + * indepedent, it does not track history between calls. + */ + void applyAllpass(Real *samples, const int count) const; +}; +using BandSplitter = BandSplitterR<float>; + + +struct FrontStablizer { + static constexpr size_t DelayLength{256u}; + + alignas(16) float DelayBuf[MAX_OUTPUT_CHANNELS][DelayLength]; + + BandSplitter LFilter, RFilter; + alignas(16) float LSplit[2][BUFFERSIZE]; + alignas(16) float RSplit[2][BUFFERSIZE]; + + alignas(16) float TempBuf[BUFFERSIZE + DelayLength]; + + DEF_NEWDEL(FrontStablizer) +}; + +#endif /* FILTER_SPLITTER_H */ diff --git a/alc/fpu_modes.h b/alc/fpu_modes.h new file mode 100644 index 00000000..5465e9cf --- /dev/null +++ b/alc/fpu_modes.h @@ -0,0 +1,25 @@ +#ifndef FPU_MODES_H +#define FPU_MODES_H + +class FPUCtl { +#if defined(HAVE_SSE_INTRINSICS) || (defined(__GNUC__) && defined(HAVE_SSE)) + unsigned int sse_state{}; +#endif + bool in_mode{}; + +public: + FPUCtl(); + /* HACK: 32-bit targets for GCC seem to have a problem here with certain + * noexcept methods (which destructors are) causing an internal compiler + * error. No idea why it's these methods specifically, but this is needed + * to get it to compile. + */ + ~FPUCtl() noexcept(false) { leave(); } + + FPUCtl(const FPUCtl&) = delete; + FPUCtl& operator=(const FPUCtl&) = delete; + + void leave(); +}; + +#endif /* FPU_MODES_H */ diff --git a/alc/helpers.cpp b/alc/helpers.cpp new file mode 100644 index 00000000..e86af6ce --- /dev/null +++ b/alc/helpers.cpp @@ -0,0 +1,851 @@ +/** + * 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 + */ + +#ifdef _WIN32 +#ifdef __MINGW32__ +#define _WIN32_IE 0x501 +#else +#define _WIN32_IE 0x400 +#endif +#endif + +#include "config.h" + +#include <algorithm> +#include <cerrno> +#include <cstdarg> +#include <cstdlib> +#include <cstdio> +#include <cstring> +#include <mutex> +#include <string> + +#ifdef HAVE_DIRENT_H +#include <dirent.h> +#endif +#ifdef HAVE_PROC_PIDPATH +#include <libproc.h> +#endif + +#ifdef __FreeBSD__ +#include <sys/types.h> +#include <sys/sysctl.h> +#endif + +#ifndef AL_NO_UID_DEFS +#if defined(HAVE_GUIDDEF_H) || defined(HAVE_INITGUID_H) +#define INITGUID +#include <windows.h> +#ifdef HAVE_GUIDDEF_H +#include <guiddef.h> +#else +#include <initguid.h> +#endif + +DEFINE_GUID(KSDATAFORMAT_SUBTYPE_PCM, 0x00000001, 0x0000, 0x0010, 0x80,0x00, 0x00,0xaa,0x00,0x38,0x9b,0x71); +DEFINE_GUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x00000003, 0x0000, 0x0010, 0x80,0x00, 0x00,0xaa,0x00,0x38,0x9b,0x71); + +DEFINE_GUID(IID_IDirectSoundNotify, 0xb0210783, 0x89cd, 0x11d0, 0xaf,0x08, 0x00,0xa0,0xc9,0x25,0xcd,0x16); + +DEFINE_GUID(CLSID_MMDeviceEnumerator, 0xbcde0395, 0xe52f, 0x467c, 0x8e,0x3d, 0xc4,0x57,0x92,0x91,0x69,0x2e); +DEFINE_GUID(IID_IMMDeviceEnumerator, 0xa95664d2, 0x9614, 0x4f35, 0xa7,0x46, 0xde,0x8d,0xb6,0x36,0x17,0xe6); +DEFINE_GUID(IID_IAudioClient, 0x1cb9ad4c, 0xdbfa, 0x4c32, 0xb1,0x78, 0xc2,0xf5,0x68,0xa7,0x03,0xb2); +DEFINE_GUID(IID_IAudioRenderClient, 0xf294acfc, 0x3146, 0x4483, 0xa7,0xbf, 0xad,0xdc,0xa7,0xc2,0x60,0xe2); +DEFINE_GUID(IID_IAudioCaptureClient, 0xc8adbd64, 0xe71e, 0x48a0, 0xa4,0xde, 0x18,0x5c,0x39,0x5c,0xd3,0x17); + +#ifdef HAVE_WASAPI +#include <wtypes.h> +#include <devpropdef.h> +#include <propkeydef.h> +DEFINE_DEVPROPKEY(DEVPKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80,0x20, 0x67,0xd1,0x46,0xa8,0x50,0xe0, 14); +DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_FormFactor, 0x1da5d803, 0xd492, 0x4edd, 0x8c,0x23, 0xe0,0xc0,0xff,0xee,0x7f,0x0e, 0); +DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_GUID, 0x1da5d803, 0xd492, 0x4edd, 0x8c, 0x23,0xe0, 0xc0,0xff,0xee,0x7f,0x0e, 4 ); +#endif +#endif +#endif /* AL_NO_UID_DEFS */ + +#ifdef HAVE_DLFCN_H +#include <dlfcn.h> +#endif +#ifdef HAVE_INTRIN_H +#include <intrin.h> +#endif +#ifdef HAVE_CPUID_H +#include <cpuid.h> +#endif +#ifdef HAVE_SSE_INTRINSICS +#include <xmmintrin.h> +#endif +#ifdef HAVE_SYS_SYSCONF_H +#include <sys/sysconf.h> +#endif + +#ifndef _WIN32 +#include <unistd.h> +#elif defined(_WIN32_IE) +#include <shlobj.h> +#endif + +#include "alcmain.h" +#include "almalloc.h" +#include "compat.h" +#include "cpu_caps.h" +#include "fpu_modes.h" +#include "logging.h" + + +#if defined(HAVE_GCC_GET_CPUID) && (defined(__i386__) || defined(__x86_64__) || \ + defined(_M_IX86) || defined(_M_X64)) +using reg_type = unsigned int; +static inline void get_cpuid(int f, reg_type *regs) +{ __get_cpuid(f, ®s[0], ®s[1], ®s[2], ®s[3]); } +#define CAN_GET_CPUID +#elif defined(HAVE_CPUID_INTRINSIC) && (defined(__i386__) || defined(__x86_64__) || \ + defined(_M_IX86) || defined(_M_X64)) +using reg_type = int; +static inline void get_cpuid(int f, reg_type *regs) +{ (__cpuid)(regs, f); } +#define CAN_GET_CPUID +#endif + +int CPUCapFlags = 0; + +void FillCPUCaps(int capfilter) +{ + int caps = 0; + +/* FIXME: We really should get this for all available CPUs in case different + * CPUs have different caps (is that possible on one machine?). */ +#ifdef CAN_GET_CPUID + union { + reg_type regs[4]; + char str[sizeof(reg_type[4])]; + } cpuinf[3] = {{ { 0, 0, 0, 0 } }}; + + get_cpuid(0, cpuinf[0].regs); + if(cpuinf[0].regs[0] == 0) + ERR("Failed to get CPUID\n"); + else + { + unsigned int maxfunc = cpuinf[0].regs[0]; + unsigned int maxextfunc; + + get_cpuid(0x80000000, cpuinf[0].regs); + maxextfunc = cpuinf[0].regs[0]; + + TRACE("Detected max CPUID function: 0x%x (ext. 0x%x)\n", maxfunc, maxextfunc); + + TRACE("Vendor ID: \"%.4s%.4s%.4s\"\n", cpuinf[0].str+4, cpuinf[0].str+12, cpuinf[0].str+8); + if(maxextfunc >= 0x80000004) + { + get_cpuid(0x80000002, cpuinf[0].regs); + get_cpuid(0x80000003, cpuinf[1].regs); + get_cpuid(0x80000004, cpuinf[2].regs); + TRACE("Name: \"%.16s%.16s%.16s\"\n", cpuinf[0].str, cpuinf[1].str, cpuinf[2].str); + } + + if(maxfunc >= 1) + { + get_cpuid(1, cpuinf[0].regs); + if((cpuinf[0].regs[3]&(1<<25))) + caps |= CPU_CAP_SSE; + if((caps&CPU_CAP_SSE) && (cpuinf[0].regs[3]&(1<<26))) + caps |= CPU_CAP_SSE2; + if((caps&CPU_CAP_SSE2) && (cpuinf[0].regs[2]&(1<<0))) + caps |= CPU_CAP_SSE3; + if((caps&CPU_CAP_SSE3) && (cpuinf[0].regs[2]&(1<<19))) + caps |= CPU_CAP_SSE4_1; + } + } +#else + /* Assume support for whatever's supported if we can't check for it */ +#if defined(HAVE_SSE4_1) +#warning "Assuming SSE 4.1 run-time support!" + caps |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3 | CPU_CAP_SSE4_1; +#elif defined(HAVE_SSE3) +#warning "Assuming SSE 3 run-time support!" + caps |= CPU_CAP_SSE | CPU_CAP_SSE2 | CPU_CAP_SSE3; +#elif defined(HAVE_SSE2) +#warning "Assuming SSE 2 run-time support!" + caps |= CPU_CAP_SSE | CPU_CAP_SSE2; +#elif defined(HAVE_SSE) +#warning "Assuming SSE run-time support!" + caps |= CPU_CAP_SSE; +#endif +#endif +#ifdef HAVE_NEON + al::ifstream file{"/proc/cpuinfo"}; + if(!file.is_open()) + ERR("Failed to open /proc/cpuinfo, cannot check for NEON support\n"); + else + { + std::string features; + + auto getline = [](std::istream &f, std::string &output) -> bool + { + while(f.good() && f.peek() == '\n') + f.ignore(); + return std::getline(f, output) && !output.empty(); + + }; + while(getline(file, features)) + { + if(features.compare(0, 10, "Features\t:", 10) == 0) + break; + } + file.close(); + + size_t extpos{9}; + while((extpos=features.find("neon", extpos+1)) != std::string::npos) + { + if((extpos == 0 || std::isspace(features[extpos-1])) && + (extpos+4 == features.length() || std::isspace(features[extpos+4]))) + { + caps |= CPU_CAP_NEON; + break; + } + } + } +#endif + + TRACE("Extensions:%s%s%s%s%s%s\n", + ((capfilter&CPU_CAP_SSE) ? ((caps&CPU_CAP_SSE) ? " +SSE" : " -SSE") : ""), + ((capfilter&CPU_CAP_SSE2) ? ((caps&CPU_CAP_SSE2) ? " +SSE2" : " -SSE2") : ""), + ((capfilter&CPU_CAP_SSE3) ? ((caps&CPU_CAP_SSE3) ? " +SSE3" : " -SSE3") : ""), + ((capfilter&CPU_CAP_SSE4_1) ? ((caps&CPU_CAP_SSE4_1) ? " +SSE4.1" : " -SSE4.1") : ""), + ((capfilter&CPU_CAP_NEON) ? ((caps&CPU_CAP_NEON) ? " +NEON" : " -NEON") : ""), + ((!capfilter) ? " -none-" : "") + ); + CPUCapFlags = caps & capfilter; +} + + +FPUCtl::FPUCtl() +{ +#if defined(HAVE_SSE_INTRINSICS) + this->sse_state = _mm_getcsr(); + unsigned int sseState = this->sse_state; + sseState |= 0x8000; /* set flush-to-zero */ + sseState |= 0x0040; /* set denormals-are-zero */ + _mm_setcsr(sseState); + +#elif defined(__GNUC__) && defined(HAVE_SSE) + + if((CPUCapFlags&CPU_CAP_SSE)) + { + __asm__ __volatile__("stmxcsr %0" : "=m" (*&this->sse_state)); + unsigned int sseState = this->sse_state; + sseState |= 0x8000; /* set flush-to-zero */ + if((CPUCapFlags&CPU_CAP_SSE2)) + sseState |= 0x0040; /* set denormals-are-zero */ + __asm__ __volatile__("ldmxcsr %0" : : "m" (*&sseState)); + } +#endif + + this->in_mode = true; +} + +void FPUCtl::leave() +{ + if(!this->in_mode) return; + +#if defined(HAVE_SSE_INTRINSICS) + _mm_setcsr(this->sse_state); + +#elif defined(__GNUC__) && defined(HAVE_SSE) + + if((CPUCapFlags&CPU_CAP_SSE)) + __asm__ __volatile__("ldmxcsr %0" : : "m" (*&this->sse_state)); +#endif + this->in_mode = false; +} + + +#ifdef _WIN32 + +namespace al { + +auto filebuf::underflow() -> int_type +{ + if(mFile != INVALID_HANDLE_VALUE && gptr() == egptr()) + { + // Read in the next chunk of data, and set the pointers on success + DWORD got{}; + if(ReadFile(mFile, mBuffer.data(), (DWORD)mBuffer.size(), &got, nullptr)) + setg(mBuffer.data(), mBuffer.data(), mBuffer.data()+got); + } + if(gptr() == egptr()) + return traits_type::eof(); + return traits_type::to_int_type(*gptr()); +} + +auto filebuf::seekoff(off_type offset, std::ios_base::seekdir whence, std::ios_base::openmode mode) -> pos_type +{ + if(mFile == INVALID_HANDLE_VALUE || (mode&std::ios_base::out) || !(mode&std::ios_base::in)) + return traits_type::eof(); + + LARGE_INTEGER fpos{}; + switch(whence) + { + case std::ios_base::beg: + fpos.QuadPart = offset; + if(!SetFilePointerEx(mFile, fpos, &fpos, FILE_BEGIN)) + return traits_type::eof(); + break; + + case std::ios_base::cur: + // If the offset remains in the current buffer range, just + // update the pointer. + if((offset >= 0 && offset < off_type(egptr()-gptr())) || + (offset < 0 && -offset <= off_type(gptr()-eback()))) + { + // Get the current file offset to report the correct read + // offset. + fpos.QuadPart = 0; + if(!SetFilePointerEx(mFile, fpos, &fpos, FILE_CURRENT)) + return traits_type::eof(); + setg(eback(), gptr()+offset, egptr()); + return fpos.QuadPart - off_type(egptr()-gptr()); + } + // Need to offset for the file offset being at egptr() while + // the requested offset is relative to gptr(). + offset -= off_type(egptr()-gptr()); + fpos.QuadPart = offset; + if(!SetFilePointerEx(mFile, fpos, &fpos, FILE_CURRENT)) + return traits_type::eof(); + break; + + case std::ios_base::end: + fpos.QuadPart = offset; + if(!SetFilePointerEx(mFile, fpos, &fpos, FILE_END)) + return traits_type::eof(); + break; + + default: + return traits_type::eof(); + } + setg(nullptr, nullptr, nullptr); + return fpos.QuadPart; +} + +auto filebuf::seekpos(pos_type pos, std::ios_base::openmode mode) -> pos_type +{ + // Simplified version of seekoff + if(mFile == INVALID_HANDLE_VALUE || (mode&std::ios_base::out) || !(mode&std::ios_base::in)) + return traits_type::eof(); + + LARGE_INTEGER fpos{}; + fpos.QuadPart = pos; + if(!SetFilePointerEx(mFile, fpos, &fpos, FILE_BEGIN)) + return traits_type::eof(); + + setg(nullptr, nullptr, nullptr); + return fpos.QuadPart; +} + +filebuf::~filebuf() +{ + if(mFile != INVALID_HANDLE_VALUE) + CloseHandle(mFile); + mFile = INVALID_HANDLE_VALUE; +} + +bool filebuf::open(const wchar_t *filename, std::ios_base::openmode mode) +{ + if((mode&std::ios_base::out) || !(mode&std::ios_base::in)) + return false; + HANDLE f{CreateFileW(filename, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr)}; + if(f == INVALID_HANDLE_VALUE) return false; + + if(mFile != INVALID_HANDLE_VALUE) + CloseHandle(mFile); + mFile = f; + + setg(nullptr, nullptr, nullptr); + return true; +} +bool filebuf::open(const char *filename, std::ios_base::openmode mode) +{ + std::wstring wname{utf8_to_wstr(filename)}; + return open(wname.c_str(), mode); +} + + +ifstream::ifstream(const wchar_t *filename, std::ios_base::openmode mode) + : std::istream{nullptr} +{ + init(&mStreamBuf); + + // Set the failbit if the file failed to open. + if((mode&std::ios_base::out) || !mStreamBuf.open(filename, mode|std::ios_base::in)) + clear(failbit); +} + +ifstream::ifstream(const char *filename, std::ios_base::openmode mode) + : std::istream{nullptr} +{ + init(&mStreamBuf); + + // Set the failbit if the file failed to open. + if((mode&std::ios_base::out) || !mStreamBuf.open(filename, mode|std::ios_base::in)) + clear(failbit); +} + +/* This is only here to ensure the compiler doesn't define an implicit + * destructor, which it tries to automatically inline and subsequently complain + * it can't inline without excessive code growth. + */ +ifstream::~ifstream() { } + +} // namespace al + +const PathNamePair &GetProcBinary() +{ + static PathNamePair ret; + if(!ret.fname.empty() || !ret.path.empty()) + return ret; + + al::vector<WCHAR> fullpath(256); + DWORD len; + while((len=GetModuleFileNameW(nullptr, fullpath.data(), static_cast<DWORD>(fullpath.size()))) == fullpath.size()) + fullpath.resize(fullpath.size() << 1); + if(len == 0) + { + ERR("Failed to get process name: error %lu\n", GetLastError()); + return ret; + } + + fullpath.resize(len); + if(fullpath.back() != 0) + fullpath.push_back(0); + + auto sep = std::find(fullpath.rbegin()+1, fullpath.rend(), '\\'); + sep = std::find(fullpath.rbegin()+1, sep, '/'); + if(sep != fullpath.rend()) + { + *sep = 0; + ret.fname = wstr_to_utf8(&*sep + 1); + ret.path = wstr_to_utf8(fullpath.data()); + } + else + ret.fname = wstr_to_utf8(fullpath.data()); + + TRACE("Got binary: %s, %s\n", ret.path.c_str(), ret.fname.c_str()); + return ret; +} + + +void *LoadLib(const char *name) +{ + std::wstring wname{utf8_to_wstr(name)}; + return LoadLibraryW(wname.c_str()); +} +void CloseLib(void *handle) +{ FreeLibrary(static_cast<HMODULE>(handle)); } +void *GetSymbol(void *handle, const char *name) +{ + void *ret{reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(handle), name))}; + if(!ret) ERR("Failed to load %s\n", name); + return ret; +} + + +void al_print(FILE *logfile, const char *fmt, ...) +{ + al::vector<char> dynmsg; + char stcmsg[256]; + char *str{stcmsg}; + + va_list args, args2; + va_start(args, fmt); + va_copy(args2, args); + int msglen{std::vsnprintf(str, sizeof(stcmsg), fmt, args)}; + if(UNLIKELY(msglen >= 0 && static_cast<size_t>(msglen) >= sizeof(stcmsg))) + { + dynmsg.resize(static_cast<size_t>(msglen) + 1u); + str = dynmsg.data(); + msglen = std::vsnprintf(str, dynmsg.size(), fmt, args2); + } + va_end(args2); + va_end(args); + + std::wstring wstr{utf8_to_wstr(str)}; + fprintf(logfile, "%ls", wstr.c_str()); + fflush(logfile); +} + + +static inline int is_slash(int c) +{ return (c == '\\' || c == '/'); } + +static void DirectorySearch(const char *path, const char *ext, al::vector<std::string> *const results) +{ + std::string pathstr{path}; + pathstr += "\\*"; + pathstr += ext; + TRACE("Searching %s\n", pathstr.c_str()); + + std::wstring wpath{utf8_to_wstr(pathstr.c_str())}; + WIN32_FIND_DATAW fdata; + HANDLE hdl{FindFirstFileW(wpath.c_str(), &fdata)}; + if(hdl != INVALID_HANDLE_VALUE) + { + size_t base = results->size(); + do { + results->emplace_back(); + std::string &str = results->back(); + str = path; + str += '\\'; + str += wstr_to_utf8(fdata.cFileName); + TRACE("Got result %s\n", str.c_str()); + } while(FindNextFileW(hdl, &fdata)); + FindClose(hdl); + + std::sort(results->begin()+base, results->end()); + } +} + +al::vector<std::string> SearchDataFiles(const char *ext, const char *subdir) +{ + static std::mutex search_lock; + std::lock_guard<std::mutex> _{search_lock}; + + /* If the path is absolute, use it directly. */ + al::vector<std::string> results; + if(isalpha(subdir[0]) && subdir[1] == ':' && is_slash(subdir[2])) + { + std::string path{subdir}; + std::replace(path.begin(), path.end(), '/', '\\'); + DirectorySearch(path.c_str(), ext, &results); + return results; + } + if(subdir[0] == '\\' && subdir[1] == '\\' && subdir[2] == '?' && subdir[3] == '\\') + { + DirectorySearch(subdir, ext, &results); + return results; + } + + std::string path; + + /* Search the app-local directory. */ + WCHAR *cwdbuf{_wgetenv(L"ALSOFT_LOCAL_PATH")}; + if(cwdbuf && *cwdbuf != '\0') + { + path = wstr_to_utf8(cwdbuf); + if(is_slash(path.back())) + path.pop_back(); + } + else if(!(cwdbuf=_wgetcwd(nullptr, 0))) + path = "."; + else + { + path = wstr_to_utf8(cwdbuf); + if(is_slash(path.back())) + path.pop_back(); + free(cwdbuf); + } + std::replace(path.begin(), path.end(), '/', '\\'); + DirectorySearch(path.c_str(), ext, &results); + + /* Search the local and global data dirs. */ + static constexpr int ids[2]{ CSIDL_APPDATA, CSIDL_COMMON_APPDATA }; + for(int id : ids) + { + WCHAR buffer[MAX_PATH]; + if(SHGetSpecialFolderPathW(nullptr, buffer, id, FALSE) == FALSE) + continue; + + path = wstr_to_utf8(buffer); + if(!is_slash(path.back())) + path += '\\'; + path += subdir; + std::replace(path.begin(), path.end(), '/', '\\'); + + DirectorySearch(path.c_str(), ext, &results); + } + + return results; +} + +void SetRTPriority(void) +{ + bool failed = false; + if(RTPrioLevel > 0) + failed = !SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); + if(failed) ERR("Failed to set priority level for thread\n"); +} + +#else + +const PathNamePair &GetProcBinary() +{ + static PathNamePair ret; + if(!ret.fname.empty() || !ret.path.empty()) + return ret; + + al::vector<char> pathname; +#ifdef __FreeBSD__ + size_t pathlen; + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 }; + if(sysctl(mib, 4, nullptr, &pathlen, nullptr, 0) == -1) + WARN("Failed to sysctl kern.proc.pathname: %s\n", strerror(errno)); + else + { + pathname.resize(pathlen + 1); + sysctl(mib, 4, pathname.data(), &pathlen, nullptr, 0); + pathname.resize(pathlen); + } +#endif +#ifdef HAVE_PROC_PIDPATH + if(pathname.empty()) + { + char procpath[PROC_PIDPATHINFO_MAXSIZE]{}; + const pid_t pid{getpid()}; + if(proc_pidpath(pid, procpath, sizeof(procpath)) < 1) + ERR("proc_pidpath(%d, ...) failed: %s\n", pid, strerror(errno)); + else + pathname.insert(pathname.end(), procpath, procpath+strlen(procpath)); + } +#endif + if(pathname.empty()) + { + pathname.resize(256); + + const char *selfname{"/proc/self/exe"}; + ssize_t len{readlink(selfname, pathname.data(), pathname.size())}; + if(len == -1 && errno == ENOENT) + { + selfname = "/proc/self/file"; + len = readlink(selfname, pathname.data(), pathname.size()); + } + if(len == -1 && errno == ENOENT) + { + selfname = "/proc/curproc/exe"; + len = readlink(selfname, pathname.data(), pathname.size()); + } + if(len == -1 && errno == ENOENT) + { + selfname = "/proc/curproc/file"; + len = readlink(selfname, pathname.data(), pathname.size()); + } + + while(len > 0 && static_cast<size_t>(len) == pathname.size()) + { + pathname.resize(pathname.size() << 1); + len = readlink(selfname, pathname.data(), pathname.size()); + } + if(len <= 0) + { + WARN("Failed to readlink %s: %s\n", selfname, strerror(errno)); + return ret; + } + + pathname.resize(len); + } + while(!pathname.empty() && pathname.back() == 0) + pathname.pop_back(); + + auto sep = std::find(pathname.crbegin(), pathname.crend(), '/'); + if(sep != pathname.crend()) + { + ret.path = std::string(pathname.cbegin(), sep.base()-1); + ret.fname = std::string(sep.base(), pathname.cend()); + } + else + ret.fname = std::string(pathname.cbegin(), pathname.cend()); + + TRACE("Got binary: %s, %s\n", ret.path.c_str(), ret.fname.c_str()); + return ret; +} + + +#ifdef HAVE_DLFCN_H + +void *LoadLib(const char *name) +{ + dlerror(); + void *handle{dlopen(name, RTLD_NOW)}; + const char *err{dlerror()}; + if(err) handle = nullptr; + return handle; +} +void CloseLib(void *handle) +{ dlclose(handle); } +void *GetSymbol(void *handle, const char *name) +{ + dlerror(); + void *sym{dlsym(handle, name)}; + const char *err{dlerror()}; + if(err) + { + WARN("Failed to load %s: %s\n", name, err); + sym = nullptr; + } + return sym; +} + +#endif /* HAVE_DLFCN_H */ + +void al_print(FILE *logfile, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(logfile, fmt, ap); + va_end(ap); + + fflush(logfile); +} + + +static void DirectorySearch(const char *path, const char *ext, al::vector<std::string> *const results) +{ + TRACE("Searching %s for *%s\n", path, ext); + DIR *dir{opendir(path)}; + if(dir != nullptr) + { + const size_t extlen = strlen(ext); + size_t base = results->size(); + + struct dirent *dirent; + while((dirent=readdir(dir)) != nullptr) + { + if(strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) + continue; + + size_t len{strlen(dirent->d_name)}; + if(len <= extlen) continue; + if(strcasecmp(dirent->d_name+len-extlen, ext) != 0) + continue; + + results->emplace_back(); + std::string &str = results->back(); + str = path; + if(str.back() != '/') + str.push_back('/'); + str += dirent->d_name; + TRACE("Got result %s\n", str.c_str()); + } + closedir(dir); + + std::sort(results->begin()+base, results->end()); + } +} + +al::vector<std::string> SearchDataFiles(const char *ext, const char *subdir) +{ + static std::mutex search_lock; + std::lock_guard<std::mutex> _{search_lock}; + + al::vector<std::string> results; + if(subdir[0] == '/') + { + DirectorySearch(subdir, ext, &results); + return results; + } + + /* Search the app-local directory. */ + const char *str{getenv("ALSOFT_LOCAL_PATH")}; + if(str && *str != '\0') + DirectorySearch(str, ext, &results); + else + { + al::vector<char> cwdbuf(256); + while(!getcwd(cwdbuf.data(), cwdbuf.size())) + { + if(errno != ERANGE) + { + cwdbuf.clear(); + break; + } + cwdbuf.resize(cwdbuf.size() << 1); + } + if(cwdbuf.empty()) + DirectorySearch(".", ext, &results); + else + { + DirectorySearch(cwdbuf.data(), ext, &results); + cwdbuf.clear(); + } + } + + // Search local data dir + if((str=getenv("XDG_DATA_HOME")) != nullptr && str[0] != '\0') + { + std::string path{str}; + if(path.back() != '/') + path += '/'; + path += subdir; + DirectorySearch(path.c_str(), ext, &results); + } + else if((str=getenv("HOME")) != nullptr && str[0] != '\0') + { + std::string path{str}; + if(path.back() == '/') + path.pop_back(); + path += "/.local/share/"; + path += subdir; + DirectorySearch(path.c_str(), ext, &results); + } + + // Search global data dirs + if((str=getenv("XDG_DATA_DIRS")) == nullptr || str[0] == '\0') + str = "/usr/local/share/:/usr/share/"; + + const char *next{str}; + while((str=next) != nullptr && str[0] != '\0') + { + next = strchr(str, ':'); + + std::string path = (next ? std::string(str, next++) : std::string(str)); + if(path.empty()) continue; + + if(path.back() != '/') + path += '/'; + path += subdir; + + DirectorySearch(path.c_str(), ext, &results); + } + + return results; +} + +void SetRTPriority() +{ + bool failed = false; +#if defined(HAVE_PTHREAD_SETSCHEDPARAM) && !defined(__OpenBSD__) + if(RTPrioLevel > 0) + { + struct sched_param param; + /* Use the minimum real-time priority possible for now (on Linux this + * should be 1 for SCHED_RR) */ + param.sched_priority = sched_get_priority_min(SCHED_RR); + failed = !!pthread_setschedparam(pthread_self(), SCHED_RR, ¶m); + } +#else + /* Real-time priority not available */ + failed = (RTPrioLevel>0); +#endif + if(failed) + ERR("Failed to set priority level for thread\n"); +} + +#endif diff --git a/alc/hrtf.cpp b/alc/hrtf.cpp new file mode 100644 index 00000000..786c4c5d --- /dev/null +++ b/alc/hrtf.cpp @@ -0,0 +1,1400 @@ +/** + * 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 "hrtf.h" + +#include <algorithm> +#include <array> +#include <cassert> +#include <cctype> +#include <cstdint> +#include <cstdio> +#include <cstring> +#include <functional> +#include <fstream> +#include <iterator> +#include <memory> +#include <mutex> +#include <new> +#include <numeric> +#include <utility> + +#include "AL/al.h" + +#include "alcmain.h" +#include "alconfig.h" +#include "almalloc.h" +#include "alnumeric.h" +#include "aloptional.h" +#include "alspan.h" +#include "compat.h" +#include "filters/splitter.h" +#include "logging.h" +#include "math_defs.h" +#include "opthelpers.h" + + +struct HrtfHandle { + std::unique_ptr<HrtfEntry> entry; + al::FlexArray<char> filename; + + HrtfHandle(size_t fname_len) : filename{fname_len} { } + HrtfHandle(const HrtfHandle&) = delete; + HrtfHandle& operator=(const HrtfHandle&) = delete; + + static std::unique_ptr<HrtfHandle> Create(size_t fname_len); + static constexpr size_t Sizeof(size_t length) noexcept + { + return maxz(sizeof(HrtfHandle), + al::FlexArray<char>::Sizeof(length, offsetof(HrtfHandle, filename))); + } + + DEF_PLACE_NEWDEL() +}; + +std::unique_ptr<HrtfHandle> HrtfHandle::Create(size_t fname_len) +{ + void *ptr{al_calloc(alignof(HrtfHandle), HrtfHandle::Sizeof(fname_len))}; + return std::unique_ptr<HrtfHandle>{new (ptr) HrtfHandle{fname_len}}; +} + +namespace { + +using namespace std::placeholders; + +using HrtfHandlePtr = std::unique_ptr<HrtfHandle>; + +/* Data set limits must be the same as or more flexible than those defined in + * the makemhr utility. + */ +#define MIN_IR_SIZE (8) +#define MAX_IR_SIZE (512) +#define MOD_IR_SIZE (2) + +#define MIN_FD_COUNT (1) +#define MAX_FD_COUNT (16) + +#define MIN_FD_DISTANCE (0.05f) +#define MAX_FD_DISTANCE (2.5f) + +#define MIN_EV_COUNT (5) +#define MAX_EV_COUNT (128) + +#define MIN_AZ_COUNT (1) +#define MAX_AZ_COUNT (128) + +#define MAX_HRIR_DELAY (HRTF_HISTORY_LENGTH-1) + +constexpr ALchar magicMarker00[8]{'M','i','n','P','H','R','0','0'}; +constexpr ALchar magicMarker01[8]{'M','i','n','P','H','R','0','1'}; +constexpr ALchar magicMarker02[8]{'M','i','n','P','H','R','0','2'}; + +/* First value for pass-through coefficients (remaining are 0), used for omni- + * directional sounds. */ +constexpr ALfloat PassthruCoeff{0.707106781187f/*sqrt(0.5)*/}; + +std::mutex LoadedHrtfLock; +al::vector<HrtfHandlePtr> LoadedHrtfs; + + +class databuf final : public std::streambuf { + int_type underflow() override + { return traits_type::eof(); } + + pos_type seekoff(off_type offset, std::ios_base::seekdir whence, std::ios_base::openmode mode) override + { + if((mode&std::ios_base::out) || !(mode&std::ios_base::in)) + return traits_type::eof(); + + char_type *cur; + switch(whence) + { + case std::ios_base::beg: + if(offset < 0 || offset > egptr()-eback()) + return traits_type::eof(); + cur = eback() + offset; + break; + + case std::ios_base::cur: + if((offset >= 0 && offset > egptr()-gptr()) || + (offset < 0 && -offset > gptr()-eback())) + return traits_type::eof(); + cur = gptr() + offset; + break; + + case std::ios_base::end: + if(offset > 0 || -offset > egptr()-eback()) + return traits_type::eof(); + cur = egptr() + offset; + break; + + default: + return traits_type::eof(); + } + + setg(eback(), cur, egptr()); + return cur - eback(); + } + + pos_type seekpos(pos_type pos, std::ios_base::openmode mode) override + { + // Simplified version of seekoff + if((mode&std::ios_base::out) || !(mode&std::ios_base::in)) + return traits_type::eof(); + + if(pos < 0 || pos > egptr()-eback()) + return traits_type::eof(); + + setg(eback(), eback() + static_cast<size_t>(pos), egptr()); + return pos; + } + +public: + databuf(const char_type *start, const char_type *end) noexcept + { + setg(const_cast<char_type*>(start), const_cast<char_type*>(start), + const_cast<char_type*>(end)); + } +}; + +class idstream final : public std::istream { + databuf mStreamBuf; + +public: + idstream(const char *start, const char *end) + : std::istream{nullptr}, mStreamBuf{start, end} + { init(&mStreamBuf); } +}; + + +struct IdxBlend { ALsizei idx; ALfloat blend; }; +/* Calculate the elevation index given the polar elevation in radians. This + * will return an index between 0 and (evcount - 1). + */ +IdxBlend CalcEvIndex(ALsizei evcount, ALfloat ev) +{ + ev = (al::MathDefs<float>::Pi()*0.5f + ev) * (evcount-1) / al::MathDefs<float>::Pi(); + ALsizei idx{float2int(ev)}; + + return IdxBlend{mini(idx, evcount-1), ev-idx}; +} + +/* Calculate the azimuth index given the polar azimuth in radians. This will + * return an index between 0 and (azcount - 1). + */ +IdxBlend CalcAzIndex(ALsizei azcount, ALfloat az) +{ + az = (al::MathDefs<float>::Tau()+az) * azcount / al::MathDefs<float>::Tau(); + ALsizei idx{float2int(az)}; + + return IdxBlend{idx%azcount, az-idx}; +} + +} // namespace + + +/* Calculates static HRIR coefficients and delays for the given polar elevation + * and azimuth in radians. The coefficients are normalized. + */ +void GetHrtfCoeffs(const HrtfEntry *Hrtf, ALfloat elevation, ALfloat azimuth, ALfloat distance, + ALfloat spread, HrirArray<ALfloat> &coeffs, ALsizei (&delays)[2]) +{ + const ALfloat dirfact{1.0f - (spread / al::MathDefs<float>::Tau())}; + + const auto *field = Hrtf->field; + const auto *field_end = field + Hrtf->fdCount-1; + ALsizei ebase{0}; + while(distance < field->distance && field != field_end) + { + ebase += field->evCount; + ++field; + } + + /* Claculate the elevation indinces. */ + const auto elev0 = CalcEvIndex(field->evCount, elevation); + const ALsizei elev1_idx{mini(elev0.idx+1, field->evCount-1)}; + const ALsizei ir0offset{Hrtf->elev[ebase + elev0.idx].irOffset}; + const ALsizei ir1offset{Hrtf->elev[ebase + elev1_idx].irOffset}; + + /* Calculate azimuth indices. */ + const auto az0 = CalcAzIndex(Hrtf->elev[ebase + elev0.idx].azCount, azimuth); + const auto az1 = CalcAzIndex(Hrtf->elev[ebase + elev1_idx].azCount, azimuth); + + /* Calculate the HRIR indices to blend. */ + ALsizei idx[4]{ + ir0offset + az0.idx, + ir0offset + ((az0.idx+1) % Hrtf->elev[ebase + elev0.idx].azCount), + ir1offset + az1.idx, + ir1offset + ((az1.idx+1) % Hrtf->elev[ebase + elev1_idx].azCount) + }; + + /* Calculate bilinear blending weights, attenuated according to the + * directional panning factor. + */ + const ALfloat blend[4]{ + (1.0f-elev0.blend) * (1.0f-az0.blend) * dirfact, + (1.0f-elev0.blend) * ( az0.blend) * dirfact, + ( elev0.blend) * (1.0f-az1.blend) * dirfact, + ( elev0.blend) * ( az1.blend) * dirfact + }; + + /* Calculate the blended HRIR delays. */ + delays[0] = fastf2i( + Hrtf->delays[idx[0]][0]*blend[0] + Hrtf->delays[idx[1]][0]*blend[1] + + Hrtf->delays[idx[2]][0]*blend[2] + Hrtf->delays[idx[3]][0]*blend[3] + ); + delays[1] = fastf2i( + Hrtf->delays[idx[0]][1]*blend[0] + Hrtf->delays[idx[1]][1]*blend[1] + + Hrtf->delays[idx[2]][1]*blend[2] + Hrtf->delays[idx[3]][1]*blend[3] + ); + + const ALsizei irSize{Hrtf->irSize}; + ASSUME(irSize >= MIN_IR_SIZE); + + /* Calculate the sample offsets for the HRIR indices. */ + idx[0] *= irSize; + idx[1] *= irSize; + idx[2] *= irSize; + idx[3] *= irSize; + + /* Calculate the blended HRIR coefficients. */ + ALfloat *coeffout{al::assume_aligned<16>(&coeffs[0][0])}; + coeffout[0] = PassthruCoeff * (1.0f-dirfact); + coeffout[1] = PassthruCoeff * (1.0f-dirfact); + std::fill(coeffout+2, coeffout + irSize*2, 0.0f); + for(ALsizei c{0};c < 4;c++) + { + const ALfloat *srccoeffs{al::assume_aligned<16>(Hrtf->coeffs[idx[c]])}; + const ALfloat mult{blend[c]}; + auto blend_coeffs = [mult](const ALfloat src, const ALfloat coeff) noexcept -> ALfloat + { return src*mult + coeff; }; + std::transform(srccoeffs, srccoeffs + irSize*2, coeffout, coeffout, blend_coeffs); + } +} + + +std::unique_ptr<DirectHrtfState> DirectHrtfState::Create(size_t num_chans) +{ + void *ptr{al_calloc(16, DirectHrtfState::Sizeof(num_chans))}; + return std::unique_ptr<DirectHrtfState>{new (ptr) DirectHrtfState{num_chans}}; +} + +void BuildBFormatHrtf(const HrtfEntry *Hrtf, DirectHrtfState *state, const ALuint NumChannels, + const AngularPoint *AmbiPoints, const ALfloat (*RESTRICT AmbiMatrix)[MAX_AMBI_CHANNELS], + const size_t AmbiCount, const ALfloat *RESTRICT AmbiOrderHFGain) +{ + static constexpr int OrderFromChan[MAX_AMBI_CHANNELS]{ + 0, 1,1,1, 2,2,2,2,2, 3,3,3,3,3,3,3, + }; + /* Set this to true for dual-band HRTF processing. May require better + * calculation of the new IR length to deal with the head and tail + * generated by the HF scaling. + */ + static constexpr bool DualBand{true}; + + ASSUME(NumChannels > 0); + ASSUME(AmbiCount > 0); + + auto &field = Hrtf->field[0]; + ALsizei min_delay{HRTF_HISTORY_LENGTH}; + ALsizei max_delay{0}; + auto idx = al::vector<ALsizei>(AmbiCount); + auto calc_idxs = [Hrtf,&field,&max_delay,&min_delay](const AngularPoint &pt) noexcept -> ALsizei + { + /* Calculate elevation index. */ + const auto evidx = clampi( + static_cast<ALsizei>((90.0f+pt.Elev)*(field.evCount-1)/180.0f + 0.5f), + 0, field.evCount-1); + + const ALsizei azcount{Hrtf->elev[evidx].azCount}; + const ALsizei iroffset{Hrtf->elev[evidx].irOffset}; + + /* Calculate azimuth index for this elevation. */ + const auto azidx = static_cast<ALsizei>((360.0f+pt.Azim)*azcount/360.0f + 0.5f) % azcount; + + /* Calculate the index for the impulse response. */ + ALsizei idx{iroffset + azidx}; + + min_delay = mini(min_delay, mini(Hrtf->delays[idx][0], Hrtf->delays[idx][1])); + max_delay = maxi(max_delay, maxi(Hrtf->delays[idx][0], Hrtf->delays[idx][1])); + + return idx; + }; + std::transform(AmbiPoints, AmbiPoints+AmbiCount, idx.begin(), calc_idxs); + + /* For dual-band processing, add a 16-sample delay to compensate for the HF + * scale on the minimum-phase response. + */ + static constexpr ALsizei base_delay{DualBand ? 16 : 0}; + const ALdouble xover_norm{400.0 / Hrtf->sampleRate}; + BandSplitterR<double> splitter{xover_norm}; + + auto tmpres = al::vector<HrirArray<ALdouble>>(NumChannels); + auto tmpfilt = al::vector<std::array<ALdouble,HRIR_LENGTH*4>>(3); + for(size_t c{0u};c < AmbiCount;++c) + { + const ALfloat (*fir)[2]{&Hrtf->coeffs[idx[c] * Hrtf->irSize]}; + const ALsizei ldelay{Hrtf->delays[idx[c]][0] - min_delay + base_delay}; + const ALsizei rdelay{Hrtf->delays[idx[c]][1] - min_delay + base_delay}; + + if(!DualBand) + { + /* For single-band decoding, apply the HF scale to the response. */ + for(ALuint i{0u};i < NumChannels;++i) + { + const ALdouble mult{ALdouble{AmbiOrderHFGain[OrderFromChan[i]]} * + AmbiMatrix[c][i]}; + const ALsizei numirs{mini(Hrtf->irSize, HRIR_LENGTH-maxi(ldelay, rdelay))}; + ALsizei lidx{ldelay}, ridx{rdelay}; + for(ALsizei j{0};j < numirs;++j) + { + tmpres[i][lidx++][0] += fir[j][0] * mult; + tmpres[i][ridx++][1] += fir[j][1] * mult; + } + } + continue; + } + + /* For dual-band processing, the HRIR needs to be split into low and + * high frequency responses. The band-splitter alone creates frequency- + * dependent phase-shifts, which is not ideal. To counteract it, + * combine it with a backwards phase-shift. + */ + + /* Load the (left) HRIR backwards, into a temp buffer with padding. */ + std::fill(tmpfilt[2].begin(), tmpfilt[2].end(), 0.0); + std::transform(fir, fir+Hrtf->irSize, tmpfilt[2].rbegin() + HRIR_LENGTH*3, + [](const ALfloat (&ir)[2]) noexcept -> ALdouble { return ir[0]; }); + + /* Apply the all-pass on the reversed signal and reverse the resulting + * sample array. This produces the forward response with a backwards + * phase-shift (+n degrees becomes -n degrees). + */ + splitter.applyAllpass(tmpfilt[2].data(), static_cast<int>(tmpfilt[2].size())); + std::reverse(tmpfilt[2].begin(), tmpfilt[2].end()); + + /* Now apply the band-splitter. This applies the normal phase-shift, + * which cancels out with the backwards phase-shift to get the original + * phase on the split signal. + */ + splitter.clear(); + splitter.process(tmpfilt[0].data(), tmpfilt[1].data(), tmpfilt[2].data(), + static_cast<int>(tmpfilt[2].size())); + + /* Apply left ear response with delay and HF scale. */ + for(ALuint i{0u};i < NumChannels;++i) + { + const ALdouble mult{AmbiMatrix[c][i]}; + const ALdouble hfgain{AmbiOrderHFGain[OrderFromChan[i]]}; + ALsizei j{HRIR_LENGTH*3 - ldelay}; + for(ALsizei lidx{0};lidx < HRIR_LENGTH;++lidx,++j) + tmpres[i][lidx][0] += (tmpfilt[0][j]*hfgain + tmpfilt[1][j]) * mult; + } + + /* Now run the same process on the right HRIR. */ + std::fill(tmpfilt[2].begin(), tmpfilt[2].end(), 0.0); + std::transform(fir, fir+Hrtf->irSize, tmpfilt[2].rbegin() + HRIR_LENGTH*3, + [](const ALfloat (&ir)[2]) noexcept -> ALdouble { return ir[1]; }); + + splitter.applyAllpass(tmpfilt[2].data(), static_cast<int>(tmpfilt[2].size())); + std::reverse(tmpfilt[2].begin(), tmpfilt[2].end()); + + splitter.clear(); + splitter.process(tmpfilt[0].data(), tmpfilt[1].data(), tmpfilt[2].data(), + static_cast<int>(tmpfilt[2].size())); + + for(ALuint i{0u};i < NumChannels;++i) + { + const ALdouble mult{AmbiMatrix[c][i]}; + const ALdouble hfgain{AmbiOrderHFGain[OrderFromChan[i]]}; + ALsizei j{HRIR_LENGTH*3 - rdelay}; + for(ALsizei ridx{0};ridx < HRIR_LENGTH;++ridx,++j) + tmpres[i][ridx][1] += (tmpfilt[0][j]*hfgain + tmpfilt[1][j]) * mult; + } + } + tmpfilt.clear(); + idx.clear(); + + for(ALuint i{0u};i < NumChannels;++i) + { + auto copy_arr = [](const std::array<double,2> &in) noexcept -> std::array<float,2> + { return std::array<float,2>{{static_cast<float>(in[0]), static_cast<float>(in[1])}}; }; + std::transform(tmpres[i].begin(), tmpres[i].end(), state->Chan[i].Coeffs.begin(), + copy_arr); + } + tmpres.clear(); + + ALsizei max_length{HRIR_LENGTH}; + /* Increase the IR size by double the base delay with dual-band processing + * to account for the head and tail from the HF response scale. + */ + const ALsizei irsize{mini(Hrtf->irSize + base_delay*2, max_length)}; + max_length = mini(max_delay-min_delay + irsize, max_length); + + /* Round up to the next IR size multiple. */ + max_length += MOD_IR_SIZE-1; + max_length -= max_length%MOD_IR_SIZE; + + TRACE("Skipped delay: %d, max delay: %d, new FIR length: %d\n", + min_delay, max_delay-min_delay, max_length); + state->IrSize = max_length; +} + + +namespace { + +std::unique_ptr<HrtfEntry> CreateHrtfStore(ALuint rate, ALsizei irSize, const ALsizei fdCount, + const ALubyte *evCount, const ALfloat *distance, const ALushort *azCount, + const ALushort *irOffset, ALsizei irCount, const ALfloat (*coeffs)[2], + const ALubyte (*delays)[2], const char *filename) +{ + std::unique_ptr<HrtfEntry> Hrtf; + + ALsizei evTotal{std::accumulate(evCount, evCount+fdCount, 0)}; + size_t total{sizeof(HrtfEntry)}; + total = RoundUp(total, alignof(HrtfEntry::Field)); /* Align for field infos */ + total += sizeof(HrtfEntry::Field)*fdCount; + total = RoundUp(total, alignof(HrtfEntry::Elevation)); /* Align for elevation infos */ + total += sizeof(Hrtf->elev[0])*evTotal; + total = RoundUp(total, 16); /* Align for coefficients using SIMD */ + total += sizeof(Hrtf->coeffs[0])*irSize*irCount; + total += sizeof(Hrtf->delays[0])*irCount; + + Hrtf.reset(new (al_calloc(16, total)) HrtfEntry{}); + if(!Hrtf) + ERR("Out of memory allocating storage for %s.\n", filename); + else + { + InitRef(&Hrtf->ref, 1u); + Hrtf->sampleRate = rate; + Hrtf->irSize = irSize; + Hrtf->fdCount = fdCount; + + /* Set up pointers to storage following the main HRTF struct. */ + char *base = reinterpret_cast<char*>(Hrtf.get()); + uintptr_t offset = sizeof(HrtfEntry); + + offset = RoundUp(offset, alignof(HrtfEntry::Field)); /* Align for field infos */ + auto field_ = reinterpret_cast<HrtfEntry::Field*>(base + offset); + offset += sizeof(field_[0])*fdCount; + + offset = RoundUp(offset, alignof(HrtfEntry::Elevation)); /* Align for elevation infos */ + auto elev_ = reinterpret_cast<HrtfEntry::Elevation*>(base + offset); + offset += sizeof(elev_[0])*evTotal; + + offset = RoundUp(offset, 16); /* Align for coefficients using SIMD */ + auto coeffs_ = reinterpret_cast<ALfloat(*)[2]>(base + offset); + offset += sizeof(coeffs_[0])*irSize*irCount; + + auto delays_ = reinterpret_cast<ALubyte(*)[2]>(base + offset); + offset += sizeof(delays_[0])*irCount; + + assert(offset == total); + + /* Copy input data to storage. */ + for(ALsizei i{0};i < fdCount;i++) + { + field_[i].distance = distance[i]; + field_[i].evCount = evCount[i]; + } + for(ALsizei i{0};i < evTotal;i++) + { + elev_[i].azCount = azCount[i]; + elev_[i].irOffset = irOffset[i]; + } + for(ALsizei i{0};i < irSize*irCount;i++) + { + coeffs_[i][0] = coeffs[i][0]; + coeffs_[i][1] = coeffs[i][1]; + } + for(ALsizei i{0};i < irCount;i++) + { + delays_[i][0] = delays[i][0]; + delays_[i][1] = delays[i][1]; + } + + /* Finally, assign the storage pointers. */ + Hrtf->field = field_; + Hrtf->elev = elev_; + Hrtf->coeffs = coeffs_; + Hrtf->delays = delays_; + } + + return Hrtf; +} + +ALubyte GetLE_ALubyte(std::istream &data) +{ + return static_cast<ALubyte>(data.get()); +} + +ALshort GetLE_ALshort(std::istream &data) +{ + int ret = data.get(); + ret |= data.get() << 8; + return static_cast<ALshort>((ret^32768) - 32768); +} + +ALushort GetLE_ALushort(std::istream &data) +{ + int ret = data.get(); + ret |= data.get() << 8; + return static_cast<ALushort>(ret); +} + +ALint GetLE_ALint24(std::istream &data) +{ + int ret = data.get(); + ret |= data.get() << 8; + ret |= data.get() << 16; + return (ret^8388608) - 8388608; +} + +ALuint GetLE_ALuint(std::istream &data) +{ + int ret = data.get(); + ret |= data.get() << 8; + ret |= data.get() << 16; + ret |= data.get() << 24; + return ret; +} + +std::unique_ptr<HrtfEntry> LoadHrtf00(std::istream &data, const char *filename) +{ + ALuint rate{GetLE_ALuint(data)}; + ALushort irCount{GetLE_ALushort(data)}; + ALushort irSize{GetLE_ALushort(data)}; + ALubyte evCount{GetLE_ALubyte(data)}; + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + + ALboolean failed{AL_FALSE}; + if(irSize < MIN_IR_SIZE || irSize > MAX_IR_SIZE || (irSize%MOD_IR_SIZE)) + { + ERR("Unsupported HRIR size: irSize=%d (%d to %d by %d)\n", + irSize, MIN_IR_SIZE, MAX_IR_SIZE, MOD_IR_SIZE); + failed = AL_TRUE; + } + if(evCount < MIN_EV_COUNT || evCount > MAX_EV_COUNT) + { + ERR("Unsupported elevation count: evCount=%d (%d to %d)\n", + evCount, MIN_EV_COUNT, MAX_EV_COUNT); + failed = AL_TRUE; + } + if(failed) + return nullptr; + + al::vector<ALushort> evOffset(evCount); + for(auto &val : evOffset) + val = GetLE_ALushort(data); + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + for(ALsizei i{1};i < evCount;i++) + { + if(evOffset[i] <= evOffset[i-1]) + { + ERR("Invalid evOffset: evOffset[%d]=%d (last=%d)\n", + i, evOffset[i], evOffset[i-1]); + failed = AL_TRUE; + } + } + if(irCount <= evOffset.back()) + { + ERR("Invalid evOffset: evOffset[%zu]=%d (irCount=%d)\n", + evOffset.size()-1, evOffset.back(), irCount); + failed = AL_TRUE; + } + if(failed) + return nullptr; + + al::vector<ALushort> azCount(evCount); + for(ALsizei i{1};i < evCount;i++) + { + azCount[i-1] = evOffset[i] - evOffset[i-1]; + if(azCount[i-1] < MIN_AZ_COUNT || azCount[i-1] > MAX_AZ_COUNT) + { + ERR("Unsupported azimuth count: azCount[%d]=%d (%d to %d)\n", + i-1, azCount[i-1], MIN_AZ_COUNT, MAX_AZ_COUNT); + failed = AL_TRUE; + } + } + azCount.back() = irCount - evOffset.back(); + if(azCount.back() < MIN_AZ_COUNT || azCount.back() > MAX_AZ_COUNT) + { + ERR("Unsupported azimuth count: azCount[%zu]=%d (%d to %d)\n", + azCount.size()-1, azCount.back(), MIN_AZ_COUNT, MAX_AZ_COUNT); + failed = AL_TRUE; + } + if(failed) + return nullptr; + + al::vector<std::array<ALfloat,2>> coeffs(irSize*irCount); + al::vector<std::array<ALubyte,2>> delays(irCount); + for(auto &val : coeffs) + val[0] = GetLE_ALshort(data) / 32768.0f; + for(auto &val : delays) + val[0] = GetLE_ALubyte(data); + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + for(ALsizei i{0};i < irCount;i++) + { + if(delays[i][0] > MAX_HRIR_DELAY) + { + ERR("Invalid delays[%d]: %d (%d)\n", i, delays[i][0], MAX_HRIR_DELAY); + failed = AL_TRUE; + } + } + if(failed) + return nullptr; + + /* Mirror the left ear responses to the right ear. */ + for(ALsizei i{0};i < evCount;i++) + { + const ALushort evoffset{evOffset[i]}; + const ALushort azcount{azCount[i]}; + for(ALsizei j{0};j < azcount;j++) + { + const ALsizei lidx{evoffset + j}; + const ALsizei ridx{evoffset + ((azcount-j) % azcount)}; + + for(ALsizei k{0};k < irSize;k++) + coeffs[ridx*irSize + k][1] = coeffs[lidx*irSize + k][0]; + delays[ridx][1] = delays[lidx][0]; + } + } + + static constexpr ALfloat distance{0.0f}; + return CreateHrtfStore(rate, irSize, 1, &evCount, &distance, azCount.data(), evOffset.data(), + irCount, &reinterpret_cast<ALfloat(&)[2]>(coeffs[0]), + &reinterpret_cast<ALubyte(&)[2]>(delays[0]), filename); +} + +std::unique_ptr<HrtfEntry> LoadHrtf01(std::istream &data, const char *filename) +{ + ALuint rate{GetLE_ALuint(data)}; + ALushort irSize{GetLE_ALubyte(data)}; + ALubyte evCount{GetLE_ALubyte(data)}; + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + + ALboolean failed{AL_FALSE}; + if(irSize < MIN_IR_SIZE || irSize > MAX_IR_SIZE || (irSize%MOD_IR_SIZE)) + { + ERR("Unsupported HRIR size: irSize=%d (%d to %d by %d)\n", + irSize, MIN_IR_SIZE, MAX_IR_SIZE, MOD_IR_SIZE); + failed = AL_TRUE; + } + if(evCount < MIN_EV_COUNT || evCount > MAX_EV_COUNT) + { + ERR("Unsupported elevation count: evCount=%d (%d to %d)\n", + evCount, MIN_EV_COUNT, MAX_EV_COUNT); + failed = AL_TRUE; + } + if(failed) + return nullptr; + + al::vector<ALushort> azCount(evCount); + std::generate(azCount.begin(), azCount.end(), std::bind(GetLE_ALubyte, std::ref(data))); + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + for(ALsizei i{0};i < evCount;++i) + { + if(azCount[i] < MIN_AZ_COUNT || azCount[i] > MAX_AZ_COUNT) + { + ERR("Unsupported azimuth count: azCount[%d]=%d (%d to %d)\n", + i, azCount[i], MIN_AZ_COUNT, MAX_AZ_COUNT); + failed = AL_TRUE; + } + } + if(failed) + return nullptr; + + al::vector<ALushort> evOffset(evCount); + evOffset[0] = 0; + ALushort irCount{azCount[0]}; + for(ALsizei i{1};i < evCount;i++) + { + evOffset[i] = evOffset[i-1] + azCount[i-1]; + irCount += azCount[i]; + } + + al::vector<std::array<ALfloat,2>> coeffs(irSize*irCount); + al::vector<std::array<ALubyte,2>> delays(irCount); + for(auto &val : coeffs) + val[0] = GetLE_ALshort(data) / 32768.0f; + for(auto &val : delays) + val[0] = GetLE_ALubyte(data); + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + for(ALsizei i{0};i < irCount;i++) + { + if(delays[i][0] > MAX_HRIR_DELAY) + { + ERR("Invalid delays[%d]: %d (%d)\n", i, delays[i][0], MAX_HRIR_DELAY); + failed = AL_TRUE; + } + } + if(failed) + return nullptr; + + /* Mirror the left ear responses to the right ear. */ + for(ALsizei i{0};i < evCount;i++) + { + const ALushort evoffset{evOffset[i]}; + const ALushort azcount{azCount[i]}; + for(ALsizei j{0};j < azcount;j++) + { + const ALsizei lidx{evoffset + j}; + const ALsizei ridx{evoffset + ((azcount-j) % azcount)}; + + for(ALsizei k{0};k < irSize;k++) + coeffs[ridx*irSize + k][1] = coeffs[lidx*irSize + k][0]; + delays[ridx][1] = delays[lidx][0]; + } + } + + static constexpr ALfloat distance{0.0f}; + return CreateHrtfStore(rate, irSize, 1, &evCount, &distance, azCount.data(), evOffset.data(), + irCount, &reinterpret_cast<ALfloat(&)[2]>(coeffs[0]), + &reinterpret_cast<ALubyte(&)[2]>(delays[0]), filename); +} + +#define SAMPLETYPE_S16 0 +#define SAMPLETYPE_S24 1 + +#define CHANTYPE_LEFTONLY 0 +#define CHANTYPE_LEFTRIGHT 1 + +std::unique_ptr<HrtfEntry> LoadHrtf02(std::istream &data, const char *filename) +{ + ALuint rate{GetLE_ALuint(data)}; + ALubyte sampleType{GetLE_ALubyte(data)}; + ALubyte channelType{GetLE_ALubyte(data)}; + ALushort irSize{GetLE_ALubyte(data)}; + ALubyte fdCount{GetLE_ALubyte(data)}; + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + + ALboolean failed{AL_FALSE}; + if(sampleType > SAMPLETYPE_S24) + { + ERR("Unsupported sample type: %d\n", sampleType); + failed = AL_TRUE; + } + if(channelType > CHANTYPE_LEFTRIGHT) + { + ERR("Unsupported channel type: %d\n", channelType); + failed = AL_TRUE; + } + + if(irSize < MIN_IR_SIZE || irSize > MAX_IR_SIZE || (irSize%MOD_IR_SIZE)) + { + ERR("Unsupported HRIR size: irSize=%d (%d to %d by %d)\n", + irSize, MIN_IR_SIZE, MAX_IR_SIZE, MOD_IR_SIZE); + failed = AL_TRUE; + } + if(fdCount < 1 || fdCount > MAX_FD_COUNT) + { + ERR("Multiple field-depths not supported: fdCount=%d (%d to %d)\n", + fdCount, MIN_FD_COUNT, MAX_FD_COUNT); + failed = AL_TRUE; + } + if(failed) + return nullptr; + + al::vector<ALfloat> distance(fdCount); + al::vector<ALubyte> evCount(fdCount); + al::vector<ALushort> azCount; + for(ALsizei f{0};f < fdCount;f++) + { + distance[f] = GetLE_ALushort(data) / 1000.0f; + evCount[f] = GetLE_ALubyte(data); + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + + if(distance[f] < MIN_FD_DISTANCE || distance[f] > MAX_FD_DISTANCE) + { + ERR("Unsupported field distance[%d]=%f (%f to %f meters)\n", f, + distance[f], MIN_FD_DISTANCE, MAX_FD_DISTANCE); + failed = AL_TRUE; + } + if(f > 0 && distance[f] <= distance[f-1]) + { + ERR("Field distance[%d] is not after previous (%f > %f)\n", f, distance[f], + distance[f-1]); + failed = AL_TRUE; + } + if(evCount[f] < MIN_EV_COUNT || evCount[f] > MAX_EV_COUNT) + { + ERR("Unsupported elevation count: evCount[%d]=%d (%d to %d)\n", f, + evCount[f], MIN_EV_COUNT, MAX_EV_COUNT); + failed = AL_TRUE; + } + if(failed) + return nullptr; + + size_t ebase{azCount.size()}; + azCount.resize(ebase + evCount[f]); + std::generate(azCount.begin()+ebase, azCount.end(), + std::bind(GetLE_ALubyte, std::ref(data))); + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + + for(ALsizei e{0};e < evCount[f];e++) + { + if(azCount[ebase+e] < MIN_AZ_COUNT || azCount[ebase+e] > MAX_AZ_COUNT) + { + ERR("Unsupported azimuth count: azCount[%d][%d]=%d (%d to %d)\n", f, e, + azCount[ebase+e], MIN_AZ_COUNT, MAX_AZ_COUNT); + failed = AL_TRUE; + } + } + if(failed) + return nullptr; + } + + al::vector<ALushort> evOffset(azCount.size()); + evOffset[0] = 0; + std::partial_sum(azCount.cbegin(), azCount.cend()-1, evOffset.begin()+1); + const ALsizei irTotal{evOffset.back() + azCount.back()}; + + al::vector<std::array<ALfloat,2>> coeffs(irSize*irTotal); + al::vector<std::array<ALubyte,2>> delays(irTotal); + if(channelType == CHANTYPE_LEFTONLY) + { + if(sampleType == SAMPLETYPE_S16) + { + for(auto &val : coeffs) + val[0] = GetLE_ALshort(data) / 32768.0f; + } + else if(sampleType == SAMPLETYPE_S24) + { + for(auto &val : coeffs) + val[0] = GetLE_ALint24(data) / 8388608.0f; + } + for(auto &val : delays) + val[0] = GetLE_ALubyte(data); + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + for(ALsizei i{0};i < irTotal;++i) + { + if(delays[i][0] > MAX_HRIR_DELAY) + { + ERR("Invalid delays[%d][0]: %d (%d)\n", i, delays[i][0], MAX_HRIR_DELAY); + failed = AL_TRUE; + } + } + } + else if(channelType == CHANTYPE_LEFTRIGHT) + { + if(sampleType == SAMPLETYPE_S16) + { + for(auto &val : coeffs) + { + val[0] = GetLE_ALshort(data) / 32768.0f; + val[1] = GetLE_ALshort(data) / 32768.0f; + } + } + else if(sampleType == SAMPLETYPE_S24) + { + for(auto &val : coeffs) + { + val[0] = GetLE_ALint24(data) / 8388608.0f; + val[1] = GetLE_ALint24(data) / 8388608.0f; + } + } + for(auto &val : delays) + { + val[0] = GetLE_ALubyte(data); + val[1] = GetLE_ALubyte(data); + } + if(!data || data.eof()) + { + ERR("Failed reading %s\n", filename); + return nullptr; + } + + for(ALsizei i{0};i < irTotal;++i) + { + if(delays[i][0] > MAX_HRIR_DELAY) + { + ERR("Invalid delays[%d][0]: %d (%d)\n", i, delays[i][0], MAX_HRIR_DELAY); + failed = AL_TRUE; + } + if(delays[i][1] > MAX_HRIR_DELAY) + { + ERR("Invalid delays[%d][1]: %d (%d)\n", i, delays[i][1], MAX_HRIR_DELAY); + failed = AL_TRUE; + } + } + } + if(failed) + return nullptr; + + if(channelType == CHANTYPE_LEFTONLY) + { + /* Mirror the left ear responses to the right ear. */ + ALsizei ebase{0}; + for(ALsizei f{0};f < fdCount;f++) + { + for(ALsizei e{0};e < evCount[f];e++) + { + const ALushort evoffset{evOffset[ebase+e]}; + const ALushort azcount{azCount[ebase+e]}; + for(ALsizei a{0};a < azcount;a++) + { + const ALsizei lidx{evoffset + a}; + const ALsizei ridx{evoffset + ((azcount-a) % azcount)}; + + for(ALsizei k{0};k < irSize;k++) + coeffs[ridx*irSize + k][1] = coeffs[lidx*irSize + k][0]; + delays[ridx][1] = delays[lidx][0]; + } + } + ebase += evCount[f]; + } + } + + if(fdCount > 1) + { + auto distance_ = al::vector<ALfloat>(distance.size()); + auto evCount_ = al::vector<ALubyte>(evCount.size()); + auto azCount_ = al::vector<ALushort>(azCount.size()); + auto evOffset_ = al::vector<ALushort>(evOffset.size()); + auto coeffs_ = al::vector<float2>(coeffs.size()); + auto delays_ = al::vector<std::array<ALubyte,2>>(delays.size()); + + /* Simple reverse for the per-field elements. */ + std::reverse_copy(distance.cbegin(), distance.cend(), distance_.begin()); + std::reverse_copy(evCount.cbegin(), evCount.cend(), evCount_.begin()); + + /* Each field has a group of elevations, which each have an azimuth + * count. Reverse the order of the groups, keeping the relative order + * of per-group azimuth counts. + */ + auto azcnt_end = azCount_.end(); + auto copy_azs = [&azCount,&azcnt_end](const size_t ebase, const ALubyte num_evs) -> size_t + { + auto azcnt_src = azCount.begin()+ebase; + azcnt_end = std::copy_backward(azcnt_src, azcnt_src+num_evs, azcnt_end); + return ebase + num_evs; + }; + std::accumulate(evCount.cbegin(), evCount.cend(), size_t{0u}, copy_azs); + assert(azCount_.begin() == azcnt_end); + + /* Reestablish the IR offset for each elevation index, given the new + * ordering of elevations. + */ + evOffset_[0] = 0; + std::partial_sum(azCount_.cbegin(), azCount_.cend()-1, evOffset_.begin()+1); + + /* Reverse the order of each field's group of IRs. */ + auto coeffs_end = coeffs_.end(); + auto delays_end = delays_.end(); + auto copy_irs = [irSize,&azCount,&coeffs,&delays,&coeffs_end,&delays_end](const size_t ebase, const ALubyte num_evs) -> size_t + { + const ALsizei abase{std::accumulate(azCount.cbegin(), azCount.cbegin()+ebase, 0)}; + const ALsizei num_azs{std::accumulate(azCount.cbegin()+ebase, + azCount.cbegin() + (ebase+num_evs), 0)}; + + coeffs_end = std::copy_backward(coeffs.cbegin() + abase*irSize, + coeffs.cbegin() + (abase+num_azs)*irSize, coeffs_end); + delays_end = std::copy_backward(delays.cbegin() + abase, + delays.cbegin() + (abase+num_azs), delays_end); + + return ebase + num_evs; + }; + std::accumulate(evCount.cbegin(), evCount.cend(), size_t{0u}, copy_irs); + assert(coeffs_.begin() == coeffs_end); + assert(delays_.begin() == delays_end); + + distance = std::move(distance_); + evCount = std::move(evCount_); + azCount = std::move(azCount_); + evOffset = std::move(evOffset_); + coeffs = std::move(coeffs_); + delays = std::move(delays_); + } + + return CreateHrtfStore(rate, irSize, fdCount, evCount.data(), distance.data(), azCount.data(), + evOffset.data(), irTotal, &reinterpret_cast<ALfloat(&)[2]>(coeffs[0]), + &reinterpret_cast<ALubyte(&)[2]>(delays[0]), filename); +} + + +bool checkName(al::vector<EnumeratedHrtf> &list, const std::string &name) +{ + return std::find_if(list.cbegin(), list.cend(), + [&name](const EnumeratedHrtf &entry) + { return name == entry.name; } + ) != list.cend(); +} + +void AddFileEntry(al::vector<EnumeratedHrtf> &list, const std::string &filename) +{ + /* Check if this file has already been loaded globally. */ + auto loaded_entry = LoadedHrtfs.begin(); + for(;loaded_entry != LoadedHrtfs.end();++loaded_entry) + { + if(filename != (*loaded_entry)->filename.data()) + continue; + + /* Check if this entry has already been added to the list. */ + auto iter = std::find_if(list.cbegin(), list.cend(), + [loaded_entry](const EnumeratedHrtf &entry) -> bool + { return loaded_entry->get() == entry.hrtf; } + ); + if(iter != list.cend()) + { + TRACE("Skipping duplicate file entry %s\n", filename.c_str()); + return; + } + + break; + } + + if(loaded_entry == LoadedHrtfs.end()) + { + TRACE("Got new file \"%s\"\n", filename.c_str()); + + LoadedHrtfs.emplace_back(HrtfHandle::Create(filename.length()+1)); + loaded_entry = LoadedHrtfs.end()-1; + std::copy(filename.begin(), filename.end(), (*loaded_entry)->filename.begin()); + (*loaded_entry)->filename.back() = '\0'; + } + + /* TODO: Get a human-readable name from the HRTF data (possibly coming in a + * format update). */ + size_t namepos = filename.find_last_of('/')+1; + if(!namepos) namepos = filename.find_last_of('\\')+1; + + size_t extpos{filename.find_last_of('.')}; + if(extpos <= namepos) extpos = std::string::npos; + + const std::string basename{(extpos == std::string::npos) ? + filename.substr(namepos) : filename.substr(namepos, extpos-namepos)}; + std::string newname{basename}; + int count{1}; + while(checkName(list, newname)) + { + newname = basename; + newname += " #"; + newname += std::to_string(++count); + } + list.emplace_back(EnumeratedHrtf{newname, loaded_entry->get()}); + const EnumeratedHrtf &entry = list.back(); + + TRACE("Adding file entry \"%s\"\n", entry.name.c_str()); +} + +/* Unfortunate that we have to duplicate AddFileEntry to take a memory buffer + * for input instead of opening the given filename. + */ +void AddBuiltInEntry(al::vector<EnumeratedHrtf> &list, const std::string &filename, ALuint residx) +{ + auto loaded_entry = LoadedHrtfs.begin(); + for(;loaded_entry != LoadedHrtfs.end();++loaded_entry) + { + if(filename != (*loaded_entry)->filename.data()) + continue; + + /* Check if this entry has already been added to the list. */ + auto iter = std::find_if(list.cbegin(), list.cend(), + [loaded_entry](const EnumeratedHrtf &entry) -> bool + { return loaded_entry->get() == entry.hrtf; } + ); + if(iter != list.cend()) + { + TRACE("Skipping duplicate file entry %s\n", filename.c_str()); + return; + } + + break; + } + + if(loaded_entry == LoadedHrtfs.end()) + { + TRACE("Got new file \"%s\"\n", filename.c_str()); + + LoadedHrtfs.emplace_back(HrtfHandle::Create(filename.length()+32)); + loaded_entry = LoadedHrtfs.end()-1; + snprintf((*loaded_entry)->filename.data(), (*loaded_entry)->filename.size(), "!%u_%s", + residx, filename.c_str()); + } + + /* TODO: Get a human-readable name from the HRTF data (possibly coming in a + * format update). */ + + std::string newname{filename}; + int count{1}; + while(checkName(list, newname)) + { + newname = filename; + newname += " #"; + newname += std::to_string(++count); + } + list.emplace_back(EnumeratedHrtf{newname, loaded_entry->get()}); + const EnumeratedHrtf &entry = list.back(); + + TRACE("Adding built-in entry \"%s\"\n", entry.name.c_str()); +} + + +#define IDR_DEFAULT_44100_MHR 1 +#define IDR_DEFAULT_48000_MHR 2 + +using ResData = al::span<const char>; +#ifndef ALSOFT_EMBED_HRTF_DATA + +ResData GetResource(int /*name*/) +{ return ResData{}; } + +#else + +#include "default-44100.mhr.h" +#include "default-48000.mhr.h" + +ResData GetResource(int name) +{ + if(name == IDR_DEFAULT_44100_MHR) + return {reinterpret_cast<const char*>(hrtf_default_44100), sizeof(hrtf_default_44100)}; + if(name == IDR_DEFAULT_48000_MHR) + return {reinterpret_cast<const char*>(hrtf_default_48000), sizeof(hrtf_default_48000)}; + return ResData{}; +} +#endif + +} // namespace + + +al::vector<EnumeratedHrtf> EnumerateHrtf(const char *devname) +{ + al::vector<EnumeratedHrtf> list; + + bool usedefaults{true}; + if(auto pathopt = ConfigValueStr(devname, nullptr, "hrtf-paths")) + { + const char *pathlist{pathopt->c_str()}; + while(pathlist && *pathlist) + { + const char *next, *end; + + while(isspace(*pathlist) || *pathlist == ',') + pathlist++; + if(*pathlist == '\0') + continue; + + next = strchr(pathlist, ','); + if(next) + end = next++; + else + { + end = pathlist + strlen(pathlist); + usedefaults = false; + } + + while(end != pathlist && isspace(*(end-1))) + --end; + if(end != pathlist) + { + const std::string pname{pathlist, end}; + for(const auto &fname : SearchDataFiles(".mhr", pname.c_str())) + AddFileEntry(list, fname); + } + + pathlist = next; + } + } + else if(ConfigValueExists(devname, nullptr, "hrtf_tables")) + ERR("The hrtf_tables option is deprecated, please use hrtf-paths instead.\n"); + + if(usedefaults) + { + for(const auto &fname : SearchDataFiles(".mhr", "openal/hrtf")) + AddFileEntry(list, fname); + + if(!GetResource(IDR_DEFAULT_44100_MHR).empty()) + AddBuiltInEntry(list, "Built-In 44100hz", IDR_DEFAULT_44100_MHR); + + if(!GetResource(IDR_DEFAULT_48000_MHR).empty()) + AddBuiltInEntry(list, "Built-In 48000hz", IDR_DEFAULT_48000_MHR); + } + + if(!list.empty()) + { + if(auto defhrtfopt = ConfigValueStr(devname, nullptr, "default-hrtf")) + { + auto iter = std::find_if(list.begin(), list.end(), + [&defhrtfopt](const EnumeratedHrtf &entry) -> bool + { return entry.name == *defhrtfopt; } + ); + if(iter == list.end()) + WARN("Failed to find default HRTF \"%s\"\n", defhrtfopt->c_str()); + else if(iter != list.begin()) + { + EnumeratedHrtf entry{std::move(*iter)}; + list.erase(iter); + list.insert(list.begin(), std::move(entry)); + } + } + } + + return list; +} + +HrtfEntry *GetLoadedHrtf(HrtfHandle *handle) +{ + std::lock_guard<std::mutex> _{LoadedHrtfLock}; + + if(handle->entry) + { + HrtfEntry *hrtf{handle->entry.get()}; + hrtf->IncRef(); + return hrtf; + } + + std::unique_ptr<std::istream> stream; + const char *name{""}; + ALuint residx{}; + char ch{}; + if(sscanf(handle->filename.data(), "!%u%c", &residx, &ch) == 2 && ch == '_') + { + name = strchr(handle->filename.data(), ch)+1; + + TRACE("Loading %s...\n", name); + ResData res{GetResource(residx)}; + if(res.empty()) + { + ERR("Could not get resource %u, %s\n", residx, name); + return nullptr; + } + stream = al::make_unique<idstream>(res.begin(), res.end()); + } + else + { + name = handle->filename.data(); + + TRACE("Loading %s...\n", handle->filename.data()); + auto fstr = al::make_unique<al::ifstream>(handle->filename.data(), std::ios::binary); + if(!fstr->is_open()) + { + ERR("Could not open %s\n", handle->filename.data()); + return nullptr; + } + stream = std::move(fstr); + } + + std::unique_ptr<HrtfEntry> hrtf; + char magic[sizeof(magicMarker02)]; + stream->read(magic, sizeof(magic)); + if(stream->gcount() < static_cast<std::streamsize>(sizeof(magicMarker02))) + ERR("%s data is too short (%zu bytes)\n", name, stream->gcount()); + else if(memcmp(magic, magicMarker02, sizeof(magicMarker02)) == 0) + { + TRACE("Detected data set format v2\n"); + hrtf = LoadHrtf02(*stream, name); + } + else if(memcmp(magic, magicMarker01, sizeof(magicMarker01)) == 0) + { + TRACE("Detected data set format v1\n"); + hrtf = LoadHrtf01(*stream, name); + } + else if(memcmp(magic, magicMarker00, sizeof(magicMarker00)) == 0) + { + TRACE("Detected data set format v0\n"); + hrtf = LoadHrtf00(*stream, name); + } + else + ERR("Invalid header in %s: \"%.8s\"\n", name, magic); + stream.reset(); + + if(!hrtf) + { + ERR("Failed to load %s\n", name); + return nullptr; + } + + TRACE("Loaded HRTF support for format: %s %uhz\n", + DevFmtChannelsString(DevFmtStereo), hrtf->sampleRate); + handle->entry = std::move(hrtf); + + return handle->entry.get(); +} + + +void HrtfEntry::IncRef() +{ + auto ref = IncrementRef(&this->ref); + TRACEREF("HrtfEntry %p increasing refcount to %u\n", this, ref); +} + +void HrtfEntry::DecRef() +{ + auto ref = DecrementRef(&this->ref); + TRACEREF("HrtfEntry %p decreasing refcount to %u\n", this, ref); + if(ref == 0) + { + std::lock_guard<std::mutex> _{LoadedHrtfLock}; + + /* Go through and clear all unused HRTFs. */ + auto delete_unused = [](HrtfHandlePtr &handle) -> void + { + HrtfEntry *entry{handle->entry.get()}; + if(entry && ReadRef(&entry->ref) == 0) + { + TRACE("Unloading unused HRTF %s\n", handle->filename.data()); + handle->entry = nullptr; + } + }; + std::for_each(LoadedHrtfs.begin(), LoadedHrtfs.end(), delete_unused); + } +} diff --git a/alc/hrtf.h b/alc/hrtf.h new file mode 100644 index 00000000..6c41cb82 --- /dev/null +++ b/alc/hrtf.h @@ -0,0 +1,124 @@ +#ifndef ALC_HRTF_H +#define ALC_HRTF_H + +#include <array> +#include <cstddef> +#include <memory> +#include <string> + +#include "AL/al.h" + +#include "almalloc.h" +#include "ambidefs.h" +#include "atomic.h" +#include "vector.h" + + +struct HrtfHandle; + +#define HRTF_HISTORY_BITS (6) +#define HRTF_HISTORY_LENGTH (1<<HRTF_HISTORY_BITS) +#define HRTF_HISTORY_MASK (HRTF_HISTORY_LENGTH-1) + +#define HRIR_BITS (7) +#define HRIR_LENGTH (1<<HRIR_BITS) +#define HRIR_MASK (HRIR_LENGTH-1) + + +struct HrtfEntry { + RefCount ref; + + ALuint sampleRate; + ALsizei irSize; + + struct Field { + ALfloat distance; + ALubyte evCount; + }; + /* NOTE: Fields are stored *backwards*. field[0] is the farthest field, and + * field[fdCount-1] is the nearest. + */ + ALsizei fdCount; + const Field *field; + + struct Elevation { + ALushort azCount; + ALushort irOffset; + }; + Elevation *elev; + const ALfloat (*coeffs)[2]; + const ALubyte (*delays)[2]; + + void IncRef(); + void DecRef(); + + DEF_PLACE_NEWDEL() +}; + +struct EnumeratedHrtf { + std::string name; + + HrtfHandle *hrtf; +}; + + +using float2 = std::array<float,2>; + +template<typename T> +using HrirArray = std::array<std::array<T,2>,HRIR_LENGTH>; + +struct HrtfState { + alignas(16) std::array<ALfloat,HRTF_HISTORY_LENGTH> History; + alignas(16) HrirArray<ALfloat> Values; +}; + +struct HrtfFilter { + alignas(16) HrirArray<ALfloat> Coeffs; + ALsizei Delay[2]; + ALfloat Gain; +}; + +struct DirectHrtfState { + /* HRTF filter state for dry buffer content */ + ALsizei IrSize{0}; + struct ChanData { + alignas(16) HrirArray<ALfloat> Values; + alignas(16) HrirArray<ALfloat> Coeffs; + }; + al::FlexArray<ChanData> Chan; + + DirectHrtfState(size_t numchans) : Chan{numchans} { } + DirectHrtfState(const DirectHrtfState&) = delete; + DirectHrtfState& operator=(const DirectHrtfState&) = delete; + + static std::unique_ptr<DirectHrtfState> Create(size_t num_chans); + static constexpr size_t Sizeof(size_t numchans) noexcept + { return al::FlexArray<ChanData>::Sizeof(numchans, offsetof(DirectHrtfState, Chan)); } + + DEF_PLACE_NEWDEL() +}; + +struct AngularPoint { + ALfloat Elev; + ALfloat Azim; +}; + + +al::vector<EnumeratedHrtf> EnumerateHrtf(const char *devname); +HrtfEntry *GetLoadedHrtf(HrtfHandle *handle); + +void GetHrtfCoeffs(const HrtfEntry *Hrtf, ALfloat elevation, ALfloat azimuth, ALfloat distance, + ALfloat spread, HrirArray<ALfloat> &coeffs, ALsizei (&delays)[2]); + +/** + * Produces HRTF filter coefficients for decoding B-Format, given a set of + * virtual speaker positions, a matching decoding matrix, and per-order high- + * frequency gains for the decoder. The calculated impulse responses are + * ordered and scaled according to the matrix input. Note the specified virtual + * positions should be in degrees, not radians! + */ +void BuildBFormatHrtf(const HrtfEntry *Hrtf, DirectHrtfState *state, const ALuint NumChannels, + const AngularPoint *AmbiPoints, const ALfloat (*RESTRICT AmbiMatrix)[MAX_AMBI_CHANNELS], + const size_t AmbiCount, const ALfloat *RESTRICT AmbiOrderHFGain); + +#endif /* ALC_HRTF_H */ diff --git a/alc/inprogext.h b/alc/inprogext.h new file mode 100644 index 00000000..15881b59 --- /dev/null +++ b/alc/inprogext.h @@ -0,0 +1,92 @@ +#ifndef INPROGEXT_H +#define INPROGEXT_H + +#include "AL/al.h" +#include "AL/alc.h" +#include "AL/alext.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef ALC_SOFT_loopback_bformat +#define ALC_SOFT_loopback_bformat 1 +#define ALC_AMBISONIC_LAYOUT_SOFT 0x1997 +#define ALC_AMBISONIC_SCALING_SOFT 0x1998 +#define ALC_AMBISONIC_ORDER_SOFT 0x1999 +#define ALC_MAX_AMBISONIC_ORDER_SOFT 0x199B + +#define ALC_BFORMAT3D_SOFT 0x1508 + +/* Ambisonic layouts */ +#define ALC_FUMA_SOFT 0x0000 +#define ALC_ACN_SOFT 0x0001 + +/* Ambisonic scalings (normalization) */ +/*#define ALC_FUMA_SOFT*/ +#define ALC_SN3D_SOFT 0x0001 +#define ALC_N3D_SOFT 0x0002 +#endif + +#ifndef AL_SOFT_map_buffer +#define AL_SOFT_map_buffer 1 +typedef unsigned int ALbitfieldSOFT; +#define AL_MAP_READ_BIT_SOFT 0x00000001 +#define AL_MAP_WRITE_BIT_SOFT 0x00000002 +#define AL_MAP_PERSISTENT_BIT_SOFT 0x00000004 +#define AL_PRESERVE_DATA_BIT_SOFT 0x00000008 +typedef void (AL_APIENTRY*LPALBUFFERSTORAGESOFT)(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei freq, ALbitfieldSOFT flags); +typedef void* (AL_APIENTRY*LPALMAPBUFFERSOFT)(ALuint buffer, ALsizei offset, ALsizei length, ALbitfieldSOFT access); +typedef void (AL_APIENTRY*LPALUNMAPBUFFERSOFT)(ALuint buffer); +typedef void (AL_APIENTRY*LPALFLUSHMAPPEDBUFFERSOFT)(ALuint buffer, ALsizei offset, ALsizei length); +#ifdef AL_ALEXT_PROTOTYPES +AL_API void AL_APIENTRY alBufferStorageSOFT(ALuint buffer, ALenum format, const ALvoid *data, ALsizei size, ALsizei freq, ALbitfieldSOFT flags); +AL_API void* AL_APIENTRY alMapBufferSOFT(ALuint buffer, ALsizei offset, ALsizei length, ALbitfieldSOFT access); +AL_API void AL_APIENTRY alUnmapBufferSOFT(ALuint buffer); +AL_API void AL_APIENTRY alFlushMappedBufferSOFT(ALuint buffer, ALsizei offset, ALsizei length); +#endif +#endif + +#ifndef AL_SOFT_events +#define AL_SOFT_events 1 +#define AL_EVENT_CALLBACK_FUNCTION_SOFT 0x1220 +#define AL_EVENT_CALLBACK_USER_PARAM_SOFT 0x1221 +#define AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT 0x1222 +#define AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT 0x1223 +#define AL_EVENT_TYPE_ERROR_SOFT 0x1224 +#define AL_EVENT_TYPE_PERFORMANCE_SOFT 0x1225 +#define AL_EVENT_TYPE_DEPRECATED_SOFT 0x1226 +#define AL_EVENT_TYPE_DISCONNECTED_SOFT 0x1227 +typedef void (AL_APIENTRY*ALEVENTPROCSOFT)(ALenum eventType, ALuint object, ALuint param, + ALsizei length, const ALchar *message, + void *userParam); +typedef void (AL_APIENTRY*LPALEVENTCONTROLSOFT)(ALsizei count, const ALenum *types, ALboolean enable); +typedef void (AL_APIENTRY*LPALEVENTCALLBACKSOFT)(ALEVENTPROCSOFT callback, void *userParam); +typedef void* (AL_APIENTRY*LPALGETPOINTERSOFT)(ALenum pname); +typedef void (AL_APIENTRY*LPALGETPOINTERVSOFT)(ALenum pname, void **values); +#ifdef AL_ALEXT_PROTOTYPES +AL_API void AL_APIENTRY alEventControlSOFT(ALsizei count, const ALenum *types, ALboolean enable); +AL_API void AL_APIENTRY alEventCallbackSOFT(ALEVENTPROCSOFT callback, void *userParam); +AL_API void* AL_APIENTRY alGetPointerSOFT(ALenum pname); +AL_API void AL_APIENTRY alGetPointervSOFT(ALenum pname, void **values); +#endif +#endif + +#ifndef AL_SOFT_buffer_layers +#define AL_SOFT_buffer_layers +typedef void (AL_APIENTRY*LPALSOURCEQUEUEBUFFERLAYERSSOFT)(ALuint src, ALsizei nb, const ALuint *buffers); +#ifdef AL_ALEXT_PROTOTYPES +AL_API void AL_APIENTRY alSourceQueueBufferLayersSOFT(ALuint src, ALsizei nb, const ALuint *buffers); +#endif +#endif + +#ifndef AL_SOFT_effect_chain +#define AL_SOFT_effect_chain +#define AL_EFFECTSLOT_TARGET_SOFT 0xf000 +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* INPROGEXT_H */ diff --git a/alc/logging.h b/alc/logging.h new file mode 100644 index 00000000..0bb0c87b --- /dev/null +++ b/alc/logging.h @@ -0,0 +1,64 @@ +#ifndef LOGGING_H +#define LOGGING_H + +#include <stdio.h> + +#include "opthelpers.h" + + +#ifdef __GNUC__ +#define DECL_FORMAT(x, y, z) __attribute__((format(x, (y), (z)))) +#else +#define DECL_FORMAT(x, y, z) +#endif + + +extern FILE *gLogFile; + +void al_print(FILE *logfile, const char *fmt, ...) DECL_FORMAT(printf, 2,3); +#if !defined(_WIN32) +#define AL_PRINT(T, ...) fprintf(gLogFile, "AL lib: " T " " __VA_ARGS__) +#else +#define AL_PRINT(T, ...) al_print(gLogFile, "AL lib: " T " " __VA_ARGS__) +#endif + +#ifdef __ANDROID__ +#include <android/log.h> +#define LOG_ANDROID(T, ...) __android_log_print(T, "openal", "AL lib: " __VA_ARGS__) +#else +#define LOG_ANDROID(T, ...) ((void)0) +#endif + +enum LogLevel { + NoLog, + LogError, + LogWarning, + LogTrace, + LogRef +}; +extern LogLevel gLogLevel; + +#define TRACEREF(...) do { \ + if(UNLIKELY(gLogLevel >= LogRef)) \ + AL_PRINT("(--)", __VA_ARGS__); \ +} while(0) + +#define TRACE(...) do { \ + if(UNLIKELY(gLogLevel >= LogTrace)) \ + AL_PRINT("(II)", __VA_ARGS__); \ + LOG_ANDROID(ANDROID_LOG_DEBUG, __VA_ARGS__); \ +} while(0) + +#define WARN(...) do { \ + if(UNLIKELY(gLogLevel >= LogWarning)) \ + AL_PRINT("(WW)", __VA_ARGS__); \ + LOG_ANDROID(ANDROID_LOG_WARN, __VA_ARGS__); \ +} while(0) + +#define ERR(...) do { \ + if(UNLIKELY(gLogLevel >= LogError)) \ + AL_PRINT("(EE)", __VA_ARGS__); \ + LOG_ANDROID(ANDROID_LOG_ERROR, __VA_ARGS__); \ +} while(0) + +#endif /* LOGGING_H */ diff --git a/alc/mastering.cpp b/alc/mastering.cpp new file mode 100644 index 00000000..551fdcdf --- /dev/null +++ b/alc/mastering.cpp @@ -0,0 +1,479 @@ +#include "config.h" + +#include <cmath> +#include <limits> +#include <algorithm> +#include <functional> + +#include "mastering.h" +#include "alu.h" +#include "almalloc.h" +#include "math_defs.h" + + +/* These structures assume BUFFERSIZE is a power of 2. */ +static_assert((BUFFERSIZE & (BUFFERSIZE-1)) == 0, "BUFFERSIZE is not a power of 2"); + +struct SlidingHold { + alignas(16) ALfloat mValues[BUFFERSIZE]; + ALsizei mExpiries[BUFFERSIZE]; + ALsizei mLowerIndex; + ALsizei mUpperIndex; + ALsizei mLength; +}; + + +namespace { + +using namespace std::placeholders; + +/* This sliding hold follows the input level with an instant attack and a + * fixed duration hold before an instant release to the next highest level. + * It is a sliding window maximum (descending maxima) implementation based on + * Richard Harter's ascending minima algorithm available at: + * + * http://www.richardhartersworld.com/cri/2001/slidingmin.html + */ +ALfloat UpdateSlidingHold(SlidingHold *Hold, const ALsizei i, const ALfloat in) +{ + static constexpr ALsizei mask{BUFFERSIZE - 1}; + const ALsizei length{Hold->mLength}; + ALfloat (&values)[BUFFERSIZE] = Hold->mValues; + ALsizei (&expiries)[BUFFERSIZE] = Hold->mExpiries; + ALsizei lowerIndex{Hold->mLowerIndex}; + ALsizei upperIndex{Hold->mUpperIndex}; + + ASSUME(upperIndex >= 0); + ASSUME(lowerIndex >= 0); + + if(i >= expiries[upperIndex]) + upperIndex = (upperIndex + 1) & mask; + + if(in >= values[upperIndex]) + { + values[upperIndex] = in; + expiries[upperIndex] = i + length; + lowerIndex = upperIndex; + } + else + { + do { + do { + if(!(in >= values[lowerIndex])) + goto found_place; + } while(lowerIndex--); + lowerIndex = mask; + } while(1); + found_place: + + lowerIndex = (lowerIndex + 1) & mask; + values[lowerIndex] = in; + expiries[lowerIndex] = i + length; + } + + Hold->mLowerIndex = lowerIndex; + Hold->mUpperIndex = upperIndex; + + return values[upperIndex]; +} + +void ShiftSlidingHold(SlidingHold *Hold, const ALsizei n) +{ + ASSUME(Hold->mUpperIndex >= 0); + ASSUME(Hold->mLowerIndex >= 0); + + auto exp_begin = std::begin(Hold->mExpiries) + Hold->mUpperIndex; + auto exp_last = std::begin(Hold->mExpiries) + Hold->mLowerIndex; + if(exp_last < exp_begin) + { + std::transform(exp_begin, std::end(Hold->mExpiries), exp_begin, + std::bind(std::minus<ALsizei>{}, _1, n)); + exp_begin = std::begin(Hold->mExpiries); + } + std::transform(exp_begin, exp_last+1, exp_begin, std::bind(std::minus<ALsizei>{}, _1, n)); +} + + +/* Multichannel compression is linked via the absolute maximum of all + * channels. + */ +void LinkChannels(Compressor *Comp, const ALsizei SamplesToDo, const FloatBufferLine *OutBuffer) +{ + const ALsizei index{Comp->mLookAhead}; + const ALuint numChans{Comp->mNumChans}; + + ASSUME(SamplesToDo > 0); + ASSUME(numChans > 0); + ASSUME(index >= 0); + + auto side_begin = std::begin(Comp->mSideChain) + index; + std::fill(side_begin, side_begin+SamplesToDo, 0.0f); + + auto fill_max = [SamplesToDo,side_begin](const FloatBufferLine &input) -> void + { + const ALfloat *RESTRICT buffer{al::assume_aligned<16>(input.data())}; + auto max_abs = std::bind(maxf, _1, std::bind(static_cast<float(&)(float)>(std::fabs), _2)); + std::transform(side_begin, side_begin+SamplesToDo, buffer, side_begin, max_abs); + }; + std::for_each(OutBuffer, OutBuffer+numChans, fill_max); +} + +/* This calculates the squared crest factor of the control signal for the + * basic automation of the attack/release times. As suggested by the paper, + * it uses an instantaneous squared peak detector and a squared RMS detector + * both with 200ms release times. + */ +static void CrestDetector(Compressor *Comp, const ALsizei SamplesToDo) +{ + const ALfloat a_crest{Comp->mCrestCoeff}; + const ALsizei index{Comp->mLookAhead}; + ALfloat y2_peak{Comp->mLastPeakSq}; + ALfloat y2_rms{Comp->mLastRmsSq}; + + ASSUME(SamplesToDo > 0); + ASSUME(index >= 0); + + auto calc_crest = [&y2_rms,&y2_peak,a_crest](const ALfloat x_abs) noexcept -> ALfloat + { + ALfloat x2 = maxf(0.000001f, x_abs * x_abs); + + y2_peak = maxf(x2, lerp(x2, y2_peak, a_crest)); + y2_rms = lerp(x2, y2_rms, a_crest); + return y2_peak / y2_rms; + }; + auto side_begin = std::begin(Comp->mSideChain) + index; + std::transform(side_begin, side_begin+SamplesToDo, std::begin(Comp->mCrestFactor), calc_crest); + + Comp->mLastPeakSq = y2_peak; + Comp->mLastRmsSq = y2_rms; +} + +/* The side-chain starts with a simple peak detector (based on the absolute + * value of the incoming signal) and performs most of its operations in the + * log domain. + */ +void PeakDetector(Compressor *Comp, const ALsizei SamplesToDo) +{ + const ALsizei index{Comp->mLookAhead}; + + ASSUME(SamplesToDo > 0); + ASSUME(index >= 0); + + /* Clamp the minimum amplitude to near-zero and convert to logarithm. */ + auto side_begin = std::begin(Comp->mSideChain) + index; + std::transform(side_begin, side_begin+SamplesToDo, side_begin, + std::bind(static_cast<float(&)(float)>(std::log), std::bind(maxf, 0.000001f, _1))); +} + +/* An optional hold can be used to extend the peak detector so it can more + * solidly detect fast transients. This is best used when operating as a + * limiter. + */ +void PeakHoldDetector(Compressor *Comp, const ALsizei SamplesToDo) +{ + const ALsizei index{Comp->mLookAhead}; + + ASSUME(SamplesToDo > 0); + ASSUME(index >= 0); + + SlidingHold *hold{Comp->mHold}; + ALsizei i{0}; + auto detect_peak = [&i,hold](const ALfloat x_abs) -> ALfloat + { + const ALfloat x_G{std::log(maxf(0.000001f, x_abs))}; + return UpdateSlidingHold(hold, i++, x_G); + }; + auto side_begin = std::begin(Comp->mSideChain) + index; + std::transform(side_begin, side_begin+SamplesToDo, side_begin, detect_peak); + + ShiftSlidingHold(hold, SamplesToDo); +} + +/* This is the heart of the feed-forward compressor. It operates in the log + * domain (to better match human hearing) and can apply some basic automation + * to knee width, attack/release times, make-up/post gain, and clipping + * reduction. + */ +void GainCompressor(Compressor *Comp, const ALsizei SamplesToDo) +{ + const bool autoKnee{Comp->mAuto.Knee}; + const bool autoAttack{Comp->mAuto.Attack}; + const bool autoRelease{Comp->mAuto.Release}; + const bool autoPostGain{Comp->mAuto.PostGain}; + const bool autoDeclip{Comp->mAuto.Declip}; + const ALsizei lookAhead{Comp->mLookAhead}; + const ALfloat threshold{Comp->mThreshold}; + const ALfloat slope{Comp->mSlope}; + const ALfloat attack{Comp->mAttack}; + const ALfloat release{Comp->mRelease}; + const ALfloat c_est{Comp->mGainEstimate}; + const ALfloat a_adp{Comp->mAdaptCoeff}; + const ALfloat (&crestFactor)[BUFFERSIZE] = Comp->mCrestFactor; + ALfloat (&sideChain)[BUFFERSIZE*2] = Comp->mSideChain; + ALfloat postGain{Comp->mPostGain}; + ALfloat knee{Comp->mKnee}; + ALfloat t_att{attack}; + ALfloat t_rel{release - attack}; + ALfloat a_att{std::exp(-1.0f / t_att)}; + ALfloat a_rel{std::exp(-1.0f / t_rel)}; + ALfloat y_1{Comp->mLastRelease}; + ALfloat y_L{Comp->mLastAttack}; + ALfloat c_dev{Comp->mLastGainDev}; + + ASSUME(SamplesToDo > 0); + ASSUME(lookAhead >= 0); + + for(ALsizei i{0};i < SamplesToDo;i++) + { + if(autoKnee) + knee = maxf(0.0f, 2.5f * (c_dev + c_est)); + const ALfloat knee_h{0.5f * knee}; + + /* This is the gain computer. It applies a static compression curve + * to the control signal. + */ + const ALfloat x_over{sideChain[lookAhead+i] - threshold}; + const ALfloat y_G{ + (x_over <= -knee_h) ? 0.0f : + (std::fabs(x_over) < knee_h) ? (x_over + knee_h) * (x_over + knee_h) / (2.0f * knee) : + x_over + }; + + const ALfloat y2_crest{crestFactor[i]}; + if(autoAttack) + { + t_att = 2.0f*attack/y2_crest; + a_att = std::exp(-1.0f / t_att); + } + if(autoRelease) + { + t_rel = 2.0f*release/y2_crest - t_att; + a_rel = std::exp(-1.0f / t_rel); + } + + /* Gain smoothing (ballistics) is done via a smooth decoupled peak + * detector. The attack time is subtracted from the release time + * above to compensate for the chained operating mode. + */ + const ALfloat x_L{-slope * y_G}; + y_1 = maxf(x_L, lerp(x_L, y_1, a_rel)); + y_L = lerp(y_1, y_L, a_att); + + /* Knee width and make-up gain automation make use of a smoothed + * measurement of deviation between the control signal and estimate. + * The estimate is also used to bias the measurement to hot-start its + * average. + */ + c_dev = lerp(-(y_L+c_est), c_dev, a_adp); + + if(autoPostGain) + { + /* Clipping reduction is only viable when make-up gain is being + * automated. It modifies the deviation to further attenuate the + * control signal when clipping is detected. The adaptation time + * is sufficiently long enough to suppress further clipping at the + * same output level. + */ + if(autoDeclip) + c_dev = maxf(c_dev, sideChain[i] - y_L - threshold - c_est); + + postGain = -(c_dev + c_est); + } + + sideChain[i] = std::exp(postGain - y_L); + } + + Comp->mLastRelease = y_1; + Comp->mLastAttack = y_L; + Comp->mLastGainDev = c_dev; +} + +/* Combined with the hold time, a look-ahead delay can improve handling of + * fast transients by allowing the envelope time to converge prior to + * reaching the offending impulse. This is best used when operating as a + * limiter. + */ +void SignalDelay(Compressor *Comp, const ALsizei SamplesToDo, FloatBufferLine *OutBuffer) +{ + const ALuint numChans{Comp->mNumChans}; + const ALsizei lookAhead{Comp->mLookAhead}; + + ASSUME(SamplesToDo > 0); + ASSUME(numChans > 0); + ASSUME(lookAhead > 0); + + for(ALuint c{0};c < numChans;c++) + { + ALfloat *inout{al::assume_aligned<16>(OutBuffer[c].data())}; + ALfloat *delaybuf{al::assume_aligned<16>(Comp->mDelay[c].data())}; + + auto inout_end = inout + SamplesToDo; + if(LIKELY(SamplesToDo >= lookAhead)) + { + auto delay_end = std::rotate(inout, inout_end - lookAhead, inout_end); + std::swap_ranges(inout, delay_end, delaybuf); + } + else + { + auto delay_start = std::swap_ranges(inout, inout_end, delaybuf); + std::rotate(delaybuf, delay_start, delaybuf + lookAhead); + } + } +} + +} // namespace + +/* The compressor is initialized with the following settings: + * + * NumChans - Number of channels to process. + * SampleRate - Sample rate to process. + * AutoKnee - Whether to automate the knee width parameter. + * AutoAttack - Whether to automate the attack time parameter. + * AutoRelease - Whether to automate the release time parameter. + * AutoPostGain - Whether to automate the make-up (post) gain parameter. + * AutoDeclip - Whether to automate clipping reduction. Ignored when + * not automating make-up gain. + * LookAheadTime - Look-ahead time (in seconds). + * HoldTime - Peak hold-time (in seconds). + * PreGainDb - Gain applied before detection (in dB). + * PostGainDb - Make-up gain applied after compression (in dB). + * ThresholdDb - Triggering threshold (in dB). + * Ratio - Compression ratio (x:1). Set to INFINITY for true + * limiting. Ignored when automating knee width. + * KneeDb - Knee width (in dB). Ignored when automating knee + * width. + * AttackTimeMin - Attack time (in seconds). Acts as a maximum when + * automating attack time. + * ReleaseTimeMin - Release time (in seconds). Acts as a maximum when + * automating release time. + */ +std::unique_ptr<Compressor> CompressorInit(const ALuint NumChans, const ALuint SampleRate, + const ALboolean AutoKnee, const ALboolean AutoAttack, const ALboolean AutoRelease, + const ALboolean AutoPostGain, const ALboolean AutoDeclip, const ALfloat LookAheadTime, + const ALfloat HoldTime, const ALfloat PreGainDb, const ALfloat PostGainDb, + const ALfloat ThresholdDb, const ALfloat Ratio, const ALfloat KneeDb, const ALfloat AttackTime, + const ALfloat ReleaseTime) +{ + const auto lookAhead = static_cast<ALsizei>( + clampf(std::round(LookAheadTime*SampleRate), 0.0f, BUFFERSIZE-1)); + const auto hold = static_cast<ALsizei>( + clampf(std::round(HoldTime*SampleRate), 0.0f, BUFFERSIZE-1)); + + size_t size{sizeof(Compressor)}; + if(lookAhead > 0) + { + size += sizeof(*Compressor::mDelay) * NumChans; + /* The sliding hold implementation doesn't handle a length of 1. A 1- + * sample hold is useless anyway, it would only ever give back what was + * just given to it. + */ + if(hold > 1) + size += sizeof(*Compressor::mHold); + } + + auto Comp = std::unique_ptr<Compressor>{new (al_calloc(16, size)) Compressor{}}; + Comp->mNumChans = NumChans; + Comp->mSampleRate = SampleRate; + Comp->mAuto.Knee = AutoKnee != AL_FALSE; + Comp->mAuto.Attack = AutoAttack != AL_FALSE; + Comp->mAuto.Release = AutoRelease != AL_FALSE; + Comp->mAuto.PostGain = AutoPostGain != AL_FALSE; + Comp->mAuto.Declip = AutoPostGain && AutoDeclip; + Comp->mLookAhead = lookAhead; + Comp->mPreGain = std::pow(10.0f, PreGainDb / 20.0f); + Comp->mPostGain = PostGainDb * std::log(10.0f) / 20.0f; + Comp->mThreshold = ThresholdDb * std::log(10.0f) / 20.0f; + Comp->mSlope = 1.0f / maxf(1.0f, Ratio) - 1.0f; + Comp->mKnee = maxf(0.0f, KneeDb * std::log(10.0f) / 20.0f); + Comp->mAttack = maxf(1.0f, AttackTime * SampleRate); + Comp->mRelease = maxf(1.0f, ReleaseTime * SampleRate); + + /* Knee width automation actually treats the compressor as a limiter. By + * varying the knee width, it can effectively be seen as applying + * compression over a wide range of ratios. + */ + if(AutoKnee) + Comp->mSlope = -1.0f; + + if(lookAhead > 0) + { + if(hold > 1) + { + Comp->mHold = ::new (static_cast<void*>(Comp.get() + 1)) SlidingHold{}; + Comp->mHold->mValues[0] = -std::numeric_limits<float>::infinity(); + Comp->mHold->mExpiries[0] = hold; + Comp->mHold->mLength = hold; + Comp->mDelay = ::new (static_cast<void*>(Comp->mHold + 1)) FloatBufferLine[NumChans]; + } + else + { + Comp->mDelay = ::new (static_cast<void*>(Comp.get() + 1)) FloatBufferLine[NumChans]; + } + } + + Comp->mCrestCoeff = std::exp(-1.0f / (0.200f * SampleRate)); // 200ms + Comp->mGainEstimate = Comp->mThreshold * -0.5f * Comp->mSlope; + Comp->mAdaptCoeff = std::exp(-1.0f / (2.0f * SampleRate)); // 2s + + return Comp; +} + +Compressor::~Compressor() +{ + if(mHold) + al::destroy_at(mHold); + mHold = nullptr; + if(mDelay) + al::destroy_n(mDelay, mNumChans); + mDelay = nullptr; +} + + +void Compressor::process(const ALsizei SamplesToDo, FloatBufferLine *OutBuffer) +{ + const ALuint numChans{mNumChans}; + + ASSUME(SamplesToDo > 0); + ASSUME(numChans > 0); + + const ALfloat preGain{mPreGain}; + if(preGain != 1.0f) + { + auto apply_gain = [SamplesToDo,preGain](FloatBufferLine &input) noexcept -> void + { + ALfloat *buffer{al::assume_aligned<16>(input.data())}; + std::transform(buffer, buffer+SamplesToDo, buffer, + std::bind(std::multiplies<float>{}, _1, preGain)); + }; + std::for_each(OutBuffer, OutBuffer+numChans, apply_gain); + } + + LinkChannels(this, SamplesToDo, OutBuffer); + + if(mAuto.Attack || mAuto.Release) + CrestDetector(this, SamplesToDo); + + if(mHold) + PeakHoldDetector(this, SamplesToDo); + else + PeakDetector(this, SamplesToDo); + + GainCompressor(this, SamplesToDo); + + if(mDelay) + SignalDelay(this, SamplesToDo, OutBuffer); + + const ALfloat (&sideChain)[BUFFERSIZE*2] = mSideChain; + auto apply_comp = [SamplesToDo,&sideChain](FloatBufferLine &input) noexcept -> void + { + ALfloat *buffer{al::assume_aligned<16>(input.data())}; + const ALfloat *gains{al::assume_aligned<16>(&sideChain[0])}; + std::transform(gains, gains+SamplesToDo, buffer, buffer, + std::bind(std::multiplies<float>{}, _1, _2)); + }; + std::for_each(OutBuffer, OutBuffer+numChans, apply_comp); + + ASSUME(mLookAhead >= 0); + auto side_begin = std::begin(mSideChain) + SamplesToDo; + std::copy(side_begin, side_begin+mLookAhead, std::begin(mSideChain)); +} diff --git a/alc/mastering.h b/alc/mastering.h new file mode 100644 index 00000000..34dc8dcb --- /dev/null +++ b/alc/mastering.h @@ -0,0 +1,104 @@ +#ifndef MASTERING_H +#define MASTERING_H + +#include <memory> + +#include "AL/al.h" + +#include "almalloc.h" +/* For FloatBufferLine/BUFFERSIZE. */ +#include "alcmain.h" + + +struct SlidingHold; + +/* General topology and basic automation was based on the following paper: + * + * D. Giannoulis, M. Massberg and J. D. Reiss, + * "Parameter Automation in a Dynamic Range Compressor," + * Journal of the Audio Engineering Society, v61 (10), Oct. 2013 + * + * Available (along with supplemental reading) at: + * + * http://c4dm.eecs.qmul.ac.uk/audioengineering/compressors/ + */ +struct Compressor { + ALuint mNumChans{0u}; + ALuint mSampleRate{0u}; + + struct { + bool Knee : 1; + bool Attack : 1; + bool Release : 1; + bool PostGain : 1; + bool Declip : 1; + } mAuto{}; + + ALsizei mLookAhead{0}; + + ALfloat mPreGain{0.0f}; + ALfloat mPostGain{0.0f}; + + ALfloat mThreshold{0.0f}; + ALfloat mSlope{0.0f}; + ALfloat mKnee{0.0f}; + + ALfloat mAttack{0.0f}; + ALfloat mRelease{0.0f}; + + alignas(16) ALfloat mSideChain[2*BUFFERSIZE]{}; + alignas(16) ALfloat mCrestFactor[BUFFERSIZE]{}; + + SlidingHold *mHold{nullptr}; + FloatBufferLine *mDelay{nullptr}; + + ALfloat mCrestCoeff{0.0f}; + ALfloat mGainEstimate{0.0f}; + ALfloat mAdaptCoeff{0.0f}; + + ALfloat mLastPeakSq{0.0f}; + ALfloat mLastRmsSq{0.0f}; + ALfloat mLastRelease{0.0f}; + ALfloat mLastAttack{0.0f}; + ALfloat mLastGainDev{0.0f}; + + + ~Compressor(); + void process(const ALsizei SamplesToDo, FloatBufferLine *OutBuffer); + ALsizei getLookAhead() const noexcept { return mLookAhead; } + + DEF_PLACE_NEWDEL() +}; + +/* The compressor is initialized with the following settings: + * + * NumChans - Number of channels to process. + * SampleRate - Sample rate to process. + * AutoKnee - Whether to automate the knee width parameter. + * AutoAttack - Whether to automate the attack time parameter. + * AutoRelease - Whether to automate the release time parameter. + * AutoPostGain - Whether to automate the make-up (post) gain parameter. + * AutoDeclip - Whether to automate clipping reduction. Ignored when + * not automating make-up gain. + * LookAheadTime - Look-ahead time (in seconds). + * HoldTime - Peak hold-time (in seconds). + * PreGainDb - Gain applied before detection (in dB). + * PostGainDb - Make-up gain applied after compression (in dB). + * ThresholdDb - Triggering threshold (in dB). + * Ratio - Compression ratio (x:1). Set to INFINIFTY for true + * limiting. Ignored when automating knee width. + * KneeDb - Knee width (in dB). Ignored when automating knee + * width. + * AttackTimeMin - Attack time (in seconds). Acts as a maximum when + * automating attack time. + * ReleaseTimeMin - Release time (in seconds). Acts as a maximum when + * automating release time. + */ +std::unique_ptr<Compressor> CompressorInit(const ALuint NumChans, const ALuint SampleRate, + const ALboolean AutoKnee, const ALboolean AutoAttack, const ALboolean AutoRelease, + const ALboolean AutoPostGain, const ALboolean AutoDeclip, const ALfloat LookAheadTime, + const ALfloat HoldTime, const ALfloat PreGainDb, const ALfloat PostGainDb, + const ALfloat ThresholdDb, const ALfloat Ratio, const ALfloat KneeDb, const ALfloat AttackTime, + const ALfloat ReleaseTime); + +#endif /* MASTERING_H */ diff --git a/alc/mixer/defs.h b/alc/mixer/defs.h new file mode 100644 index 00000000..3e5d1125 --- /dev/null +++ b/alc/mixer/defs.h @@ -0,0 +1,59 @@ +#ifndef MIXER_DEFS_H +#define MIXER_DEFS_H + +#include "AL/alc.h" +#include "AL/al.h" + +#include "alcmain.h" +#include "alu.h" +#include "alspan.h" + + +struct MixGains; +struct MixHrtfFilter; +struct HrtfState; +struct DirectHrtfState; + + +struct CTag { }; +struct SSETag { }; +struct SSE2Tag { }; +struct SSE3Tag { }; +struct SSE4Tag { }; +struct NEONTag { }; + +struct CopyTag { }; +struct PointTag { }; +struct LerpTag { }; +struct CubicTag { }; +struct BSincTag { }; + +template<typename TypeTag, typename InstTag> +const ALfloat *Resample_(const InterpState *state, const ALfloat *RESTRICT src, ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen); + +template<typename InstTag> +void Mix_(const ALfloat *data, const al::span<FloatBufferLine> OutBuffer, ALfloat *CurrentGains, const ALfloat *TargetGains, const ALsizei Counter, const ALsizei OutPos, const ALsizei BufferSize); +template<typename InstTag> +void MixRow_(FloatBufferLine &OutBuffer, const ALfloat *Gains, const al::span<const FloatBufferLine> InSamples, const ALsizei InPos, const ALsizei BufferSize); + +template<typename InstTag> +void MixHrtf_(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, MixHrtfFilter *hrtfparams, const ALsizei BufferSize); +template<typename InstTag> +void MixHrtfBlend_(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, const HrtfFilter *oldparams, MixHrtfFilter *newparams, const ALsizei BufferSize); +template<typename InstTag> +void MixDirectHrtf_(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples, DirectHrtfState *State, const ALsizei BufferSize); + +/* Vectorized resampler helpers */ +inline void InitiatePositionArrays(ALsizei frac, ALint increment, ALsizei *RESTRICT frac_arr, ALsizei *RESTRICT pos_arr, ALsizei size) +{ + pos_arr[0] = 0; + frac_arr[0] = frac; + for(ALsizei i{1};i < size;i++) + { + ALint frac_tmp = frac_arr[i-1] + increment; + pos_arr[i] = pos_arr[i-1] + (frac_tmp>>FRACTIONBITS); + frac_arr[i] = frac_tmp&FRACTIONMASK; + } +} + +#endif /* MIXER_DEFS_H */ diff --git a/alc/mixer/hrtfbase.h b/alc/mixer/hrtfbase.h new file mode 100644 index 00000000..a76bd62e --- /dev/null +++ b/alc/mixer/hrtfbase.h @@ -0,0 +1,138 @@ +#ifndef MIXER_HRTFBASE_H +#define MIXER_HRTFBASE_H + +#include <algorithm> + +#include "alu.h" +#include "../hrtf.h" +#include "opthelpers.h" + + +using ApplyCoeffsT = void(ALsizei Offset, float2 *RESTRICT Values, const ALsizei irSize, + const HrirArray<ALfloat> &Coeffs, const ALfloat left, const ALfloat right); + +template<ApplyCoeffsT &ApplyCoeffs> +inline void MixHrtfBase(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *RESTRICT AccumSamples, const ALsizei OutPos, + const ALsizei IrSize, MixHrtfFilter *hrtfparams, const ALsizei BufferSize) +{ + ASSUME(OutPos >= 0); + ASSUME(IrSize >= 4); + ASSUME(BufferSize > 0); + + const auto &Coeffs = *hrtfparams->Coeffs; + const ALfloat gainstep{hrtfparams->GainStep}; + const ALfloat gain{hrtfparams->Gain}; + + ALsizei Delay[2]{ + HRTF_HISTORY_LENGTH - hrtfparams->Delay[0], + HRTF_HISTORY_LENGTH - hrtfparams->Delay[1] }; + ASSUME(Delay[0] >= 0 && Delay[1] >= 0); + ALfloat stepcount{0.0f}; + for(ALsizei i{0};i < BufferSize;++i) + { + const ALfloat g{gain + gainstep*stepcount}; + const ALfloat left{InSamples[Delay[0]++] * g}; + const ALfloat right{InSamples[Delay[1]++] * g}; + ApplyCoeffs(i, AccumSamples+i, IrSize, Coeffs, left, right); + + stepcount += 1.0f; + } + + for(ALsizei i{0};i < BufferSize;++i) + LeftOut[OutPos+i] += AccumSamples[i][0]; + for(ALsizei i{0};i < BufferSize;++i) + RightOut[OutPos+i] += AccumSamples[i][1]; + + hrtfparams->Gain = gain + gainstep*stepcount; +} + +template<ApplyCoeffsT &ApplyCoeffs> +inline void MixHrtfBlendBase(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *RESTRICT AccumSamples, const ALsizei OutPos, + const ALsizei IrSize, const HrtfFilter *oldparams, MixHrtfFilter *newparams, + const ALsizei BufferSize) +{ + const auto &OldCoeffs = oldparams->Coeffs; + const ALfloat oldGain{oldparams->Gain}; + const ALfloat oldGainStep{-oldGain / static_cast<ALfloat>(BufferSize)}; + const auto &NewCoeffs = *newparams->Coeffs; + const ALfloat newGainStep{newparams->GainStep}; + + ASSUME(OutPos >= 0); + ASSUME(IrSize >= 4); + ASSUME(BufferSize > 0); + + ALsizei Delay[2]{ + HRTF_HISTORY_LENGTH - oldparams->Delay[0], + HRTF_HISTORY_LENGTH - oldparams->Delay[1] }; + ASSUME(Delay[0] >= 0 && Delay[1] >= 0); + ALfloat stepcount{0.0f}; + for(ALsizei i{0};i < BufferSize;++i) + { + const ALfloat g{oldGain + oldGainStep*stepcount}; + const ALfloat left{InSamples[Delay[0]++] * g}; + const ALfloat right{InSamples[Delay[1]++] * g}; + ApplyCoeffs(i, AccumSamples+i, IrSize, OldCoeffs, left, right); + + stepcount += 1.0f; + } + + Delay[0] = HRTF_HISTORY_LENGTH - newparams->Delay[0]; + Delay[1] = HRTF_HISTORY_LENGTH - newparams->Delay[1]; + ASSUME(Delay[0] >= 0 && Delay[1] >= 0); + stepcount = 0.0f; + for(ALsizei i{0};i < BufferSize;++i) + { + const ALfloat g{newGainStep*stepcount}; + const ALfloat left{InSamples[Delay[0]++] * g}; + const ALfloat right{InSamples[Delay[1]++] * g}; + ApplyCoeffs(i, AccumSamples+i, IrSize, NewCoeffs, left, right); + + stepcount += 1.0f; + } + + for(ALsizei i{0};i < BufferSize;++i) + LeftOut[OutPos+i] += AccumSamples[i][0]; + for(ALsizei i{0};i < BufferSize;++i) + RightOut[OutPos+i] += AccumSamples[i][1]; + + newparams->Gain = newGainStep*stepcount; +} + +template<ApplyCoeffsT &ApplyCoeffs> +inline void MixDirectHrtfBase(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const al::span<const FloatBufferLine> InSamples, float2 *RESTRICT AccumSamples, + DirectHrtfState *State, const ALsizei BufferSize) +{ + ASSUME(BufferSize > 0); + + const ALsizei IrSize{State->IrSize}; + ASSUME(IrSize >= 4); + + auto chanstate = State->Chan.begin(); + for(const FloatBufferLine &input : InSamples) + { + const auto &Coeffs = chanstate->Coeffs; + + auto accum_iter = std::copy_n(chanstate->Values.begin(), + chanstate->Values.size(), AccumSamples); + std::fill_n(accum_iter, BufferSize, float2{}); + + for(ALsizei i{0};i < BufferSize;++i) + { + const ALfloat insample{input[i]}; + ApplyCoeffs(i, AccumSamples+i, IrSize, Coeffs, insample, insample); + } + for(ALsizei i{0};i < BufferSize;++i) + LeftOut[i] += AccumSamples[i][0]; + for(ALsizei i{0};i < BufferSize;++i) + RightOut[i] += AccumSamples[i][1]; + + std::copy_n(AccumSamples + BufferSize, chanstate->Values.size(), + chanstate->Values.begin()); + ++chanstate; + } +} + +#endif /* MIXER_HRTFBASE_H */ diff --git a/alc/mixer/mixer_c.cpp b/alc/mixer/mixer_c.cpp new file mode 100644 index 00000000..47c4a6f4 --- /dev/null +++ b/alc/mixer/mixer_c.cpp @@ -0,0 +1,208 @@ +#include "config.h" + +#include <cassert> + +#include <limits> + +#include "alcmain.h" +#include "alu.h" +#include "alSource.h" +#include "alAuxEffectSlot.h" +#include "defs.h" +#include "hrtfbase.h" + + +namespace { + +inline ALfloat do_point(const InterpState&, const ALfloat *RESTRICT vals, const ALsizei) +{ return vals[0]; } +inline ALfloat do_lerp(const InterpState&, const ALfloat *RESTRICT vals, const ALsizei frac) +{ return lerp(vals[0], vals[1], frac * (1.0f/FRACTIONONE)); } +inline ALfloat do_cubic(const InterpState&, const ALfloat *RESTRICT vals, const ALsizei frac) +{ return cubic(vals[0], vals[1], vals[2], vals[3], frac * (1.0f/FRACTIONONE)); } +inline ALfloat do_bsinc(const InterpState &istate, const ALfloat *RESTRICT vals, const ALsizei frac) +{ + ASSUME(istate.bsinc.m > 0); + + // Calculate the phase index and factor. +#define FRAC_PHASE_BITDIFF (FRACTIONBITS-BSINC_PHASE_BITS) + const ALsizei pi{frac >> FRAC_PHASE_BITDIFF}; + const ALfloat pf{(frac & ((1<<FRAC_PHASE_BITDIFF)-1)) * (1.0f/(1<<FRAC_PHASE_BITDIFF))}; +#undef FRAC_PHASE_BITDIFF + + const ALfloat *fil{istate.bsinc.filter + istate.bsinc.m*pi*4}; + const ALfloat *scd{fil + istate.bsinc.m}; + const ALfloat *phd{scd + istate.bsinc.m}; + const ALfloat *spd{phd + istate.bsinc.m}; + + // Apply the scale and phase interpolated filter. + ALfloat r{0.0f}; + for(ALsizei j_f{0};j_f < istate.bsinc.m;j_f++) + r += (fil[j_f] + istate.bsinc.sf*scd[j_f] + pf*(phd[j_f] + istate.bsinc.sf*spd[j_f])) * vals[j_f]; + return r; +} + +using SamplerT = ALfloat(const InterpState&, const ALfloat*RESTRICT, const ALsizei); +template<SamplerT &Sampler> +const ALfloat *DoResample(const InterpState *state, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei numsamples) +{ + ASSUME(numsamples > 0); + ASSUME(increment > 0); + ASSUME(frac >= 0); + + const InterpState istate{*state}; + auto proc_sample = [&src,&frac,istate,increment]() -> ALfloat + { + const ALfloat ret{Sampler(istate, src, frac)}; + + frac += increment; + src += frac>>FRACTIONBITS; + frac &= FRACTIONMASK; + + return ret; + }; + std::generate_n(dst, numsamples, proc_sample); + + return dst; +} + +} // namespace + +template<> +const ALfloat *Resample_<CopyTag,CTag>(const InterpState*, const ALfloat *RESTRICT src, ALsizei, + ALint, ALfloat *RESTRICT dst, ALsizei dstlen) +{ + ASSUME(dstlen > 0); +#if defined(HAVE_SSE) || defined(HAVE_NEON) + /* Avoid copying the source data if it's aligned like the destination. */ + if((reinterpret_cast<intptr_t>(src)&15) == (reinterpret_cast<intptr_t>(dst)&15)) + return src; +#endif + std::copy_n(src, dstlen, dst); + return dst; +} + +template<> +const ALfloat *Resample_<PointTag,CTag>(const InterpState *state, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ return DoResample<do_point>(state, src, frac, increment, dst, dstlen); } + +template<> +const ALfloat *Resample_<LerpTag,CTag>(const InterpState *state, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ return DoResample<do_lerp>(state, src, frac, increment, dst, dstlen); } + +template<> +const ALfloat *Resample_<CubicTag,CTag>(const InterpState *state, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ return DoResample<do_cubic>(state, src-1, frac, increment, dst, dstlen); } + +template<> +const ALfloat *Resample_<BSincTag,CTag>(const InterpState *state, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ return DoResample<do_bsinc>(state, src-state->bsinc.l, frac, increment, dst, dstlen); } + + +static inline void ApplyCoeffs(ALsizei /*Offset*/, float2 *RESTRICT Values, const ALsizei IrSize, + const HrirArray<ALfloat> &Coeffs, const ALfloat left, const ALfloat right) +{ + ASSUME(IrSize >= 2); + for(ALsizei c{0};c < IrSize;++c) + { + Values[c][0] += Coeffs[c][0] * left; + Values[c][1] += Coeffs[c][1] * right; + } +} + +template<> +void MixHrtf_<CTag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + MixHrtfFilter *hrtfparams, const ALsizei BufferSize) +{ + MixHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, OutPos, IrSize, + hrtfparams, BufferSize); +} + +template<> +void MixHrtfBlend_<CTag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + const HrtfFilter *oldparams, MixHrtfFilter *newparams, const ALsizei BufferSize) +{ + MixHrtfBlendBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, OutPos, IrSize, + oldparams, newparams, BufferSize); +} + +template<> +void MixDirectHrtf_<CTag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples, DirectHrtfState *State, + const ALsizei BufferSize) +{ + MixDirectHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, State, BufferSize); +} + + +template<> +void Mix_<CTag>(const ALfloat *data, const al::span<FloatBufferLine> OutBuffer, + ALfloat *CurrentGains, const ALfloat *TargetGains, const ALsizei Counter, const ALsizei OutPos, + const ALsizei BufferSize) +{ + ASSUME(BufferSize > 0); + + const ALfloat delta{(Counter > 0) ? 1.0f / static_cast<ALfloat>(Counter) : 0.0f}; + for(FloatBufferLine &output : OutBuffer) + { + ALfloat *RESTRICT dst{output.data()+OutPos}; + ALfloat gain{*CurrentGains}; + const ALfloat diff{*TargetGains - gain}; + + ALsizei pos{0}; + if(std::fabs(diff) > std::numeric_limits<float>::epsilon()) + { + ALsizei minsize{mini(BufferSize, Counter)}; + const ALfloat step{diff * delta}; + ALfloat step_count{0.0f}; + for(;pos < minsize;pos++) + { + dst[pos] += data[pos] * (gain + step*step_count); + step_count += 1.0f; + } + if(pos == Counter) + gain = *TargetGains; + else + gain += step*step_count; + *CurrentGains = gain; + } + ++CurrentGains; + ++TargetGains; + + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + for(;pos < BufferSize;pos++) + dst[pos] += data[pos]*gain; + } +} + +/* Basically the inverse of the above. Rather than one input going to multiple + * outputs (each with its own gain), it's multiple inputs (each with its own + * gain) going to one output. This applies one row (vs one column) of a matrix + * transform. And as the matrices are more or less static once set up, no + * stepping is necessary. + */ +template<> +void MixRow_<CTag>(FloatBufferLine &OutBuffer, const ALfloat *Gains, + const al::span<const FloatBufferLine> InSamples, const ALsizei InPos, const ALsizei BufferSize) +{ + ASSUME(BufferSize > 0); + + for(const FloatBufferLine &input : InSamples) + { + const ALfloat *RESTRICT src{input.data()+InPos}; + const ALfloat gain{*(Gains++)}; + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + + for(ALsizei i{0};i < BufferSize;i++) + OutBuffer[i] += src[i] * gain; + } +} diff --git a/alc/mixer/mixer_neon.cpp b/alc/mixer/mixer_neon.cpp new file mode 100644 index 00000000..fa487d97 --- /dev/null +++ b/alc/mixer/mixer_neon.cpp @@ -0,0 +1,307 @@ +#include "config.h" + +#include <arm_neon.h> + +#include <limits> + +#include "AL/al.h" +#include "AL/alc.h" +#include "alcmain.h" +#include "alu.h" +#include "hrtf.h" +#include "defs.h" +#include "hrtfbase.h" + + + +template<> +const ALfloat *Resample_<LerpTag,NEONTag>(const InterpState*, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ + const int32x4_t increment4 = vdupq_n_s32(increment*4); + const float32x4_t fracOne4 = vdupq_n_f32(1.0f/FRACTIONONE); + const int32x4_t fracMask4 = vdupq_n_s32(FRACTIONMASK); + alignas(16) ALsizei pos_[4], frac_[4]; + int32x4_t pos4, frac4; + ALsizei todo, pos, i; + + ASSUME(frac >= 0); + ASSUME(increment > 0); + ASSUME(dstlen > 0); + + InitiatePositionArrays(frac, increment, frac_, pos_, 4); + frac4 = vld1q_s32(frac_); + pos4 = vld1q_s32(pos_); + + todo = dstlen & ~3; + for(i = 0;i < todo;i += 4) + { + const int pos0 = vgetq_lane_s32(pos4, 0); + const int pos1 = vgetq_lane_s32(pos4, 1); + const int pos2 = vgetq_lane_s32(pos4, 2); + const int pos3 = vgetq_lane_s32(pos4, 3); + const float32x4_t val1 = (float32x4_t){src[pos0], src[pos1], src[pos2], src[pos3]}; + const float32x4_t val2 = (float32x4_t){src[pos0+1], src[pos1+1], src[pos2+1], src[pos3+1]}; + + /* val1 + (val2-val1)*mu */ + const float32x4_t r0 = vsubq_f32(val2, val1); + const float32x4_t mu = vmulq_f32(vcvtq_f32_s32(frac4), fracOne4); + const float32x4_t out = vmlaq_f32(val1, mu, r0); + + vst1q_f32(&dst[i], out); + + frac4 = vaddq_s32(frac4, increment4); + pos4 = vaddq_s32(pos4, vshrq_n_s32(frac4, FRACTIONBITS)); + frac4 = vandq_s32(frac4, fracMask4); + } + + /* NOTE: These four elements represent the position *after* the last four + * samples, so the lowest element is the next position to resample. + */ + pos = vgetq_lane_s32(pos4, 0); + frac = vgetq_lane_s32(frac4, 0); + + for(;i < dstlen;++i) + { + dst[i] = lerp(src[pos], src[pos+1], frac * (1.0f/FRACTIONONE)); + + frac += increment; + pos += frac>>FRACTIONBITS; + frac &= FRACTIONMASK; + } + return dst; +} + +template<> +const ALfloat *Resample_<BSincTag,NEONTag>(const InterpState *state, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ + const ALfloat *const filter = state->bsinc.filter; + const float32x4_t sf4 = vdupq_n_f32(state->bsinc.sf); + const ALsizei m = state->bsinc.m; + const float32x4_t *fil, *scd, *phd, *spd; + ALsizei pi, i, j, offset; + float32x4_t r4; + ALfloat pf; + + ASSUME(m > 0); + ASSUME(dstlen > 0); + ASSUME(increment > 0); + ASSUME(frac >= 0); + + src -= state->bsinc.l; + for(i = 0;i < dstlen;i++) + { + // Calculate the phase index and factor. +#define FRAC_PHASE_BITDIFF (FRACTIONBITS-BSINC_PHASE_BITS) + pi = frac >> FRAC_PHASE_BITDIFF; + pf = (frac & ((1<<FRAC_PHASE_BITDIFF)-1)) * (1.0f/(1<<FRAC_PHASE_BITDIFF)); +#undef FRAC_PHASE_BITDIFF + + offset = m*pi*4; + fil = (const float32x4_t*)(filter + offset); offset += m; + scd = (const float32x4_t*)(filter + offset); offset += m; + phd = (const float32x4_t*)(filter + offset); offset += m; + spd = (const float32x4_t*)(filter + offset); + + // Apply the scale and phase interpolated filter. + r4 = vdupq_n_f32(0.0f); + { + const ALsizei count = m >> 2; + const float32x4_t pf4 = vdupq_n_f32(pf); + + ASSUME(count > 0); + + for(j = 0;j < count;j++) + { + /* f = ((fil + sf*scd) + pf*(phd + sf*spd)) */ + const float32x4_t f4 = vmlaq_f32( + vmlaq_f32(fil[j], sf4, scd[j]), + pf4, vmlaq_f32(phd[j], sf4, spd[j]) + ); + /* r += f*src */ + r4 = vmlaq_f32(r4, f4, vld1q_f32(&src[j*4])); + } + } + r4 = vaddq_f32(r4, vcombine_f32(vrev64_f32(vget_high_f32(r4)), + vrev64_f32(vget_low_f32(r4)))); + dst[i] = vget_lane_f32(vadd_f32(vget_low_f32(r4), vget_high_f32(r4)), 0); + + frac += increment; + src += frac>>FRACTIONBITS; + frac &= FRACTIONMASK; + } + return dst; +} + + +static inline void ApplyCoeffs(ALsizei /*Offset*/, float2 *RESTRICT Values, const ALsizei IrSize, + const HrirArray<ALfloat> &Coeffs, const ALfloat left, const ALfloat right) +{ + ASSUME(IrSize >= 2); + + float32x4_t leftright4; + { + float32x2_t leftright2 = vdup_n_f32(0.0); + leftright2 = vset_lane_f32(left, leftright2, 0); + leftright2 = vset_lane_f32(right, leftright2, 1); + leftright4 = vcombine_f32(leftright2, leftright2); + } + + for(ALsizei c{0};c < IrSize;c += 2) + { + float32x4_t vals = vcombine_f32(vld1_f32((float32_t*)&Values[c ][0]), + vld1_f32((float32_t*)&Values[c+1][0])); + float32x4_t coefs = vld1q_f32((float32_t*)&Coeffs[c][0]); + + vals = vmlaq_f32(vals, coefs, leftright4); + + vst1_f32((float32_t*)&Values[c ][0], vget_low_f32(vals)); + vst1_f32((float32_t*)&Values[c+1][0], vget_high_f32(vals)); + } +} + +template<> +void MixHrtf_<NEONTag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + MixHrtfFilter *hrtfparams, const ALsizei BufferSize) +{ + MixHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, OutPos, IrSize, + hrtfparams, BufferSize); +} + +template<> +void MixHrtfBlend_<NEONTag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + const HrtfFilter *oldparams, MixHrtfFilter *newparams, const ALsizei BufferSize) +{ + MixHrtfBlendBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, OutPos, IrSize, + oldparams, newparams, BufferSize); +} + +template<> +void MixDirectHrtf_<NEONTag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples, DirectHrtfState *State, + const ALsizei BufferSize) +{ + MixDirectHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, State, BufferSize); +} + + +template<> +void Mix_<NEONTag>(const ALfloat *data, const al::span<FloatBufferLine> OutBuffer, + ALfloat *CurrentGains, const ALfloat *TargetGains, const ALsizei Counter, const ALsizei OutPos, + const ALsizei BufferSize) +{ + ASSUME(BufferSize > 0); + + const ALfloat delta{(Counter > 0) ? 1.0f/(ALfloat)Counter : 0.0f}; + for(FloatBufferLine &output : OutBuffer) + { + ALfloat *RESTRICT dst{al::assume_aligned<16>(output.data()+OutPos)}; + ALfloat gain{*CurrentGains}; + const ALfloat diff{*TargetGains - gain}; + + ALsizei pos{0}; + if(std::fabs(diff) > std::numeric_limits<float>::epsilon()) + { + ALsizei minsize{mini(BufferSize, Counter)}; + const ALfloat step{diff * delta}; + ALfloat step_count{0.0f}; + /* Mix with applying gain steps in aligned multiples of 4. */ + if(LIKELY(minsize > 3)) + { + const float32x4_t four4{vdupq_n_f32(4.0f)}; + const float32x4_t step4{vdupq_n_f32(step)}; + const float32x4_t gain4{vdupq_n_f32(gain)}; + float32x4_t step_count4{vsetq_lane_f32(0.0f, + vsetq_lane_f32(1.0f, + vsetq_lane_f32(2.0f, + vsetq_lane_f32(3.0f, vdupq_n_f32(0.0f), 3), + 2), 1), 0 + )}; + ALsizei todo{minsize >> 2}; + + do { + const float32x4_t val4 = vld1q_f32(&data[pos]); + float32x4_t dry4 = vld1q_f32(&dst[pos]); + dry4 = vmlaq_f32(dry4, val4, vmlaq_f32(gain4, step4, step_count4)); + step_count4 = vaddq_f32(step_count4, four4); + vst1q_f32(&dst[pos], dry4); + pos += 4; + } while(--todo); + /* NOTE: step_count4 now represents the next four counts after + * the last four mixed samples, so the lowest element + * represents the next step count to apply. + */ + step_count = vgetq_lane_f32(step_count4, 0); + } + /* Mix with applying left over gain steps that aren't aligned multiples of 4. */ + for(;pos < minsize;pos++) + { + dst[pos] += data[pos]*(gain + step*step_count); + step_count += 1.0f; + } + if(pos == Counter) + gain = *TargetGains; + else + gain += step*step_count; + *CurrentGains = gain; + + /* Mix until pos is aligned with 4 or the mix is done. */ + minsize = mini(BufferSize, (pos+3)&~3); + for(;pos < minsize;pos++) + dst[pos] += data[pos]*gain; + } + ++CurrentGains; + ++TargetGains; + + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + if(LIKELY(BufferSize-pos > 3)) + { + ALsizei todo{(BufferSize-pos) >> 2}; + const float32x4_t gain4 = vdupq_n_f32(gain); + do { + const float32x4_t val4 = vld1q_f32(&data[pos]); + float32x4_t dry4 = vld1q_f32(&dst[pos]); + dry4 = vmlaq_f32(dry4, val4, gain4); + vst1q_f32(&dst[pos], dry4); + pos += 4; + } while(--todo); + } + for(;pos < BufferSize;pos++) + dst[pos] += data[pos]*gain; + } +} + +template<> +void MixRow_<NEONTag>(FloatBufferLine &OutBuffer, const ALfloat *Gains, + const al::span<const FloatBufferLine> InSamples, const ALsizei InPos, const ALsizei BufferSize) +{ + ASSUME(BufferSize > 0); + + for(const FloatBufferLine &input : InSamples) + { + const ALfloat *RESTRICT src{al::assume_aligned<16>(input.data()+InPos)}; + const ALfloat gain{*(Gains++)}; + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + + ALsizei pos{0}; + if(LIKELY(BufferSize > 3)) + { + ALsizei todo{BufferSize >> 2}; + float32x4_t gain4{vdupq_n_f32(gain)}; + do { + const float32x4_t val4 = vld1q_f32(&src[pos]); + float32x4_t dry4 = vld1q_f32(&OutBuffer[pos]); + dry4 = vmlaq_f32(dry4, val4, gain4); + vst1q_f32(&OutBuffer[pos], dry4); + pos += 4; + } while(--todo); + } + for(;pos < BufferSize;pos++) + OutBuffer[pos] += src[pos]*gain; + } +} diff --git a/alc/mixer/mixer_sse.cpp b/alc/mixer/mixer_sse.cpp new file mode 100644 index 00000000..b763fdbd --- /dev/null +++ b/alc/mixer/mixer_sse.cpp @@ -0,0 +1,262 @@ +#include "config.h" + +#include <xmmintrin.h> + +#include <limits> + +#include "AL/al.h" +#include "AL/alc.h" +#include "alcmain.h" +#include "alu.h" + +#include "alSource.h" +#include "alAuxEffectSlot.h" +#include "defs.h" +#include "hrtfbase.h" + + +template<> +const ALfloat *Resample_<BSincTag,SSETag>(const InterpState *state, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ + const ALfloat *const filter{state->bsinc.filter}; + const __m128 sf4{_mm_set1_ps(state->bsinc.sf)}; + const ALsizei m{state->bsinc.m}; + + ASSUME(m > 0); + ASSUME(dstlen > 0); + ASSUME(increment > 0); + ASSUME(frac >= 0); + + src -= state->bsinc.l; + for(ALsizei i{0};i < dstlen;i++) + { + // Calculate the phase index and factor. +#define FRAC_PHASE_BITDIFF (FRACTIONBITS-BSINC_PHASE_BITS) + const ALsizei pi{frac >> FRAC_PHASE_BITDIFF}; + const ALfloat pf{(frac & ((1<<FRAC_PHASE_BITDIFF)-1)) * (1.0f/(1<<FRAC_PHASE_BITDIFF))}; +#undef FRAC_PHASE_BITDIFF + + ALsizei offset{m*pi*4}; + const __m128 *fil{reinterpret_cast<const __m128*>(filter + offset)}; offset += m; + const __m128 *scd{reinterpret_cast<const __m128*>(filter + offset)}; offset += m; + const __m128 *phd{reinterpret_cast<const __m128*>(filter + offset)}; offset += m; + const __m128 *spd{reinterpret_cast<const __m128*>(filter + offset)}; + + // Apply the scale and phase interpolated filter. + __m128 r4{_mm_setzero_ps()}; + { + const ALsizei count{m >> 2}; + const __m128 pf4{_mm_set1_ps(pf)}; + + ASSUME(count > 0); + +#define MLA4(x, y, z) _mm_add_ps(x, _mm_mul_ps(y, z)) + for(ALsizei j{0};j < count;j++) + { + /* f = ((fil + sf*scd) + pf*(phd + sf*spd)) */ + const __m128 f4 = MLA4( + MLA4(fil[j], sf4, scd[j]), + pf4, MLA4(phd[j], sf4, spd[j]) + ); + /* r += f*src */ + r4 = MLA4(r4, f4, _mm_loadu_ps(&src[j*4])); + } +#undef MLA4 + } + r4 = _mm_add_ps(r4, _mm_shuffle_ps(r4, r4, _MM_SHUFFLE(0, 1, 2, 3))); + r4 = _mm_add_ps(r4, _mm_movehl_ps(r4, r4)); + dst[i] = _mm_cvtss_f32(r4); + + frac += increment; + src += frac>>FRACTIONBITS; + frac &= FRACTIONMASK; + } + return dst; +} + + +static inline void ApplyCoeffs(ALsizei Offset, float2 *RESTRICT Values, const ALsizei IrSize, + const HrirArray<ALfloat> &Coeffs, const ALfloat left, const ALfloat right) +{ + const __m128 lrlr{_mm_setr_ps(left, right, left, right)}; + + ASSUME(IrSize >= 2); + + if((Offset&1)) + { + __m128 imp0, imp1; + __m128 coeffs{_mm_load_ps(&Coeffs[0][0])}; + __m128 vals{_mm_loadl_pi(_mm_setzero_ps(), reinterpret_cast<__m64*>(&Values[0][0]))}; + imp0 = _mm_mul_ps(lrlr, coeffs); + vals = _mm_add_ps(imp0, vals); + _mm_storel_pi(reinterpret_cast<__m64*>(&Values[0][0]), vals); + ALsizei i{1}; + for(;i < IrSize-1;i += 2) + { + coeffs = _mm_load_ps(&Coeffs[i+1][0]); + vals = _mm_load_ps(&Values[i][0]); + imp1 = _mm_mul_ps(lrlr, coeffs); + imp0 = _mm_shuffle_ps(imp0, imp1, _MM_SHUFFLE(1, 0, 3, 2)); + vals = _mm_add_ps(imp0, vals); + _mm_store_ps(&Values[i][0], vals); + imp0 = imp1; + } + vals = _mm_loadl_pi(vals, reinterpret_cast<__m64*>(&Values[i][0])); + imp0 = _mm_movehl_ps(imp0, imp0); + vals = _mm_add_ps(imp0, vals); + _mm_storel_pi(reinterpret_cast<__m64*>(&Values[i][0]), vals); + } + else + { + for(ALsizei i{0};i < IrSize;i += 2) + { + __m128 coeffs{_mm_load_ps(&Coeffs[i][0])}; + __m128 vals{_mm_load_ps(&Values[i][0])}; + vals = _mm_add_ps(vals, _mm_mul_ps(lrlr, coeffs)); + _mm_store_ps(&Values[i][0], vals); + } + } +} + +template<> +void MixHrtf_<SSETag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + MixHrtfFilter *hrtfparams, const ALsizei BufferSize) +{ + MixHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, OutPos, IrSize, + hrtfparams, BufferSize); +} + +template<> +void MixHrtfBlend_<SSETag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const ALfloat *InSamples, float2 *AccumSamples, const ALsizei OutPos, const ALsizei IrSize, + const HrtfFilter *oldparams, MixHrtfFilter *newparams, const ALsizei BufferSize) +{ + MixHrtfBlendBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, OutPos, IrSize, + oldparams, newparams, BufferSize); +} + +template<> +void MixDirectHrtf_<SSETag>(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, + const al::span<const FloatBufferLine> InSamples, float2 *AccumSamples, DirectHrtfState *State, + const ALsizei BufferSize) +{ + MixDirectHrtfBase<ApplyCoeffs>(LeftOut, RightOut, InSamples, AccumSamples, State, BufferSize); +} + + +template<> +void Mix_<SSETag>(const ALfloat *data, const al::span<FloatBufferLine> OutBuffer, + ALfloat *CurrentGains, const ALfloat *TargetGains, const ALsizei Counter, const ALsizei OutPos, + const ALsizei BufferSize) +{ + ASSUME(BufferSize > 0); + + const ALfloat delta{(Counter > 0) ? 1.0f / static_cast<ALfloat>(Counter) : 0.0f}; + for(FloatBufferLine &output : OutBuffer) + { + ALfloat *RESTRICT dst{al::assume_aligned<16>(output.data()+OutPos)}; + ALfloat gain{*CurrentGains}; + const ALfloat diff{*TargetGains - gain}; + + ALsizei pos{0}; + if(std::fabs(diff) > std::numeric_limits<float>::epsilon()) + { + ALsizei minsize{mini(BufferSize, Counter)}; + const ALfloat step{diff * delta}; + ALfloat step_count{0.0f}; + /* Mix with applying gain steps in aligned multiples of 4. */ + if(LIKELY(minsize > 3)) + { + const __m128 four4{_mm_set1_ps(4.0f)}; + const __m128 step4{_mm_set1_ps(step)}; + const __m128 gain4{_mm_set1_ps(gain)}; + __m128 step_count4{_mm_setr_ps(0.0f, 1.0f, 2.0f, 3.0f)}; + ALsizei todo{minsize >> 2}; + do { + const __m128 val4{_mm_load_ps(&data[pos])}; + __m128 dry4{_mm_load_ps(&dst[pos])}; +#define MLA4(x, y, z) _mm_add_ps(x, _mm_mul_ps(y, z)) + /* dry += val * (gain + step*step_count) */ + dry4 = MLA4(dry4, val4, MLA4(gain4, step4, step_count4)); +#undef MLA4 + _mm_store_ps(&dst[pos], dry4); + step_count4 = _mm_add_ps(step_count4, four4); + pos += 4; + } while(--todo); + /* NOTE: step_count4 now represents the next four counts after + * the last four mixed samples, so the lowest element + * represents the next step count to apply. + */ + step_count = _mm_cvtss_f32(step_count4); + } + /* Mix with applying left over gain steps that aren't aligned multiples of 4. */ + for(;pos < minsize;pos++) + { + dst[pos] += data[pos]*(gain + step*step_count); + step_count += 1.0f; + } + if(pos == Counter) + gain = *TargetGains; + else + gain += step*step_count; + *CurrentGains = gain; + + /* Mix until pos is aligned with 4 or the mix is done. */ + minsize = mini(BufferSize, (pos+3)&~3); + for(;pos < minsize;pos++) + dst[pos] += data[pos]*gain; + } + ++CurrentGains; + ++TargetGains; + + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + if(LIKELY(BufferSize-pos > 3)) + { + ALsizei todo{(BufferSize-pos) >> 2}; + const __m128 gain4{_mm_set1_ps(gain)}; + do { + const __m128 val4{_mm_load_ps(&data[pos])}; + __m128 dry4{_mm_load_ps(&dst[pos])}; + dry4 = _mm_add_ps(dry4, _mm_mul_ps(val4, gain4)); + _mm_store_ps(&dst[pos], dry4); + pos += 4; + } while(--todo); + } + for(;pos < BufferSize;pos++) + dst[pos] += data[pos]*gain; + } +} + +template<> +void MixRow_<SSETag>(FloatBufferLine &OutBuffer, const ALfloat *Gains, + const al::span<const FloatBufferLine> InSamples, const ALsizei InPos, const ALsizei BufferSize) +{ + ASSUME(BufferSize > 0); + + for(const FloatBufferLine &input : InSamples) + { + const ALfloat *RESTRICT src{al::assume_aligned<16>(input.data()+InPos)}; + const ALfloat gain{*(Gains++)}; + if(!(std::fabs(gain) > GAIN_SILENCE_THRESHOLD)) + continue; + + ALsizei pos{0}; + if(LIKELY(BufferSize > 3)) + { + ALsizei todo{BufferSize >> 2}; + const __m128 gain4 = _mm_set1_ps(gain); + do { + const __m128 val4{_mm_load_ps(&src[pos])}; + __m128 dry4{_mm_load_ps(&OutBuffer[pos])}; + dry4 = _mm_add_ps(dry4, _mm_mul_ps(val4, gain4)); + _mm_store_ps(&OutBuffer[pos], dry4); + pos += 4; + } while(--todo); + } + for(;pos < BufferSize;pos++) + OutBuffer[pos] += src[pos]*gain; + } +} diff --git a/alc/mixer/mixer_sse2.cpp b/alc/mixer/mixer_sse2.cpp new file mode 100644 index 00000000..b5d00106 --- /dev/null +++ b/alc/mixer/mixer_sse2.cpp @@ -0,0 +1,84 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2014 by Timothy Arceri <[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 <xmmintrin.h> +#include <emmintrin.h> + +#include "alu.h" +#include "defs.h" + + +template<> +const ALfloat *Resample_<LerpTag,SSE2Tag>(const InterpState*, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ + const __m128i increment4{_mm_set1_epi32(increment*4)}; + const __m128 fracOne4{_mm_set1_ps(1.0f/FRACTIONONE)}; + const __m128i fracMask4{_mm_set1_epi32(FRACTIONMASK)}; + + ASSUME(frac > 0); + ASSUME(increment > 0); + ASSUME(dstlen >= 0); + + alignas(16) ALsizei pos_[4], frac_[4]; + InitiatePositionArrays(frac, increment, frac_, pos_, 4); + __m128i frac4{_mm_setr_epi32(frac_[0], frac_[1], frac_[2], frac_[3])}; + __m128i pos4{_mm_setr_epi32(pos_[0], pos_[1], pos_[2], pos_[3])}; + + const ALsizei todo{dstlen & ~3}; + for(ALsizei i{0};i < todo;i += 4) + { + const int pos0{_mm_cvtsi128_si32(_mm_shuffle_epi32(pos4, _MM_SHUFFLE(0, 0, 0, 0)))}; + const int pos1{_mm_cvtsi128_si32(_mm_shuffle_epi32(pos4, _MM_SHUFFLE(1, 1, 1, 1)))}; + const int pos2{_mm_cvtsi128_si32(_mm_shuffle_epi32(pos4, _MM_SHUFFLE(2, 2, 2, 2)))}; + const int pos3{_mm_cvtsi128_si32(_mm_shuffle_epi32(pos4, _MM_SHUFFLE(3, 3, 3, 3)))}; + const __m128 val1{_mm_setr_ps(src[pos0 ], src[pos1 ], src[pos2 ], src[pos3 ])}; + const __m128 val2{_mm_setr_ps(src[pos0+1], src[pos1+1], src[pos2+1], src[pos3+1])}; + + /* val1 + (val2-val1)*mu */ + const __m128 r0{_mm_sub_ps(val2, val1)}; + const __m128 mu{_mm_mul_ps(_mm_cvtepi32_ps(frac4), fracOne4)}; + const __m128 out{_mm_add_ps(val1, _mm_mul_ps(mu, r0))}; + + _mm_store_ps(&dst[i], out); + + frac4 = _mm_add_epi32(frac4, increment4); + pos4 = _mm_add_epi32(pos4, _mm_srli_epi32(frac4, FRACTIONBITS)); + frac4 = _mm_and_si128(frac4, fracMask4); + } + + /* NOTE: These four elements represent the position *after* the last four + * samples, so the lowest element is the next position to resample. + */ + ALsizei pos{_mm_cvtsi128_si32(pos4)}; + frac = _mm_cvtsi128_si32(frac4); + + for(ALsizei i{todo};i < dstlen;++i) + { + dst[i] = lerp(src[pos], src[pos+1], frac * (1.0f/FRACTIONONE)); + + frac += increment; + pos += frac>>FRACTIONBITS; + frac &= FRACTIONMASK; + } + return dst; +} diff --git a/alc/mixer/mixer_sse3.cpp b/alc/mixer/mixer_sse3.cpp new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/alc/mixer/mixer_sse3.cpp diff --git a/alc/mixer/mixer_sse41.cpp b/alc/mixer/mixer_sse41.cpp new file mode 100644 index 00000000..7efbda7b --- /dev/null +++ b/alc/mixer/mixer_sse41.cpp @@ -0,0 +1,85 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 2014 by Timothy Arceri <[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 <xmmintrin.h> +#include <emmintrin.h> +#include <smmintrin.h> + +#include "alu.h" +#include "defs.h" + + +template<> +const ALfloat *Resample_<LerpTag,SSE4Tag>(const InterpState*, const ALfloat *RESTRICT src, + ALsizei frac, ALint increment, ALfloat *RESTRICT dst, ALsizei dstlen) +{ + const __m128i increment4{_mm_set1_epi32(increment*4)}; + const __m128 fracOne4{_mm_set1_ps(1.0f/FRACTIONONE)}; + const __m128i fracMask4{_mm_set1_epi32(FRACTIONMASK)}; + + ASSUME(frac > 0); + ASSUME(increment > 0); + ASSUME(dstlen >= 0); + + alignas(16) ALsizei pos_[4], frac_[4]; + InitiatePositionArrays(frac, increment, frac_, pos_, 4); + __m128i frac4{_mm_setr_epi32(frac_[0], frac_[1], frac_[2], frac_[3])}; + __m128i pos4{_mm_setr_epi32(pos_[0], pos_[1], pos_[2], pos_[3])}; + + const ALsizei todo{dstlen & ~3}; + for(ALsizei i{0};i < todo;i += 4) + { + const int pos0{_mm_extract_epi32(pos4, 0)}; + const int pos1{_mm_extract_epi32(pos4, 1)}; + const int pos2{_mm_extract_epi32(pos4, 2)}; + const int pos3{_mm_extract_epi32(pos4, 3)}; + const __m128 val1{_mm_setr_ps(src[pos0 ], src[pos1 ], src[pos2 ], src[pos3 ])}; + const __m128 val2{_mm_setr_ps(src[pos0+1], src[pos1+1], src[pos2+1], src[pos3+1])}; + + /* val1 + (val2-val1)*mu */ + const __m128 r0{_mm_sub_ps(val2, val1)}; + const __m128 mu{_mm_mul_ps(_mm_cvtepi32_ps(frac4), fracOne4)}; + const __m128 out{_mm_add_ps(val1, _mm_mul_ps(mu, r0))}; + + _mm_store_ps(&dst[i], out); + + frac4 = _mm_add_epi32(frac4, increment4); + pos4 = _mm_add_epi32(pos4, _mm_srli_epi32(frac4, FRACTIONBITS)); + frac4 = _mm_and_si128(frac4, fracMask4); + } + + /* NOTE: These four elements represent the position *after* the last four + * samples, so the lowest element is the next position to resample. + */ + ALsizei pos{_mm_cvtsi128_si32(pos4)}; + frac = _mm_cvtsi128_si32(frac4); + + for(ALsizei i{todo};i < dstlen;++i) + { + dst[i] = lerp(src[pos], src[pos+1], frac * (1.0f/FRACTIONONE)); + + frac += increment; + pos += frac>>FRACTIONBITS; + frac &= FRACTIONMASK; + } + return dst; +} diff --git a/alc/mixvoice.cpp b/alc/mixvoice.cpp new file mode 100644 index 00000000..be872f6d --- /dev/null +++ b/alc/mixvoice.cpp @@ -0,0 +1,954 @@ +/** + * 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 <algorithm> +#include <array> +#include <atomic> +#include <cassert> +#include <climits> +#include <cstddef> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <iterator> +#include <memory> +#include <new> +#include <numeric> +#include <string> +#include <utility> + +#include "AL/al.h" +#include "AL/alc.h" + +#include "alBuffer.h" +#include "alcmain.h" +#include "alSource.h" +#include "albyte.h" +#include "alconfig.h" +#include "alcontext.h" +#include "alnumeric.h" +#include "aloptional.h" +#include "alspan.h" +#include "alu.h" +#include "cpu_caps.h" +#include "filters/biquad.h" +#include "filters/nfc.h" +#include "filters/splitter.h" +#include "hrtf.h" +#include "inprogext.h" +#include "logging.h" +#include "mixer/defs.h" +#include "opthelpers.h" +#include "ringbuffer.h" +#include "threads.h" +#include "vector.h" + + +static_assert((INT_MAX>>FRACTIONBITS)/MAX_PITCH > BUFFERSIZE, + "MAX_PITCH and/or BUFFERSIZE are too large for FRACTIONBITS!"); + +/* BSinc24 requires up to 23 extra samples before the current position, and 24 after. */ +static_assert(MAX_RESAMPLE_PADDING >= 24, "MAX_RESAMPLE_PADDING must be at least 24!"); + + +Resampler ResamplerDefault = LinearResampler; + +MixerFunc MixSamples = Mix_<CTag>; +RowMixerFunc MixRowSamples = MixRow_<CTag>; +static HrtfMixerFunc MixHrtfSamples = MixHrtf_<CTag>; +static HrtfMixerBlendFunc MixHrtfBlendSamples = MixHrtfBlend_<CTag>; + +static MixerFunc SelectMixer() +{ +#ifdef HAVE_NEON + if((CPUCapFlags&CPU_CAP_NEON)) + return Mix_<NEONTag>; +#endif +#ifdef HAVE_SSE + if((CPUCapFlags&CPU_CAP_SSE)) + return Mix_<SSETag>; +#endif + return Mix_<CTag>; +} + +static RowMixerFunc SelectRowMixer() +{ +#ifdef HAVE_NEON + if((CPUCapFlags&CPU_CAP_NEON)) + return MixRow_<NEONTag>; +#endif +#ifdef HAVE_SSE + if((CPUCapFlags&CPU_CAP_SSE)) + return MixRow_<SSETag>; +#endif + return MixRow_<CTag>; +} + +static inline HrtfMixerFunc SelectHrtfMixer() +{ +#ifdef HAVE_NEON + if((CPUCapFlags&CPU_CAP_NEON)) + return MixHrtf_<NEONTag>; +#endif +#ifdef HAVE_SSE + if((CPUCapFlags&CPU_CAP_SSE)) + return MixHrtf_<SSETag>; +#endif + return MixHrtf_<CTag>; +} + +static inline HrtfMixerBlendFunc SelectHrtfBlendMixer() +{ +#ifdef HAVE_NEON + if((CPUCapFlags&CPU_CAP_NEON)) + return MixHrtfBlend_<NEONTag>; +#endif +#ifdef HAVE_SSE + if((CPUCapFlags&CPU_CAP_SSE)) + return MixHrtfBlend_<SSETag>; +#endif + return MixHrtfBlend_<CTag>; +} + +ResamplerFunc SelectResampler(Resampler resampler) +{ + switch(resampler) + { + case PointResampler: + return Resample_<PointTag,CTag>; + case LinearResampler: +#ifdef HAVE_NEON + if((CPUCapFlags&CPU_CAP_NEON)) + return Resample_<LerpTag,NEONTag>; +#endif +#ifdef HAVE_SSE4_1 + if((CPUCapFlags&CPU_CAP_SSE4_1)) + return Resample_<LerpTag,SSE4Tag>; +#endif +#ifdef HAVE_SSE2 + if((CPUCapFlags&CPU_CAP_SSE2)) + return Resample_<LerpTag,SSE2Tag>; +#endif + return Resample_<LerpTag,CTag>; + case FIR4Resampler: + return Resample_<CubicTag,CTag>; + case BSinc12Resampler: + case BSinc24Resampler: +#ifdef HAVE_NEON + if((CPUCapFlags&CPU_CAP_NEON)) + return Resample_<BSincTag,NEONTag>; +#endif +#ifdef HAVE_SSE + if((CPUCapFlags&CPU_CAP_SSE)) + return Resample_<BSincTag,SSETag>; +#endif + return Resample_<BSincTag,CTag>; + } + + return Resample_<PointTag,CTag>; +} + + +void aluInitMixer() +{ + if(auto resopt = ConfigValueStr(nullptr, nullptr, "resampler")) + { + const char *str{resopt->c_str()}; + if(strcasecmp(str, "point") == 0 || strcasecmp(str, "none") == 0) + ResamplerDefault = PointResampler; + else if(strcasecmp(str, "linear") == 0) + ResamplerDefault = LinearResampler; + else if(strcasecmp(str, "cubic") == 0) + ResamplerDefault = FIR4Resampler; + else if(strcasecmp(str, "bsinc12") == 0) + ResamplerDefault = BSinc12Resampler; + else if(strcasecmp(str, "bsinc24") == 0) + ResamplerDefault = BSinc24Resampler; + else if(strcasecmp(str, "bsinc") == 0) + { + WARN("Resampler option \"%s\" is deprecated, using bsinc12\n", str); + ResamplerDefault = BSinc12Resampler; + } + else if(strcasecmp(str, "sinc4") == 0 || strcasecmp(str, "sinc8") == 0) + { + WARN("Resampler option \"%s\" is deprecated, using cubic\n", str); + ResamplerDefault = FIR4Resampler; + } + else + { + char *end; + long n = strtol(str, &end, 0); + if(*end == '\0' && (n == PointResampler || n == LinearResampler || n == FIR4Resampler)) + ResamplerDefault = static_cast<Resampler>(n); + else + WARN("Invalid resampler: %s\n", str); + } + } + + MixHrtfBlendSamples = SelectHrtfBlendMixer(); + MixHrtfSamples = SelectHrtfMixer(); + MixSamples = SelectMixer(); + MixRowSamples = SelectRowMixer(); +} + + +namespace { + +/* A quick'n'dirty lookup table to decode a muLaw-encoded byte sample into a + * signed 16-bit sample */ +constexpr ALshort muLawDecompressionTable[256] = { + -32124,-31100,-30076,-29052,-28028,-27004,-25980,-24956, + -23932,-22908,-21884,-20860,-19836,-18812,-17788,-16764, + -15996,-15484,-14972,-14460,-13948,-13436,-12924,-12412, + -11900,-11388,-10876,-10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, 0, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0 +}; + +/* A quick'n'dirty lookup table to decode an aLaw-encoded byte sample into a + * signed 16-bit sample */ +constexpr ALshort aLawDecompressionTable[256] = { + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, + -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, + -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, + -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, + -22016,-20992,-24064,-23040,-17920,-16896,-19968,-18944, + -30208,-29184,-32256,-31232,-26112,-25088,-28160,-27136, + -11008,-10496,-12032,-11520, -8960, -8448, -9984, -9472, + -15104,-14592,-16128,-15616,-13056,-12544,-14080,-13568, + -344, -328, -376, -360, -280, -264, -312, -296, + -472, -456, -504, -488, -408, -392, -440, -424, + -88, -72, -120, -104, -24, -8, -56, -40, + -216, -200, -248, -232, -152, -136, -184, -168, + -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, + -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, + -688, -656, -752, -720, -560, -528, -624, -592, + -944, -912, -1008, -976, -816, -784, -880, -848, + 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, + 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, + 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, + 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, + 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, + 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, + 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, + 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, + 344, 328, 376, 360, 280, 264, 312, 296, + 472, 456, 504, 488, 408, 392, 440, 424, + 88, 72, 120, 104, 24, 8, 56, 40, + 216, 200, 248, 232, 152, 136, 184, 168, + 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, + 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, + 688, 656, 752, 720, 560, 528, 624, 592, + 944, 912, 1008, 976, 816, 784, 880, 848 +}; + + +void SendSourceStoppedEvent(ALCcontext *context, ALuint id) +{ + ALbitfieldSOFT enabledevt{context->EnabledEvts.load(std::memory_order_acquire)}; + if(!(enabledevt&EventType_SourceStateChange)) return; + + RingBuffer *ring{context->AsyncEvents.get()}; + auto evt_vec = ring->getWriteVector(); + if(evt_vec.first.len < 1) return; + + AsyncEvent *evt{new (evt_vec.first.buf) AsyncEvent{EventType_SourceStateChange}}; + evt->u.srcstate.id = id; + evt->u.srcstate.state = AL_STOPPED; + + ring->writeAdvance(1); + context->EventSem.post(); +} + + +const ALfloat *DoFilters(BiquadFilter *lpfilter, BiquadFilter *hpfilter, ALfloat *dst, + const ALfloat *src, ALsizei numsamples, int type) +{ + switch(type) + { + case AF_None: + lpfilter->clear(); + hpfilter->clear(); + break; + + case AF_LowPass: + lpfilter->process(dst, src, numsamples); + hpfilter->clear(); + return dst; + case AF_HighPass: + lpfilter->clear(); + hpfilter->process(dst, src, numsamples); + return dst; + + case AF_BandPass: + lpfilter->process(dst, src, numsamples); + hpfilter->process(dst, dst, numsamples); + return dst; + } + return src; +} + + +/* Base template left undefined. Should be marked =delete, but Clang 3.8.1 + * chokes on that given the inline specializations. + */ +template<FmtType T> +inline ALfloat LoadSample(typename FmtTypeTraits<T>::Type val); + +template<> inline ALfloat LoadSample<FmtUByte>(FmtTypeTraits<FmtUByte>::Type val) +{ return (val-128) * (1.0f/128.0f); } +template<> inline ALfloat LoadSample<FmtShort>(FmtTypeTraits<FmtShort>::Type val) +{ return val * (1.0f/32768.0f); } +template<> inline ALfloat LoadSample<FmtFloat>(FmtTypeTraits<FmtFloat>::Type val) +{ return val; } +template<> inline ALfloat LoadSample<FmtDouble>(FmtTypeTraits<FmtDouble>::Type val) +{ return static_cast<ALfloat>(val); } +template<> inline ALfloat LoadSample<FmtMulaw>(FmtTypeTraits<FmtMulaw>::Type val) +{ return muLawDecompressionTable[val] * (1.0f/32768.0f); } +template<> inline ALfloat LoadSample<FmtAlaw>(FmtTypeTraits<FmtAlaw>::Type val) +{ return aLawDecompressionTable[val] * (1.0f/32768.0f); } + +template<FmtType T> +inline void LoadSampleArray(ALfloat *RESTRICT dst, const al::byte *src, ALint srcstep, + const ptrdiff_t samples) +{ + using SampleType = typename FmtTypeTraits<T>::Type; + + const SampleType *RESTRICT ssrc{reinterpret_cast<const SampleType*>(src)}; + for(ALsizei i{0};i < samples;i++) + dst[i] += LoadSample<T>(ssrc[i*srcstep]); +} + +void LoadSamples(ALfloat *RESTRICT dst, const al::byte *src, ALint srcstep, FmtType srctype, + const ptrdiff_t samples) +{ +#define HANDLE_FMT(T) case T: LoadSampleArray<T>(dst, src, srcstep, samples); break + switch(srctype) + { + HANDLE_FMT(FmtUByte); + HANDLE_FMT(FmtShort); + HANDLE_FMT(FmtFloat); + HANDLE_FMT(FmtDouble); + HANDLE_FMT(FmtMulaw); + HANDLE_FMT(FmtAlaw); + } +#undef HANDLE_FMT +} + +ALfloat *LoadBufferStatic(ALbufferlistitem *BufferListItem, ALbufferlistitem *&BufferLoopItem, + const ALsizei NumChannels, const ALsizei SampleSize, const ALsizei chan, ALsizei DataPosInt, + al::span<ALfloat> SrcBuffer) +{ + /* TODO: For static sources, loop points are taken from the first buffer + * (should be adjusted by any buffer offset, to possibly be added later). + */ + const ALbuffer *Buffer0{BufferListItem->buffers[0]}; + const ALsizei LoopStart{Buffer0->LoopStart}; + const ALsizei LoopEnd{Buffer0->LoopEnd}; + ASSUME(LoopStart >= 0); + ASSUME(LoopEnd > LoopStart); + + /* If current pos is beyond the loop range, do not loop */ + if(!BufferLoopItem || DataPosInt >= LoopEnd) + { + BufferLoopItem = nullptr; + + auto load_buffer = [DataPosInt,NumChannels,SampleSize,chan,SrcBuffer](size_t CompLen, const ALbuffer *buffer) -> size_t + { + if(DataPosInt >= buffer->SampleLen) + return CompLen; + + /* Load what's left to play from the buffer */ + const size_t DataSize{std::min<size_t>(SrcBuffer.size(), + buffer->SampleLen - DataPosInt)}; + CompLen = std::max(CompLen, DataSize); + + const al::byte *Data{buffer->mData.data()}; + Data += (DataPosInt*NumChannels + chan)*SampleSize; + + LoadSamples(SrcBuffer.data(), Data, NumChannels, buffer->mFmtType, DataSize); + return CompLen; + }; + /* It's impossible to have a buffer list item with no entries. */ + ASSUME(BufferListItem->num_buffers > 0); + auto buffers_end = BufferListItem->buffers + BufferListItem->num_buffers; + SrcBuffer = SrcBuffer.subspan(std::accumulate(BufferListItem->buffers, buffers_end, + size_t{0u}, load_buffer)); + } + else + { + const al::span<ALfloat> SrcData{SrcBuffer.first( + std::min<size_t>(SrcBuffer.size(), LoopEnd - DataPosInt))}; + + auto load_buffer = [DataPosInt,NumChannels,SampleSize,chan,SrcData](size_t CompLen, const ALbuffer *buffer) -> size_t + { + if(DataPosInt >= buffer->SampleLen) + return CompLen; + + /* Load what's left of this loop iteration */ + const size_t DataSize{std::min<size_t>(SrcData.size(), + buffer->SampleLen - DataPosInt)}; + CompLen = std::max(CompLen, DataSize); + + const al::byte *Data{buffer->mData.data()}; + Data += (DataPosInt*NumChannels + chan)*SampleSize; + + LoadSamples(SrcData.data(), Data, NumChannels, buffer->mFmtType, DataSize); + return CompLen; + }; + ASSUME(BufferListItem->num_buffers > 0); + auto buffers_end = BufferListItem->buffers + BufferListItem->num_buffers; + SrcBuffer = SrcBuffer.subspan(std::accumulate(BufferListItem->buffers, buffers_end, + size_t{0u}, load_buffer)); + + const auto LoopSize = static_cast<size_t>(LoopEnd - LoopStart); + while(!SrcBuffer.empty()) + { + const al::span<ALfloat> SrcData{SrcBuffer.first( + std::min<size_t>(SrcBuffer.size(), LoopSize))}; + + auto load_buffer_loop = [LoopStart,NumChannels,SampleSize,chan,SrcData](size_t CompLen, const ALbuffer *buffer) -> size_t + { + if(LoopStart >= buffer->SampleLen) + return CompLen; + + const size_t DataSize{std::min<size_t>(SrcData.size(), + buffer->SampleLen-LoopStart)}; + CompLen = std::max(CompLen, DataSize); + + const al::byte *Data{buffer->mData.data()}; + Data += (LoopStart*NumChannels + chan)*SampleSize; + + LoadSamples(SrcData.data(), Data, NumChannels, buffer->mFmtType, DataSize); + return CompLen; + }; + SrcBuffer = SrcBuffer.subspan(std::accumulate(BufferListItem->buffers, buffers_end, + size_t{0u}, load_buffer_loop)); + } + } + return SrcBuffer.begin(); +} + +ALfloat *LoadBufferQueue(ALbufferlistitem *BufferListItem, ALbufferlistitem *BufferLoopItem, + const ALsizei NumChannels, const ALsizei SampleSize, const ALsizei chan, ALsizei DataPosInt, + al::span<ALfloat> SrcBuffer) +{ + /* Crawl the buffer queue to fill in the temp buffer */ + while(BufferListItem && !SrcBuffer.empty()) + { + if(DataPosInt >= BufferListItem->max_samples) + { + DataPosInt -= BufferListItem->max_samples; + BufferListItem = BufferListItem->next.load(std::memory_order_acquire); + if(!BufferListItem) BufferListItem = BufferLoopItem; + continue; + } + + auto load_buffer = [DataPosInt,NumChannels,SampleSize,chan,SrcBuffer](size_t CompLen, const ALbuffer *buffer) -> size_t + { + if(!buffer) return CompLen; + if(DataPosInt >= buffer->SampleLen) + return CompLen; + + const size_t DataSize{std::min<size_t>(SrcBuffer.size(), buffer->SampleLen-DataPosInt)}; + CompLen = std::max(CompLen, DataSize); + + const al::byte *Data{buffer->mData.data()}; + Data += (DataPosInt*NumChannels + chan)*SampleSize; + + LoadSamples(SrcBuffer.data(), Data, NumChannels, buffer->mFmtType, DataSize); + return CompLen; + }; + ASSUME(BufferListItem->num_buffers > 0); + auto buffers_end = BufferListItem->buffers + BufferListItem->num_buffers; + SrcBuffer = SrcBuffer.subspan(std::accumulate(BufferListItem->buffers, buffers_end, + size_t{0u}, load_buffer)); + + if(SrcBuffer.empty()) + break; + DataPosInt = 0; + BufferListItem = BufferListItem->next.load(std::memory_order_acquire); + if(!BufferListItem) BufferListItem = BufferLoopItem; + } + + return SrcBuffer.begin(); +} + +} // namespace + +void MixVoice(ALvoice *voice, ALvoice::State vstate, const ALuint SourceID, ALCcontext *Context, const ALsizei SamplesToDo) +{ + static constexpr ALfloat SilentTarget[MAX_OUTPUT_CHANNELS]{}; + + ASSUME(SamplesToDo > 0); + + /* Get voice info */ + const bool isstatic{(voice->mFlags&VOICE_IS_STATIC) != 0}; + ALsizei DataPosInt{static_cast<ALsizei>(voice->mPosition.load(std::memory_order_relaxed))}; + ALsizei DataPosFrac{voice->mPositionFrac.load(std::memory_order_relaxed)}; + ALbufferlistitem *BufferListItem{voice->mCurrentBuffer.load(std::memory_order_relaxed)}; + ALbufferlistitem *BufferLoopItem{voice->mLoopBuffer.load(std::memory_order_relaxed)}; + const ALsizei NumChannels{voice->mNumChannels}; + const ALsizei SampleSize{voice->mSampleSize}; + const ALint increment{voice->mStep}; + + ASSUME(DataPosInt >= 0); + ASSUME(DataPosFrac >= 0); + ASSUME(NumChannels > 0); + ASSUME(SampleSize > 0); + ASSUME(increment > 0); + + ALCdevice *Device{Context->Device}; + const ALsizei NumSends{Device->NumAuxSends}; + const ALsizei IrSize{Device->mHrtf ? Device->mHrtf->irSize : 0}; + + ASSUME(NumSends >= 0); + ASSUME(IrSize >= 0); + + ResamplerFunc Resample{(increment == FRACTIONONE && DataPosFrac == 0) ? + Resample_<CopyTag,CTag> : voice->mResampler}; + + ALsizei Counter{(voice->mFlags&VOICE_IS_FADING) ? SamplesToDo : 0}; + if(!Counter) + { + /* No fading, just overwrite the old/current params. */ + for(ALsizei chan{0};chan < NumChannels;chan++) + { + ALvoice::ChannelData &chandata = voice->mChans[chan]; + DirectParams &parms = chandata.mDryParams; + if(!(voice->mFlags&VOICE_HAS_HRTF)) + std::copy(std::begin(parms.Gains.Target), std::end(parms.Gains.Target), + std::begin(parms.Gains.Current)); + else + parms.Hrtf.Old = parms.Hrtf.Target; + for(ALsizei send{0};send < NumSends;++send) + { + if(voice->mSend[send].Buffer.empty()) + continue; + + SendParams &parms = chandata.mWetParams[send]; + std::copy(std::begin(parms.Gains.Target), std::end(parms.Gains.Target), + std::begin(parms.Gains.Current)); + } + } + } + else if((voice->mFlags&VOICE_HAS_HRTF)) + { + for(ALsizei chan{0};chan < NumChannels;chan++) + { + DirectParams &parms = voice->mChans[chan].mDryParams; + if(!(parms.Hrtf.Old.Gain > GAIN_SILENCE_THRESHOLD)) + { + /* The old HRTF params are silent, so overwrite the old + * coefficients with the new, and reset the old gain to 0. The + * future mix will then fade from silence. + */ + parms.Hrtf.Old = parms.Hrtf.Target; + parms.Hrtf.Old.Gain = 0.0f; + } + } + } + + ALsizei buffers_done{0}; + ALsizei OutPos{0}; + do { + /* Figure out how many buffer samples will be needed */ + ALsizei DstBufferSize{SamplesToDo - OutPos}; + + /* Calculate the last written dst sample pos. */ + int64_t DataSize64{DstBufferSize - 1}; + /* Calculate the last read src sample pos. */ + DataSize64 = (DataSize64*increment + DataPosFrac) >> FRACTIONBITS; + /* +1 to get the src sample count, include padding. */ + DataSize64 += 1 + MAX_RESAMPLE_PADDING*2; + + auto SrcBufferSize = static_cast<ALuint>( + mini64(DataSize64, BUFFERSIZE + MAX_RESAMPLE_PADDING*2 + 1)); + if(SrcBufferSize > BUFFERSIZE + MAX_RESAMPLE_PADDING*2) + { + SrcBufferSize = BUFFERSIZE + MAX_RESAMPLE_PADDING*2; + /* If the source buffer got saturated, we can't fill the desired + * dst size. Figure out how many samples we can actually mix from + * this. + */ + DataSize64 = SrcBufferSize - MAX_RESAMPLE_PADDING*2; + DataSize64 = ((DataSize64<<FRACTIONBITS) - DataPosFrac + increment-1) / increment; + DstBufferSize = static_cast<ALsizei>(mini64(DataSize64, DstBufferSize)); + + /* Some mixers like having a multiple of 4, so try to give that + * unless this is the last update. + */ + if(DstBufferSize < SamplesToDo-OutPos) + DstBufferSize &= ~3; + } + + for(ALsizei chan{0};chan < NumChannels;chan++) + { + ALvoice::ChannelData &chandata = voice->mChans[chan]; + const al::span<ALfloat> SrcData{Device->SourceData, SrcBufferSize}; + + /* Load the previous samples into the source data first, and clear the rest. */ + auto srciter = std::copy_n(chandata.mPrevSamples.begin(), MAX_RESAMPLE_PADDING, + SrcData.begin()); + std::fill(srciter, SrcData.end(), 0.0f); + + if(UNLIKELY(!BufferListItem)) + srciter = std::copy(chandata.mPrevSamples.begin()+MAX_RESAMPLE_PADDING, + chandata.mPrevSamples.end(), srciter); + else if(isstatic) + srciter = LoadBufferStatic(BufferListItem, BufferLoopItem, NumChannels, + SampleSize, chan, DataPosInt, {srciter, SrcData.end()}); + else + srciter = LoadBufferQueue(BufferListItem, BufferLoopItem, NumChannels, + SampleSize, chan, DataPosInt, {srciter, SrcData.end()}); + + if(UNLIKELY(srciter != SrcData.end())) + { + /* If the source buffer wasn't filled, copy the last sample for + * the remaining buffer. Ideally it should have ended with + * silence, but if not the gain fading should help avoid clicks + * from sudden amplitude changes. + */ + const ALfloat sample{*(srciter-1)}; + std::fill(srciter, SrcData.end(), sample); + } + + /* Store the last source samples used for next time. */ + std::copy_n(&SrcData[(increment*DstBufferSize + DataPosFrac)>>FRACTIONBITS], + chandata.mPrevSamples.size(), chandata.mPrevSamples.begin()); + + /* Resample, then apply ambisonic upsampling as needed. */ + const ALfloat *ResampledData{Resample(&voice->mResampleState, + &SrcData[MAX_RESAMPLE_PADDING], DataPosFrac, increment, + Device->ResampledData, DstBufferSize)}; + if((voice->mFlags&VOICE_IS_AMBISONIC)) + { + const ALfloat hfscale{chandata.mAmbiScale}; + /* Beware the evil const_cast. It's safe since it's pointing to + * either SourceData or ResampledData (both non-const), but the + * resample method takes the source as const float* and may + * return it without copying to output, making it currently + * unavoidable. + */ + chandata.mAmbiSplitter.applyHfScale(const_cast<ALfloat*>(ResampledData), hfscale, + DstBufferSize); + } + + /* Now filter and mix to the appropriate outputs. */ + { + DirectParams &parms = chandata.mDryParams; + const ALfloat *samples{DoFilters(&parms.LowPass, &parms.HighPass, + Device->FilteredData, ResampledData, DstBufferSize, + voice->mDirect.FilterType)}; + + if((voice->mFlags&VOICE_HAS_HRTF)) + { + const int OutLIdx{GetChannelIdxByName(Device->RealOut, FrontLeft)}; + const int OutRIdx{GetChannelIdxByName(Device->RealOut, FrontRight)}; + ASSUME(OutLIdx >= 0 && OutRIdx >= 0); + + auto &HrtfSamples = Device->HrtfSourceData; + auto &AccumSamples = Device->HrtfAccumData; + const ALfloat TargetGain{UNLIKELY(vstate == ALvoice::Stopping) ? 0.0f : + parms.Hrtf.Target.Gain}; + ALsizei fademix{0}; + + /* Copy the HRTF history and new input samples into a temp + * buffer. + */ + auto src_iter = std::copy(parms.Hrtf.State.History.begin(), + parms.Hrtf.State.History.end(), std::begin(HrtfSamples)); + std::copy_n(samples, DstBufferSize, src_iter); + /* Copy the last used samples back into the history buffer + * for later. + */ + std::copy_n(std::begin(HrtfSamples) + DstBufferSize, + parms.Hrtf.State.History.size(), parms.Hrtf.State.History.begin()); + + /* Copy the current filtered values being accumulated into + * the temp buffer. + */ + auto accum_iter = std::copy_n(parms.Hrtf.State.Values.begin(), + parms.Hrtf.State.Values.size(), std::begin(AccumSamples)); + + /* Clear the accumulation buffer that will start getting + * filled in. + */ + std::fill_n(accum_iter, DstBufferSize, float2{}); + + /* If fading, the old gain is not silence, and this is the + * first mixing pass, fade between the IRs. + */ + if(Counter && (parms.Hrtf.Old.Gain > GAIN_SILENCE_THRESHOLD) && OutPos == 0) + { + fademix = mini(DstBufferSize, 128); + + ALfloat gain{TargetGain}; + + /* The new coefficients need to fade in completely + * since they're replacing the old ones. To keep the + * gain fading consistent, interpolate between the old + * and new target gains given how much of the fade time + * this mix handles. + */ + if(LIKELY(Counter > fademix)) + { + const ALfloat a{static_cast<ALfloat>(fademix) / + static_cast<ALfloat>(Counter)}; + gain = lerp(parms.Hrtf.Old.Gain, TargetGain, a); + } + MixHrtfFilter hrtfparams; + hrtfparams.Coeffs = &parms.Hrtf.Target.Coeffs; + hrtfparams.Delay[0] = parms.Hrtf.Target.Delay[0]; + hrtfparams.Delay[1] = parms.Hrtf.Target.Delay[1]; + hrtfparams.Gain = 0.0f; + hrtfparams.GainStep = gain / static_cast<ALfloat>(fademix); + + MixHrtfBlendSamples(voice->mDirect.Buffer[OutLIdx], + voice->mDirect.Buffer[OutRIdx], HrtfSamples, AccumSamples, OutPos, + IrSize, &parms.Hrtf.Old, &hrtfparams, fademix); + /* Update the old parameters with the result. */ + parms.Hrtf.Old = parms.Hrtf.Target; + if(fademix < Counter) + parms.Hrtf.Old.Gain = hrtfparams.Gain; + else + parms.Hrtf.Old.Gain = TargetGain; + } + + if(LIKELY(fademix < DstBufferSize)) + { + const ALsizei todo{DstBufferSize - fademix}; + ALfloat gain{TargetGain}; + + /* Interpolate the target gain if the gain fading lasts + * longer than this mix. + */ + if(Counter > DstBufferSize) + { + const ALfloat a{static_cast<ALfloat>(todo) / + static_cast<ALfloat>(Counter-fademix)}; + gain = lerp(parms.Hrtf.Old.Gain, TargetGain, a); + } + + MixHrtfFilter hrtfparams; + hrtfparams.Coeffs = &parms.Hrtf.Target.Coeffs; + hrtfparams.Delay[0] = parms.Hrtf.Target.Delay[0]; + hrtfparams.Delay[1] = parms.Hrtf.Target.Delay[1]; + hrtfparams.Gain = parms.Hrtf.Old.Gain; + hrtfparams.GainStep = (gain - parms.Hrtf.Old.Gain) / + static_cast<ALfloat>(todo); + MixHrtfSamples(voice->mDirect.Buffer[OutLIdx], + voice->mDirect.Buffer[OutRIdx], HrtfSamples+fademix, + AccumSamples+fademix, OutPos+fademix, IrSize, &hrtfparams, todo); + /* Store the interpolated gain or the final target gain + * depending if the fade is done. + */ + if(DstBufferSize < Counter) + parms.Hrtf.Old.Gain = gain; + else + parms.Hrtf.Old.Gain = TargetGain; + } + + /* Copy the new in-progress accumulation values back for + * the next mix. + */ + std::copy_n(std::begin(AccumSamples) + DstBufferSize, + parms.Hrtf.State.Values.size(), parms.Hrtf.State.Values.begin()); + } + else if((voice->mFlags&VOICE_HAS_NFC)) + { + const ALfloat *TargetGains{UNLIKELY(vstate == ALvoice::Stopping) ? + SilentTarget : parms.Gains.Target}; + + const size_t outcount{Device->NumChannelsPerOrder[0]}; + MixSamples(samples, voice->mDirect.Buffer.first(outcount), parms.Gains.Current, + TargetGains, Counter, OutPos, DstBufferSize); + + ALfloat (&nfcsamples)[BUFFERSIZE] = Device->NfcSampleData; + size_t chanoffset{outcount}; + using FilterProc = void (NfcFilter::*)(float*,const float*,int); + auto apply_nfc = [voice,&parms,samples,TargetGains,DstBufferSize,Counter,OutPos,&chanoffset,&nfcsamples](const FilterProc process, const size_t outcount) -> void + { + if(outcount < 1) return; + (parms.NFCtrlFilter.*process)(nfcsamples, samples, DstBufferSize); + MixSamples(nfcsamples, voice->mDirect.Buffer.subspan(chanoffset, outcount), + parms.Gains.Current+chanoffset, TargetGains+chanoffset, Counter, + OutPos, DstBufferSize); + chanoffset += outcount; + }; + apply_nfc(&NfcFilter::process1, Device->NumChannelsPerOrder[1]); + apply_nfc(&NfcFilter::process2, Device->NumChannelsPerOrder[2]); + apply_nfc(&NfcFilter::process3, Device->NumChannelsPerOrder[3]); + } + else + { + const ALfloat *TargetGains{UNLIKELY(vstate == ALvoice::Stopping) ? + SilentTarget : parms.Gains.Target}; + MixSamples(samples, voice->mDirect.Buffer, parms.Gains.Current, TargetGains, + Counter, OutPos, DstBufferSize); + } + } + + ALfloat (&FilterBuf)[BUFFERSIZE] = Device->FilteredData; + for(ALsizei send{0};send < NumSends;++send) + { + if(voice->mSend[send].Buffer.empty()) + continue; + + SendParams &parms = chandata.mWetParams[send]; + const ALfloat *samples{DoFilters(&parms.LowPass, &parms.HighPass, + FilterBuf, ResampledData, DstBufferSize, voice->mSend[send].FilterType)}; + + const ALfloat *TargetGains{UNLIKELY(vstate==ALvoice::Stopping) ? SilentTarget : + parms.Gains.Target}; + MixSamples(samples, voice->mSend[send].Buffer, parms.Gains.Current, TargetGains, + Counter, OutPos, DstBufferSize); + }; + } + /* Update positions */ + DataPosFrac += increment*DstBufferSize; + DataPosInt += DataPosFrac>>FRACTIONBITS; + DataPosFrac &= FRACTIONMASK; + + OutPos += DstBufferSize; + Counter = maxi(DstBufferSize, Counter) - DstBufferSize; + + if(UNLIKELY(!BufferListItem)) + { + /* Do nothing extra when there's no buffers. */ + } + else if(isstatic) + { + if(BufferLoopItem) + { + /* Handle looping static source */ + const ALbuffer *Buffer{BufferListItem->buffers[0]}; + const ALsizei LoopStart{Buffer->LoopStart}; + const ALsizei LoopEnd{Buffer->LoopEnd}; + if(DataPosInt >= LoopEnd) + { + assert(LoopEnd > LoopStart); + DataPosInt = ((DataPosInt-LoopStart)%(LoopEnd-LoopStart)) + LoopStart; + } + } + else + { + /* Handle non-looping static source */ + if(DataPosInt >= BufferListItem->max_samples) + { + if(LIKELY(vstate == ALvoice::Playing)) + vstate = ALvoice::Stopped; + BufferListItem = nullptr; + break; + } + } + } + else while(1) + { + /* Handle streaming source */ + if(BufferListItem->max_samples > DataPosInt) + break; + + DataPosInt -= BufferListItem->max_samples; + + buffers_done += BufferListItem->num_buffers; + BufferListItem = BufferListItem->next.load(std::memory_order_relaxed); + if(!BufferListItem && !(BufferListItem=BufferLoopItem)) + { + if(LIKELY(vstate == ALvoice::Playing)) + vstate = ALvoice::Stopped; + break; + } + } + } while(OutPos < SamplesToDo); + + voice->mFlags |= VOICE_IS_FADING; + + /* Don't update positions and buffers if we were stopping. */ + if(UNLIKELY(vstate == ALvoice::Stopping)) + { + voice->mPlayState.store(ALvoice::Stopped, std::memory_order_release); + return; + } + + /* Update voice info */ + voice->mPosition.store(DataPosInt, std::memory_order_relaxed); + voice->mPositionFrac.store(DataPosFrac, std::memory_order_relaxed); + voice->mCurrentBuffer.store(BufferListItem, std::memory_order_relaxed); + if(vstate == ALvoice::Stopped) + { + voice->mLoopBuffer.store(nullptr, std::memory_order_relaxed); + voice->mSourceID.store(0u, std::memory_order_relaxed); + } + std::atomic_thread_fence(std::memory_order_release); + + /* Send any events now, after the position/buffer info was updated. */ + ALbitfieldSOFT enabledevt{Context->EnabledEvts.load(std::memory_order_acquire)}; + if(buffers_done > 0 && (enabledevt&EventType_BufferCompleted)) + { + RingBuffer *ring{Context->AsyncEvents.get()}; + auto evt_vec = ring->getWriteVector(); + if(evt_vec.first.len > 0) + { + AsyncEvent *evt{new (evt_vec.first.buf) AsyncEvent{EventType_BufferCompleted}}; + evt->u.bufcomp.id = SourceID; + evt->u.bufcomp.count = buffers_done; + ring->writeAdvance(1); + Context->EventSem.post(); + } + } + + if(vstate == ALvoice::Stopped) + { + /* If the voice just ended, set it to Stopping so the next render + * ensures any residual noise fades to 0 amplitude. + */ + voice->mPlayState.store(ALvoice::Stopping, std::memory_order_release); + SendSourceStoppedEvent(Context, SourceID); + } +} diff --git a/alc/panning.cpp b/alc/panning.cpp new file mode 100644 index 00000000..3a67e33a --- /dev/null +++ b/alc/panning.cpp @@ -0,0 +1,964 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 1999-2010 by authors. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include <cmath> +#include <cstdlib> +#include <cstring> +#include <cctype> +#include <cassert> + +#include <cmath> +#include <chrono> +#include <numeric> +#include <algorithm> +#include <functional> + +#include "alcmain.h" +#include "alAuxEffectSlot.h" +#include "alu.h" +#include "alconfig.h" +#include "ambdec.h" +#include "bformatdec.h" +#include "filters/splitter.h" +#include "uhjfilter.h" +#include "bs2b.h" + +#include "alspan.h" + + +constexpr std::array<float,MAX_AMBI_CHANNELS> AmbiScale::FromN3D; +constexpr std::array<float,MAX_AMBI_CHANNELS> AmbiScale::FromSN3D; +constexpr std::array<float,MAX_AMBI_CHANNELS> AmbiScale::FromFuMa; +constexpr std::array<int,MAX_AMBI_CHANNELS> AmbiIndex::FromFuMa; +constexpr std::array<int,MAX_AMBI_CHANNELS> AmbiIndex::FromACN; +constexpr std::array<int,MAX_AMBI2D_CHANNELS> AmbiIndex::From2D; +constexpr std::array<int,MAX_AMBI_CHANNELS> AmbiIndex::From3D; + + +namespace { + +using namespace std::placeholders; +using std::chrono::seconds; +using std::chrono::nanoseconds; + +inline const char *GetLabelFromChannel(Channel channel) +{ + switch(channel) + { + case FrontLeft: return "front-left"; + case FrontRight: return "front-right"; + case FrontCenter: return "front-center"; + case LFE: return "lfe"; + case BackLeft: return "back-left"; + case BackRight: return "back-right"; + case BackCenter: return "back-center"; + case SideLeft: return "side-left"; + case SideRight: return "side-right"; + + case UpperFrontLeft: return "upper-front-left"; + case UpperFrontRight: return "upper-front-right"; + case UpperBackLeft: return "upper-back-left"; + case UpperBackRight: return "upper-back-right"; + case LowerFrontLeft: return "lower-front-left"; + case LowerFrontRight: return "lower-front-right"; + case LowerBackLeft: return "lower-back-left"; + case LowerBackRight: return "lower-back-right"; + + case Aux0: return "aux-0"; + case Aux1: return "aux-1"; + case Aux2: return "aux-2"; + case Aux3: return "aux-3"; + case Aux4: return "aux-4"; + case Aux5: return "aux-5"; + case Aux6: return "aux-6"; + case Aux7: return "aux-7"; + case Aux8: return "aux-8"; + case Aux9: return "aux-9"; + case Aux10: return "aux-10"; + case Aux11: return "aux-11"; + case Aux12: return "aux-12"; + case Aux13: return "aux-13"; + case Aux14: return "aux-14"; + case Aux15: return "aux-15"; + + case MaxChannels: break; + } + return "(unknown)"; +} + + +void AllocChannels(ALCdevice *device, const ALuint main_chans, const ALuint real_chans) +{ + TRACE("Channel config, Main: %u, Real: %u\n", main_chans, real_chans); + + /* Allocate extra channels for any post-filter output. */ + const ALuint num_chans{main_chans + real_chans}; + + TRACE("Allocating %u channels, %zu bytes\n", num_chans, + num_chans*sizeof(device->MixBuffer[0])); + device->MixBuffer.resize(num_chans); + al::span<FloatBufferLine> buffer{device->MixBuffer.data(), device->MixBuffer.size()}; + + device->Dry.Buffer = buffer.first(main_chans); + buffer = buffer.subspan(main_chans); + if(real_chans != 0) + { + device->RealOut.Buffer = buffer.first(real_chans); + buffer = buffer.subspan(real_chans); + } + else + device->RealOut.Buffer = device->Dry.Buffer; +} + + +struct ChannelMap { + Channel ChanName; + ALfloat Config[MAX_AMBI2D_CHANNELS]; +}; + +bool MakeSpeakerMap(ALCdevice *device, const AmbDecConf *conf, ALsizei (&speakermap)[MAX_OUTPUT_CHANNELS]) +{ + auto map_spkr = [device](const AmbDecConf::SpeakerConf &speaker) -> ALsizei + { + /* NOTE: AmbDec does not define any standard speaker names, however + * for this to work we have to by able to find the output channel + * the speaker definition corresponds to. Therefore, OpenAL Soft + * requires these channel labels to be recognized: + * + * LF = Front left + * RF = Front right + * LS = Side left + * RS = Side right + * LB = Back left + * RB = Back right + * CE = Front center + * CB = Back center + * + * Additionally, surround51 will acknowledge back speakers for side + * channels, and surround51rear will acknowledge side speakers for + * back channels, to avoid issues with an ambdec expecting 5.1 to + * use the side channels when the device is configured for back, + * and vice-versa. + */ + Channel ch{}; + if(speaker.Name == "LF") + ch = FrontLeft; + else if(speaker.Name == "RF") + ch = FrontRight; + else if(speaker.Name == "CE") + ch = FrontCenter; + else if(speaker.Name == "LS") + { + if(device->FmtChans == DevFmtX51Rear) + ch = BackLeft; + else + ch = SideLeft; + } + else if(speaker.Name == "RS") + { + if(device->FmtChans == DevFmtX51Rear) + ch = BackRight; + else + ch = SideRight; + } + else if(speaker.Name == "LB") + { + if(device->FmtChans == DevFmtX51) + ch = SideLeft; + else + ch = BackLeft; + } + else if(speaker.Name == "RB") + { + if(device->FmtChans == DevFmtX51) + ch = SideRight; + else + ch = BackRight; + } + else if(speaker.Name == "CB") + ch = BackCenter; + else + { + const char *name{speaker.Name.c_str()}; + unsigned int n; + char c; + + if(sscanf(name, "AUX%u%c", &n, &c) == 1 && n < 16) + ch = static_cast<Channel>(Aux0+n); + else + { + ERR("AmbDec speaker label \"%s\" not recognized\n", name); + return -1; + } + } + const int chidx{GetChannelIdxByName(device->RealOut, ch)}; + if(chidx == -1) + ERR("Failed to lookup AmbDec speaker label %s\n", speaker.Name.c_str()); + return chidx; + }; + std::transform(conf->Speakers.begin(), conf->Speakers.end(), std::begin(speakermap), map_spkr); + /* Return success if no invalid entries are found. */ + auto speakermap_end = std::begin(speakermap) + conf->Speakers.size(); + return std::find(std::begin(speakermap), speakermap_end, -1) == speakermap_end; +} + + +constexpr ChannelMap MonoCfg[1] = { + { FrontCenter, { 1.0f } }, +}, StereoCfg[2] = { + { FrontLeft, { 5.00000000e-1f, 2.88675135e-1f, 5.52305643e-2f } }, + { FrontRight, { 5.00000000e-1f, -2.88675135e-1f, 5.52305643e-2f } }, +}, QuadCfg[4] = { + { BackLeft, { 3.53553391e-1f, 2.04124145e-1f, -2.04124145e-1f } }, + { FrontLeft, { 3.53553391e-1f, 2.04124145e-1f, 2.04124145e-1f } }, + { FrontRight, { 3.53553391e-1f, -2.04124145e-1f, 2.04124145e-1f } }, + { BackRight, { 3.53553391e-1f, -2.04124145e-1f, -2.04124145e-1f } }, +}, X51SideCfg[4] = { + { SideLeft, { 3.33000782e-1f, 1.89084803e-1f, -2.00042375e-1f, -2.12307769e-2f, -1.14579885e-2f } }, + { FrontLeft, { 1.88542860e-1f, 1.27709292e-1f, 1.66295695e-1f, 7.30571517e-2f, 2.10901184e-2f } }, + { FrontRight, { 1.88542860e-1f, -1.27709292e-1f, 1.66295695e-1f, -7.30571517e-2f, 2.10901184e-2f } }, + { SideRight, { 3.33000782e-1f, -1.89084803e-1f, -2.00042375e-1f, 2.12307769e-2f, -1.14579885e-2f } }, +}, X51RearCfg[4] = { + { BackLeft, { 3.33000782e-1f, 1.89084803e-1f, -2.00042375e-1f, -2.12307769e-2f, -1.14579885e-2f } }, + { FrontLeft, { 1.88542860e-1f, 1.27709292e-1f, 1.66295695e-1f, 7.30571517e-2f, 2.10901184e-2f } }, + { FrontRight, { 1.88542860e-1f, -1.27709292e-1f, 1.66295695e-1f, -7.30571517e-2f, 2.10901184e-2f } }, + { BackRight, { 3.33000782e-1f, -1.89084803e-1f, -2.00042375e-1f, 2.12307769e-2f, -1.14579885e-2f } }, +}, X61Cfg[6] = { + { SideLeft, { 2.04460341e-1f, 2.17177926e-1f, -4.39996780e-2f, -2.60790269e-2f, -6.87239792e-2f } }, + { FrontLeft, { 1.58923161e-1f, 9.21772680e-2f, 1.59658796e-1f, 6.66278083e-2f, 3.84686854e-2f } }, + { FrontRight, { 1.58923161e-1f, -9.21772680e-2f, 1.59658796e-1f, -6.66278083e-2f, 3.84686854e-2f } }, + { SideRight, { 2.04460341e-1f, -2.17177926e-1f, -4.39996780e-2f, 2.60790269e-2f, -6.87239792e-2f } }, + { BackCenter, { 2.50001688e-1f, 0.00000000e+0f, -2.50000094e-1f, 0.00000000e+0f, 6.05133395e-2f } }, +}, X71Cfg[6] = { + { BackLeft, { 2.04124145e-1f, 1.08880247e-1f, -1.88586120e-1f, -1.29099444e-1f, 7.45355993e-2f, 3.73460789e-2f, 0.00000000e+0f } }, + { SideLeft, { 2.04124145e-1f, 2.17760495e-1f, 0.00000000e+0f, 0.00000000e+0f, -1.49071198e-1f, -3.73460789e-2f, 0.00000000e+0f } }, + { FrontLeft, { 2.04124145e-1f, 1.08880247e-1f, 1.88586120e-1f, 1.29099444e-1f, 7.45355993e-2f, 3.73460789e-2f, 0.00000000e+0f } }, + { FrontRight, { 2.04124145e-1f, -1.08880247e-1f, 1.88586120e-1f, -1.29099444e-1f, 7.45355993e-2f, -3.73460789e-2f, 0.00000000e+0f } }, + { SideRight, { 2.04124145e-1f, -2.17760495e-1f, 0.00000000e+0f, 0.00000000e+0f, -1.49071198e-1f, 3.73460789e-2f, 0.00000000e+0f } }, + { BackRight, { 2.04124145e-1f, -1.08880247e-1f, -1.88586120e-1f, 1.29099444e-1f, 7.45355993e-2f, -3.73460789e-2f, 0.00000000e+0f } }, +}; + +void InitNearFieldCtrl(ALCdevice *device, ALfloat ctrl_dist, ALsizei order, + const al::span<const ALuint,MAX_AMBI_ORDER+1> chans_per_order) +{ + /* NFC is only used when AvgSpeakerDist is greater than 0. */ + const char *devname{device->DeviceName.c_str()}; + if(!GetConfigValueBool(devname, "decoder", "nfc", 0) || !(ctrl_dist > 0.0f)) + return; + + device->AvgSpeakerDist = clampf(ctrl_dist, 0.1f, 10.0f); + TRACE("Using near-field reference distance: %.2f meters\n", device->AvgSpeakerDist); + + auto iter = std::copy(chans_per_order.begin(), chans_per_order.begin()+order+1, + std::begin(device->NumChannelsPerOrder)); + std::fill(iter, std::end(device->NumChannelsPerOrder), 0u); +} + +void InitDistanceComp(ALCdevice *device, const AmbDecConf *conf, const ALsizei (&speakermap)[MAX_OUTPUT_CHANNELS]) +{ + auto get_max = std::bind(maxf, _1, + std::bind(std::mem_fn(&AmbDecConf::SpeakerConf::Distance), _2)); + const ALfloat maxdist{ + std::accumulate(conf->Speakers.begin(), conf->Speakers.end(), float{0.0f}, get_max)}; + + const char *devname{device->DeviceName.c_str()}; + if(!GetConfigValueBool(devname, "decoder", "distance-comp", 1) || !(maxdist > 0.0f)) + return; + + const auto distSampleScale = static_cast<ALfloat>(device->Frequency)/SPEEDOFSOUNDMETRESPERSEC; + const auto ChanDelay = device->ChannelDelay.as_span(); + size_t total{0u}; + for(size_t i{0u};i < conf->Speakers.size();i++) + { + const AmbDecConf::SpeakerConf &speaker = conf->Speakers[i]; + const ALsizei chan{speakermap[i]}; + + /* Distance compensation only delays in steps of the sample rate. This + * is a bit less accurate since the delay time falls to the nearest + * sample time, but it's far simpler as it doesn't have to deal with + * phase offsets. This means at 48khz, for instance, the distance delay + * will be in steps of about 7 millimeters. + */ + ALfloat delay{std::floor((maxdist - speaker.Distance)*distSampleScale + 0.5f)}; + if(delay > ALfloat{MAX_DELAY_LENGTH-1}) + { + ERR("Delay for speaker \"%s\" exceeds buffer length (%f > %d)\n", + speaker.Name.c_str(), delay, MAX_DELAY_LENGTH-1); + delay = ALfloat{MAX_DELAY_LENGTH-1}; + } + + ChanDelay[chan].Length = static_cast<ALsizei>(delay); + ChanDelay[chan].Gain = speaker.Distance / maxdist; + TRACE("Channel %u \"%s\" distance compensation: %d samples, %f gain\n", chan, + speaker.Name.c_str(), ChanDelay[chan].Length, ChanDelay[chan].Gain); + + /* Round up to the next 4th sample, so each channel buffer starts + * 16-byte aligned. + */ + total += RoundUp(ChanDelay[chan].Length, 4); + } + + if(total > 0) + { + device->ChannelDelay.setSampleCount(total); + ChanDelay[0].Buffer = device->ChannelDelay.getSamples(); + auto set_bufptr = [](const DistanceComp::DistData &last, const DistanceComp::DistData &cur) -> DistanceComp::DistData + { + DistanceComp::DistData ret{cur}; + ret.Buffer = last.Buffer + RoundUp(last.Length, 4); + return ret; + }; + std::partial_sum(ChanDelay.begin(), ChanDelay.end(), ChanDelay.begin(), set_bufptr); + } +} + + +auto GetAmbiScales(AmbiNorm scaletype) noexcept -> const std::array<float,MAX_AMBI_CHANNELS>& +{ + if(scaletype == AmbiNorm::FuMa) return AmbiScale::FromFuMa; + if(scaletype == AmbiNorm::SN3D) return AmbiScale::FromSN3D; + return AmbiScale::FromN3D; +} + +auto GetAmbiLayout(AmbiLayout layouttype) noexcept -> const std::array<int,MAX_AMBI_CHANNELS>& +{ + if(layouttype == AmbiLayout::FuMa) return AmbiIndex::FromFuMa; + return AmbiIndex::FromACN; +} + + +void InitPanning(ALCdevice *device) +{ + al::span<const ChannelMap> chanmap; + ALuint coeffcount{}; + + switch(device->FmtChans) + { + case DevFmtMono: + chanmap = MonoCfg; + coeffcount = 1; + break; + + case DevFmtStereo: + chanmap = StereoCfg; + coeffcount = 3; + break; + + case DevFmtQuad: + chanmap = QuadCfg; + coeffcount = 3; + break; + + case DevFmtX51: + chanmap = X51SideCfg; + coeffcount = 5; + break; + + case DevFmtX51Rear: + chanmap = X51RearCfg; + coeffcount = 5; + break; + + case DevFmtX61: + chanmap = X61Cfg; + coeffcount = 5; + break; + + case DevFmtX71: + chanmap = X71Cfg; + coeffcount = 7; + break; + + case DevFmtAmbi3D: + break; + } + + if(device->FmtChans == DevFmtAmbi3D) + { + const char *devname{device->DeviceName.c_str()}; + const std::array<int,MAX_AMBI_CHANNELS> &acnmap = GetAmbiLayout(device->mAmbiLayout); + const std::array<float,MAX_AMBI_CHANNELS> &n3dscale = GetAmbiScales(device->mAmbiScale); + + /* For DevFmtAmbi3D, the ambisonic order is already set. */ + const size_t count{AmbiChannelsFromOrder(device->mAmbiOrder)}; + std::transform(acnmap.begin(), acnmap.begin()+count, std::begin(device->Dry.AmbiMap), + [&n3dscale](const ALsizei &acn) noexcept -> BFChannelConfig + { return BFChannelConfig{1.0f/n3dscale[acn], acn}; } + ); + AllocChannels(device, static_cast<ALuint>(count), 0); + + ALfloat nfc_delay{ConfigValueFloat(devname, "decoder", "nfc-ref-delay").value_or(0.0f)}; + if(nfc_delay > 0.0f) + { + static constexpr ALuint chans_per_order[MAX_AMBI_ORDER+1]{ 1, 3, 5, 7 }; + InitNearFieldCtrl(device, nfc_delay * SPEEDOFSOUNDMETRESPERSEC, device->mAmbiOrder, + chans_per_order); + } + } + else + { + ChannelDec chancoeffs[MAX_OUTPUT_CHANNELS]{}; + ALsizei idxmap[MAX_OUTPUT_CHANNELS]{}; + for(size_t i{0u};i < chanmap.size();++i) + { + const ALint idx{GetChannelIdxByName(device->RealOut, chanmap[i].ChanName)}; + if(idx < 0) + { + ERR("Failed to find %s channel in device\n", + GetLabelFromChannel(chanmap[i].ChanName)); + continue; + } + idxmap[i] = idx; + std::copy_n(chanmap[i].Config, coeffcount, chancoeffs[i]); + } + + /* For non-DevFmtAmbi3D, set the ambisonic order given the mixing + * channel count. Built-in speaker decoders are always 2D, so just + * reverse that calculation. + */ + device->mAmbiOrder = static_cast<ALsizei>((coeffcount-1) / 2); + + std::transform(AmbiIndex::From2D.begin(), AmbiIndex::From2D.begin()+coeffcount, + std::begin(device->Dry.AmbiMap), + [](const ALsizei &index) noexcept { return BFChannelConfig{1.0f, index}; } + ); + AllocChannels(device, coeffcount, device->channelsFromFmt()); + + TRACE("Enabling %s-order%s ambisonic decoder\n", + (coeffcount > 5) ? "third" : + (coeffcount > 3) ? "second" : "first", + "" + ); + device->AmbiDecoder = al::make_unique<BFormatDec>(coeffcount, + static_cast<ALsizei>(chanmap.size()), chancoeffs, idxmap); + } +} + +void InitCustomPanning(ALCdevice *device, bool hqdec, const AmbDecConf *conf, const ALsizei (&speakermap)[MAX_OUTPUT_CHANNELS]) +{ + static constexpr ALuint chans_per_order2d[MAX_AMBI_ORDER+1] = { 1, 2, 2, 2 }; + static constexpr ALuint chans_per_order3d[MAX_AMBI_ORDER+1] = { 1, 3, 5, 7 }; + + if(!hqdec && conf->FreqBands != 1) + ERR("Basic renderer uses the high-frequency matrix as single-band (xover_freq = %.0fhz)\n", + conf->XOverFreq); + + ALsizei order{(conf->ChanMask > AMBI_2ORDER_MASK) ? 3 : + (conf->ChanMask > AMBI_1ORDER_MASK) ? 2 : 1}; + device->mAmbiOrder = order; + + ALuint count; + if((conf->ChanMask&AMBI_PERIPHONIC_MASK)) + { + count = static_cast<ALuint>(AmbiChannelsFromOrder(order)); + std::transform(AmbiIndex::From3D.begin(), AmbiIndex::From3D.begin()+count, + std::begin(device->Dry.AmbiMap), + [](const ALsizei &index) noexcept { return BFChannelConfig{1.0f, index}; } + ); + } + else + { + count = static_cast<ALuint>(Ambi2DChannelsFromOrder(order)); + std::transform(AmbiIndex::From2D.begin(), AmbiIndex::From2D.begin()+count, + std::begin(device->Dry.AmbiMap), + [](const ALsizei &index) noexcept { return BFChannelConfig{1.0f, index}; } + ); + } + AllocChannels(device, count, device->channelsFromFmt()); + + TRACE("Enabling %s-band %s-order%s ambisonic decoder\n", + (!hqdec || conf->FreqBands == 1) ? "single" : "dual", + (conf->ChanMask > AMBI_2ORDER_MASK) ? "third" : + (conf->ChanMask > AMBI_1ORDER_MASK) ? "second" : "first", + (conf->ChanMask&AMBI_PERIPHONIC_MASK) ? " periphonic" : "" + ); + device->AmbiDecoder = al::make_unique<BFormatDec>(conf, hqdec, count, device->Frequency, + speakermap); + + auto accum_spkr_dist = std::bind(std::plus<float>{}, _1, + std::bind(std::mem_fn(&AmbDecConf::SpeakerConf::Distance), _2)); + const ALfloat avg_dist{ + std::accumulate(conf->Speakers.begin(), conf->Speakers.end(), float{0.0f}, + accum_spkr_dist) / static_cast<ALfloat>(conf->Speakers.size()) + }; + InitNearFieldCtrl(device, avg_dist, order, + (conf->ChanMask&AMBI_PERIPHONIC_MASK) ? chans_per_order3d : chans_per_order2d); + + InitDistanceComp(device, conf, speakermap); +} + +void InitHrtfPanning(ALCdevice *device) +{ + /* NOTE: In degrees, and azimuth goes clockwise. */ + static constexpr AngularPoint AmbiPoints[]{ + { 35.264390f, -45.000000f }, + { 35.264390f, 45.000000f }, + { 35.264390f, 135.000000f }, + { 35.264390f, -135.000000f }, + { -35.264390f, -45.000000f }, + { -35.264390f, 45.000000f }, + { -35.264390f, 135.000000f }, + { -35.264390f, -135.000000f }, + { 0.000000f, -20.905157f }, + { 0.000000f, 20.905157f }, + { 0.000000f, 159.094843f }, + { 0.000000f, -159.094843f }, + { 20.905157f, -90.000000f }, + { -20.905157f, -90.000000f }, + { -20.905157f, 90.000000f }, + { 20.905157f, 90.000000f }, + { 69.094843f, 0.000000f }, + { -69.094843f, 0.000000f }, + { -69.094843f, 180.000000f }, + { 69.094843f, 180.000000f }, + }; + static constexpr ALfloat AmbiMatrix[][MAX_AMBI_CHANNELS]{ + { 5.00000000e-02f, 5.00000000e-02f, 5.00000000e-02f, 5.00000000e-02f, 6.45497224e-02f, 6.45497224e-02f, 0.00000000e+00f, 6.45497224e-02f, 0.00000000e+00f, 1.48264644e-02f, 6.33865691e-02f, 1.01126676e-01f, -7.36485380e-02f, -1.09260065e-02f, 7.08683387e-02f, -1.01622099e-01f }, + { 5.00000000e-02f, -5.00000000e-02f, 5.00000000e-02f, 5.00000000e-02f, -6.45497224e-02f, -6.45497224e-02f, 0.00000000e+00f, 6.45497224e-02f, 0.00000000e+00f, -1.48264644e-02f, -6.33865691e-02f, -1.01126676e-01f, -7.36485380e-02f, -1.09260065e-02f, 7.08683387e-02f, -1.01622099e-01f }, + { 5.00000000e-02f, -5.00000000e-02f, 5.00000000e-02f, -5.00000000e-02f, 6.45497224e-02f, -6.45497224e-02f, 0.00000000e+00f, -6.45497224e-02f, 0.00000000e+00f, -1.48264644e-02f, 6.33865691e-02f, -1.01126676e-01f, -7.36485380e-02f, 1.09260065e-02f, 7.08683387e-02f, 1.01622099e-01f }, + { 5.00000000e-02f, 5.00000000e-02f, 5.00000000e-02f, -5.00000000e-02f, -6.45497224e-02f, 6.45497224e-02f, 0.00000000e+00f, -6.45497224e-02f, 0.00000000e+00f, 1.48264644e-02f, -6.33865691e-02f, 1.01126676e-01f, -7.36485380e-02f, 1.09260065e-02f, 7.08683387e-02f, 1.01622099e-01f }, + { 5.00000000e-02f, 5.00000000e-02f, -5.00000000e-02f, 5.00000000e-02f, 6.45497224e-02f, -6.45497224e-02f, 0.00000000e+00f, -6.45497224e-02f, 0.00000000e+00f, 1.48264644e-02f, -6.33865691e-02f, 1.01126676e-01f, 7.36485380e-02f, -1.09260065e-02f, -7.08683387e-02f, -1.01622099e-01f }, + { 5.00000000e-02f, -5.00000000e-02f, -5.00000000e-02f, 5.00000000e-02f, -6.45497224e-02f, 6.45497224e-02f, 0.00000000e+00f, -6.45497224e-02f, 0.00000000e+00f, -1.48264644e-02f, 6.33865691e-02f, -1.01126676e-01f, 7.36485380e-02f, -1.09260065e-02f, -7.08683387e-02f, -1.01622099e-01f }, + { 5.00000000e-02f, -5.00000000e-02f, -5.00000000e-02f, -5.00000000e-02f, 6.45497224e-02f, 6.45497224e-02f, 0.00000000e+00f, 6.45497224e-02f, 0.00000000e+00f, -1.48264644e-02f, -6.33865691e-02f, -1.01126676e-01f, 7.36485380e-02f, 1.09260065e-02f, -7.08683387e-02f, 1.01622099e-01f }, + { 5.00000000e-02f, 5.00000000e-02f, -5.00000000e-02f, -5.00000000e-02f, -6.45497224e-02f, -6.45497224e-02f, 0.00000000e+00f, 6.45497224e-02f, 0.00000000e+00f, 1.48264644e-02f, 6.33865691e-02f, 1.01126676e-01f, 7.36485380e-02f, 1.09260065e-02f, -7.08683387e-02f, 1.01622099e-01f }, + { 5.00000000e-02f, 3.09016994e-02f, 0.00000000e+00f, 8.09016994e-02f, 6.45497224e-02f, 0.00000000e+00f, -5.59016994e-02f, 0.00000000e+00f, 7.21687836e-02f, 7.76323754e-02f, 0.00000000e+00f, -1.49775925e-01f, 0.00000000e+00f, -2.95083663e-02f, 0.00000000e+00f, 7.76323754e-02f }, + { 5.00000000e-02f, -3.09016994e-02f, 0.00000000e+00f, 8.09016994e-02f, -6.45497224e-02f, 0.00000000e+00f, -5.59016994e-02f, 0.00000000e+00f, 7.21687836e-02f, -7.76323754e-02f, 0.00000000e+00f, 1.49775925e-01f, 0.00000000e+00f, -2.95083663e-02f, 0.00000000e+00f, 7.76323754e-02f }, + { 5.00000000e-02f, -3.09016994e-02f, 0.00000000e+00f, -8.09016994e-02f, 6.45497224e-02f, 0.00000000e+00f, -5.59016994e-02f, 0.00000000e+00f, 7.21687836e-02f, -7.76323754e-02f, 0.00000000e+00f, 1.49775925e-01f, 0.00000000e+00f, 2.95083663e-02f, 0.00000000e+00f, -7.76323754e-02f }, + { 5.00000000e-02f, 3.09016994e-02f, 0.00000000e+00f, -8.09016994e-02f, -6.45497224e-02f, 0.00000000e+00f, -5.59016994e-02f, 0.00000000e+00f, 7.21687836e-02f, 7.76323754e-02f, 0.00000000e+00f, -1.49775925e-01f, 0.00000000e+00f, 2.95083663e-02f, 0.00000000e+00f, -7.76323754e-02f }, + { 5.00000000e-02f, 8.09016994e-02f, 3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, 6.45497224e-02f, -3.45491503e-02f, 0.00000000e+00f, -8.44966837e-02f, -4.79794466e-02f, 0.00000000e+00f, -6.77901327e-02f, 3.03448665e-02f, 0.00000000e+00f, -1.65948192e-01f, 0.00000000e+00f }, + { 5.00000000e-02f, 8.09016994e-02f, -3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, -6.45497224e-02f, -3.45491503e-02f, 0.00000000e+00f, -8.44966837e-02f, -4.79794466e-02f, 0.00000000e+00f, -6.77901327e-02f, -3.03448665e-02f, 0.00000000e+00f, 1.65948192e-01f, 0.00000000e+00f }, + { 5.00000000e-02f, -8.09016994e-02f, -3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, 6.45497224e-02f, -3.45491503e-02f, 0.00000000e+00f, -8.44966837e-02f, 4.79794466e-02f, 0.00000000e+00f, 6.77901327e-02f, -3.03448665e-02f, 0.00000000e+00f, 1.65948192e-01f, 0.00000000e+00f }, + { 5.00000000e-02f, -8.09016994e-02f, 3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, -6.45497224e-02f, -3.45491503e-02f, 0.00000000e+00f, -8.44966837e-02f, 4.79794466e-02f, 0.00000000e+00f, 6.77901327e-02f, 3.03448665e-02f, 0.00000000e+00f, -1.65948192e-01f, 0.00000000e+00f }, + { 5.00000000e-02f, 0.00000000e+00f, 8.09016994e-02f, 3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, 9.04508497e-02f, 6.45497224e-02f, 1.23279000e-02f, 0.00000000e+00f, 0.00000000e+00f, 0.00000000e+00f, 7.94438918e-02f, 1.12611206e-01f, -2.42115150e-02f, 1.25611822e-01f }, + { 5.00000000e-02f, 0.00000000e+00f, -8.09016994e-02f, 3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, 9.04508497e-02f, -6.45497224e-02f, 1.23279000e-02f, 0.00000000e+00f, 0.00000000e+00f, 0.00000000e+00f, -7.94438918e-02f, 1.12611206e-01f, 2.42115150e-02f, 1.25611822e-01f }, + { 5.00000000e-02f, 0.00000000e+00f, -8.09016994e-02f, -3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, 9.04508497e-02f, 6.45497224e-02f, 1.23279000e-02f, 0.00000000e+00f, 0.00000000e+00f, 0.00000000e+00f, -7.94438918e-02f, -1.12611206e-01f, 2.42115150e-02f, -1.25611822e-01f }, + { 5.00000000e-02f, 0.00000000e+00f, 8.09016994e-02f, -3.09016994e-02f, 0.00000000e+00f, 0.00000000e+00f, 9.04508497e-02f, -6.45497224e-02f, 1.23279000e-02f, 0.00000000e+00f, 0.00000000e+00f, 0.00000000e+00f, 7.94438918e-02f, -1.12611206e-01f, -2.42115150e-02f, -1.25611822e-01f } + }; + static constexpr ALfloat AmbiOrderHFGain1O[MAX_AMBI_ORDER+1]{ + 3.16227766e+00f, 1.82574186e+00f + }, AmbiOrderHFGain2O[MAX_AMBI_ORDER+1]{ + 2.35702260e+00f, 1.82574186e+00f, 9.42809042e-01f + }, AmbiOrderHFGain3O[MAX_AMBI_ORDER+1]{ + 1.86508671e+00f, 1.60609389e+00f, 1.14205530e+00f, 5.68379553e-01f + }; + static constexpr ALuint ChansPerOrder[MAX_AMBI_ORDER+1]{ 1, 3, 5, 7 }; + const ALfloat *AmbiOrderHFGain{AmbiOrderHFGain1O}; + + static_assert(al::size(AmbiPoints) == al::size(AmbiMatrix), "Ambisonic HRTF mismatch"); + + /* Don't bother with HOA when using full HRTF rendering. Nothing needs it, + * and it eases the CPU/memory load. + */ + device->mRenderMode = HrtfRender; + ALsizei ambi_order{1}; + if(auto modeopt = ConfigValueStr(device->DeviceName.c_str(), nullptr, "hrtf-mode")) + { + const char *mode{modeopt->c_str()}; + if(strcasecmp(mode, "basic") == 0) + { + ERR("HRTF mode \"%s\" deprecated, substituting \"%s\"\n", mode, "ambi2"); + mode = "ambi2"; + } + + if(strcasecmp(mode, "full") == 0) + device->mRenderMode = HrtfRender; + else if(strcasecmp(mode, "ambi1") == 0) + { + device->mRenderMode = NormalRender; + ambi_order = 1; + } + else if(strcasecmp(mode, "ambi2") == 0) + { + device->mRenderMode = NormalRender; + ambi_order = 2; + } + else if(strcasecmp(mode, "ambi3") == 0) + { + device->mRenderMode = NormalRender; + ambi_order = 3; + } + else + ERR("Unexpected hrtf-mode: %s\n", mode); + } + TRACE("%s HRTF rendering enabled, using \"%s\"\n", + (device->mRenderMode == HrtfRender) ? "Full" : + (ambi_order >= 3) ? "Third-Order" : + (ambi_order == 2) ? "Second-Order" : + (ambi_order == 1) ? "First-Order" : "Unknown", + device->HrtfName.c_str()); + + if(ambi_order >= 3) + AmbiOrderHFGain = AmbiOrderHFGain3O; + else if(ambi_order == 2) + AmbiOrderHFGain = AmbiOrderHFGain2O; + else if(ambi_order == 1) + AmbiOrderHFGain = AmbiOrderHFGain1O; + device->mAmbiOrder = ambi_order; + + const size_t count{AmbiChannelsFromOrder(ambi_order)}; + device->mHrtfState = DirectHrtfState::Create(count); + + std::transform(AmbiIndex::From3D.begin(), AmbiIndex::From3D.begin()+count, + std::begin(device->Dry.AmbiMap), + [](const ALsizei &index) noexcept { return BFChannelConfig{1.0f, index}; } + ); + AllocChannels(device, static_cast<ALuint>(count), device->channelsFromFmt()); + + BuildBFormatHrtf(device->mHrtf, device->mHrtfState.get(), static_cast<ALuint>(count), + AmbiPoints, AmbiMatrix, al::size(AmbiPoints), AmbiOrderHFGain); + + HrtfEntry *Hrtf{device->mHrtf}; + InitNearFieldCtrl(device, Hrtf->field[0].distance, ambi_order, ChansPerOrder); +} + +void InitUhjPanning(ALCdevice *device) +{ + /* UHJ is always 2D first-order. */ + static constexpr size_t count{Ambi2DChannelsFromOrder(1)}; + + device->mAmbiOrder = 1; + + auto acnmap_end = AmbiIndex::FromFuMa.begin() + count; + std::transform(AmbiIndex::FromFuMa.begin(), acnmap_end, std::begin(device->Dry.AmbiMap), + [](const ALsizei &acn) noexcept -> BFChannelConfig + { return BFChannelConfig{1.0f/AmbiScale::FromFuMa[acn], acn}; } + ); + AllocChannels(device, ALuint{count}, device->channelsFromFmt()); +} + +} // namespace + +void aluInitRenderer(ALCdevice *device, ALint hrtf_id, HrtfRequestMode hrtf_appreq, HrtfRequestMode hrtf_userreq) +{ + /* Hold the HRTF the device last used, in case it's used again. */ + HrtfEntry *old_hrtf{device->mHrtf}; + + device->mHrtfState = nullptr; + device->mHrtf = nullptr; + device->HrtfName.clear(); + device->mRenderMode = NormalRender; + + if(device->FmtChans != DevFmtStereo) + { + if(old_hrtf) + old_hrtf->DecRef(); + old_hrtf = nullptr; + if(hrtf_appreq == Hrtf_Enable) + device->HrtfStatus = ALC_HRTF_UNSUPPORTED_FORMAT_SOFT; + + const char *layout{nullptr}; + switch(device->FmtChans) + { + case DevFmtQuad: layout = "quad"; break; + case DevFmtX51: /* fall-through */ + case DevFmtX51Rear: layout = "surround51"; break; + case DevFmtX61: layout = "surround61"; break; + case DevFmtX71: layout = "surround71"; break; + /* Mono, Stereo, and Ambisonics output don't use custom decoders. */ + case DevFmtMono: + case DevFmtStereo: + case DevFmtAmbi3D: + break; + } + + const char *devname{device->DeviceName.c_str()}; + ALsizei speakermap[MAX_OUTPUT_CHANNELS]; + AmbDecConf *pconf{nullptr}; + AmbDecConf conf{}; + if(layout) + { + if(auto decopt = ConfigValueStr(devname, "decoder", layout)) + { + if(!conf.load(decopt->c_str())) + ERR("Failed to load layout file %s\n", decopt->c_str()); + else if(conf.Speakers.size() > MAX_OUTPUT_CHANNELS) + ERR("Unsupported speaker count %zu (max %d)\n", conf.Speakers.size(), + MAX_OUTPUT_CHANNELS); + else if(conf.ChanMask > AMBI_3ORDER_MASK) + ERR("Unsupported channel mask 0x%04x (max 0x%x)\n", conf.ChanMask, + AMBI_3ORDER_MASK); + else if(MakeSpeakerMap(device, &conf, speakermap)) + pconf = &conf; + } + } + + if(!pconf) + InitPanning(device); + else + { + int hqdec{GetConfigValueBool(devname, "decoder", "hq-mode", 0)}; + InitCustomPanning(device, !!hqdec, pconf, speakermap); + } + if(device->AmbiDecoder) + device->PostProcess = ProcessAmbiDec; + return; + } + + bool headphones{device->IsHeadphones != AL_FALSE}; + if(device->Type != Loopback) + { + if(auto modeopt = ConfigValueStr(device->DeviceName.c_str(), nullptr, "stereo-mode")) + { + const char *mode{modeopt->c_str()}; + if(strcasecmp(mode, "headphones") == 0) + headphones = true; + else if(strcasecmp(mode, "speakers") == 0) + headphones = false; + else if(strcasecmp(mode, "auto") != 0) + ERR("Unexpected stereo-mode: %s\n", mode); + } + } + + if(hrtf_userreq == Hrtf_Default) + { + bool usehrtf = (headphones && hrtf_appreq != Hrtf_Disable) || + (hrtf_appreq == Hrtf_Enable); + if(!usehrtf) goto no_hrtf; + + device->HrtfStatus = ALC_HRTF_ENABLED_SOFT; + if(headphones && hrtf_appreq != Hrtf_Disable) + device->HrtfStatus = ALC_HRTF_HEADPHONES_DETECTED_SOFT; + } + else + { + if(hrtf_userreq != Hrtf_Enable) + { + if(hrtf_appreq == Hrtf_Enable) + device->HrtfStatus = ALC_HRTF_DENIED_SOFT; + goto no_hrtf; + } + device->HrtfStatus = ALC_HRTF_REQUIRED_SOFT; + } + + if(device->HrtfList.empty()) + device->HrtfList = EnumerateHrtf(device->DeviceName.c_str()); + + if(hrtf_id >= 0 && static_cast<size_t>(hrtf_id) < device->HrtfList.size()) + { + const EnumeratedHrtf &entry = device->HrtfList[hrtf_id]; + HrtfEntry *hrtf{GetLoadedHrtf(entry.hrtf)}; + if(hrtf && hrtf->sampleRate == device->Frequency) + { + device->mHrtf = hrtf; + device->HrtfName = entry.name; + } + else if(hrtf) + hrtf->DecRef(); + } + + if(!device->mHrtf) + { + auto find_hrtf = [device](const EnumeratedHrtf &entry) -> bool + { + HrtfEntry *hrtf{GetLoadedHrtf(entry.hrtf)}; + if(!hrtf) return false; + if(hrtf->sampleRate != device->Frequency) + { + hrtf->DecRef(); + return false; + } + device->mHrtf = hrtf; + device->HrtfName = entry.name; + return true; + }; + std::find_if(device->HrtfList.cbegin(), device->HrtfList.cend(), find_hrtf); + } + + if(device->mHrtf) + { + if(old_hrtf) + old_hrtf->DecRef(); + old_hrtf = nullptr; + + InitHrtfPanning(device); + device->PostProcess = ProcessHrtf; + return; + } + device->HrtfStatus = ALC_HRTF_UNSUPPORTED_FORMAT_SOFT; + +no_hrtf: + if(old_hrtf) + old_hrtf->DecRef(); + old_hrtf = nullptr; + + device->mRenderMode = StereoPair; + + if(device->Type != Loopback) + { + if(auto cflevopt = ConfigValueInt(device->DeviceName.c_str(), nullptr, "cf_level")) + { + if(*cflevopt > 0 && *cflevopt <= 6) + { + device->Bs2b = al::make_unique<bs2b>(); + bs2b_set_params(device->Bs2b.get(), *cflevopt, device->Frequency); + TRACE("BS2B enabled\n"); + InitPanning(device); + device->PostProcess = ProcessBs2b; + return; + } + } + } + + if(auto encopt = ConfigValueStr(device->DeviceName.c_str(), nullptr, "stereo-encoding")) + { + const char *mode{encopt->c_str()}; + if(strcasecmp(mode, "uhj") == 0) + device->mRenderMode = NormalRender; + else if(strcasecmp(mode, "panpot") != 0) + ERR("Unexpected stereo-encoding: %s\n", mode); + } + if(device->mRenderMode == NormalRender) + { + device->Uhj_Encoder = al::make_unique<Uhj2Encoder>(); + TRACE("UHJ enabled\n"); + InitUhjPanning(device); + device->PostProcess = ProcessUhj; + return; + } + + TRACE("Stereo rendering\n"); + InitPanning(device); + device->PostProcess = ProcessAmbiDec; +} + + +void aluInitEffectPanning(ALeffectslot *slot, ALCdevice *device) +{ + const size_t count{AmbiChannelsFromOrder(device->mAmbiOrder)}; + slot->MixBuffer.resize(count); + slot->MixBuffer.shrink_to_fit(); + + auto acnmap_end = AmbiIndex::From3D.begin() + count; + auto iter = std::transform(AmbiIndex::From3D.begin(), acnmap_end, slot->Wet.AmbiMap.begin(), + [](const ALsizei &acn) noexcept -> BFChannelConfig + { return BFChannelConfig{1.0f, acn}; } + ); + std::fill(iter, slot->Wet.AmbiMap.end(), BFChannelConfig{}); + slot->Wet.Buffer = {slot->MixBuffer.data(), slot->MixBuffer.size()}; +} + + +void CalcAmbiCoeffs(const ALfloat y, const ALfloat z, const ALfloat x, const ALfloat spread, + ALfloat (&coeffs)[MAX_AMBI_CHANNELS]) +{ + /* Zeroth-order */ + coeffs[0] = 1.0f; /* ACN 0 = 1 */ + /* First-order */ + coeffs[1] = 1.732050808f * y; /* ACN 1 = sqrt(3) * Y */ + coeffs[2] = 1.732050808f * z; /* ACN 2 = sqrt(3) * Z */ + coeffs[3] = 1.732050808f * x; /* ACN 3 = sqrt(3) * X */ + /* Second-order */ + coeffs[4] = 3.872983346f * x * y; /* ACN 4 = sqrt(15) * X * Y */ + coeffs[5] = 3.872983346f * y * z; /* ACN 5 = sqrt(15) * Y * Z */ + coeffs[6] = 1.118033989f * (z*z*3.0f - 1.0f); /* ACN 6 = sqrt(5)/2 * (3*Z*Z - 1) */ + coeffs[7] = 3.872983346f * x * z; /* ACN 7 = sqrt(15) * X * Z */ + coeffs[8] = 1.936491673f * (x*x - y*y); /* ACN 8 = sqrt(15)/2 * (X*X - Y*Y) */ + /* Third-order */ + coeffs[9] = 2.091650066f * y * (x*x*3.0f - y*y); /* ACN 9 = sqrt(35/8) * Y * (3*X*X - Y*Y) */ + coeffs[10] = 10.246950766f * z * x * y; /* ACN 10 = sqrt(105) * Z * X * Y */ + coeffs[11] = 1.620185175f * y * (z*z*5.0f - 1.0f); /* ACN 11 = sqrt(21/8) * Y * (5*Z*Z - 1) */ + coeffs[12] = 1.322875656f * z * (z*z*5.0f - 3.0f); /* ACN 12 = sqrt(7)/2 * Z * (5*Z*Z - 3) */ + coeffs[13] = 1.620185175f * x * (z*z*5.0f - 1.0f); /* ACN 13 = sqrt(21/8) * X * (5*Z*Z - 1) */ + coeffs[14] = 5.123475383f * z * (x*x - y*y); /* ACN 14 = sqrt(105)/2 * Z * (X*X - Y*Y) */ + coeffs[15] = 2.091650066f * x * (x*x - y*y*3.0f); /* ACN 15 = sqrt(35/8) * X * (X*X - 3*Y*Y) */ + /* Fourth-order */ + /* ACN 16 = sqrt(35)*3/2 * X * Y * (X*X - Y*Y) */ + /* ACN 17 = sqrt(35/2)*3/2 * (3*X*X - Y*Y) * Y * Z */ + /* ACN 18 = sqrt(5)*3/2 * X * Y * (7*Z*Z - 1) */ + /* ACN 19 = sqrt(5/2)*3/2 * Y * Z * (7*Z*Z - 3) */ + /* ACN 20 = 3/8 * (35*Z*Z*Z*Z - 30*Z*Z + 3) */ + /* ACN 21 = sqrt(5/2)*3/2 * X * Z * (7*Z*Z - 3) */ + /* ACN 22 = sqrt(5)*3/4 * (X*X - Y*Y) * (7*Z*Z - 1) */ + /* ACN 23 = sqrt(35/2)*3/2 * (X*X - 3*Y*Y) * X * Z */ + /* ACN 24 = sqrt(35)*3/8 * (X*X*X*X - 6*X*X*Y*Y + Y*Y*Y*Y) */ + + if(spread > 0.0f) + { + /* Implement the spread by using a spherical source that subtends the + * angle spread. See: + * http://www.ppsloan.org/publications/StupidSH36.pdf - Appendix A3 + * + * When adjusted for N3D normalization instead of SN3D, these + * calculations are: + * + * ZH0 = -sqrt(pi) * (-1+ca); + * ZH1 = 0.5*sqrt(pi) * sa*sa; + * ZH2 = -0.5*sqrt(pi) * ca*(-1+ca)*(ca+1); + * ZH3 = -0.125*sqrt(pi) * (-1+ca)*(ca+1)*(5*ca*ca - 1); + * ZH4 = -0.125*sqrt(pi) * ca*(-1+ca)*(ca+1)*(7*ca*ca - 3); + * ZH5 = -0.0625*sqrt(pi) * (-1+ca)*(ca+1)*(21*ca*ca*ca*ca - 14*ca*ca + 1); + * + * The gain of the source is compensated for size, so that the + * loudness doesn't depend on the spread. Thus: + * + * ZH0 = 1.0f; + * ZH1 = 0.5f * (ca+1.0f); + * ZH2 = 0.5f * (ca+1.0f)*ca; + * ZH3 = 0.125f * (ca+1.0f)*(5.0f*ca*ca - 1.0f); + * ZH4 = 0.125f * (ca+1.0f)*(7.0f*ca*ca - 3.0f)*ca; + * ZH5 = 0.0625f * (ca+1.0f)*(21.0f*ca*ca*ca*ca - 14.0f*ca*ca + 1.0f); + */ + ALfloat ca = std::cos(spread * 0.5f); + /* Increase the source volume by up to +3dB for a full spread. */ + ALfloat scale = std::sqrt(1.0f + spread/al::MathDefs<float>::Tau()); + + ALfloat ZH0_norm = scale; + ALfloat ZH1_norm = 0.5f * (ca+1.f) * scale; + ALfloat ZH2_norm = 0.5f * (ca+1.f)*ca * scale; + ALfloat ZH3_norm = 0.125f * (ca+1.f)*(5.f*ca*ca-1.f) * scale; + + /* Zeroth-order */ + coeffs[0] *= ZH0_norm; + /* First-order */ + coeffs[1] *= ZH1_norm; + coeffs[2] *= ZH1_norm; + coeffs[3] *= ZH1_norm; + /* Second-order */ + coeffs[4] *= ZH2_norm; + coeffs[5] *= ZH2_norm; + coeffs[6] *= ZH2_norm; + coeffs[7] *= ZH2_norm; + coeffs[8] *= ZH2_norm; + /* Third-order */ + coeffs[9] *= ZH3_norm; + coeffs[10] *= ZH3_norm; + coeffs[11] *= ZH3_norm; + coeffs[12] *= ZH3_norm; + coeffs[13] *= ZH3_norm; + coeffs[14] *= ZH3_norm; + coeffs[15] *= ZH3_norm; + } +} + +void ComputePanGains(const MixParams *mix, const ALfloat *RESTRICT coeffs, ALfloat ingain, ALfloat (&gains)[MAX_OUTPUT_CHANNELS]) +{ + auto ambimap = mix->AmbiMap.cbegin(); + + auto iter = std::transform(ambimap, ambimap+mix->Buffer.size(), std::begin(gains), + [coeffs,ingain](const BFChannelConfig &chanmap) noexcept -> ALfloat + { + ASSUME(chanmap.Index >= 0); + return chanmap.Scale * coeffs[chanmap.Index] * ingain; + } + ); + std::fill(iter, std::end(gains), 0.0f); +} diff --git a/alc/ringbuffer.cpp b/alc/ringbuffer.cpp new file mode 100644 index 00000000..6ef576a5 --- /dev/null +++ b/alc/ringbuffer.cpp @@ -0,0 +1,253 @@ +/** + * 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 <cstring> +#include <cstdlib> +#include <climits> + +#include <algorithm> + +#include "ringbuffer.h" +#include "atomic.h" +#include "threads.h" +#include "almalloc.h" +#include "compat.h" + + +RingBufferPtr CreateRingBuffer(size_t sz, size_t elem_sz, int limit_writes) +{ + size_t power_of_two{0u}; + if(sz > 0) + { + power_of_two = sz; + power_of_two |= power_of_two>>1; + power_of_two |= power_of_two>>2; + power_of_two |= power_of_two>>4; + power_of_two |= power_of_two>>8; + power_of_two |= power_of_two>>16; +#if SIZE_MAX > UINT_MAX + power_of_two |= power_of_two>>32; +#endif + } + ++power_of_two; + if(power_of_two < sz) return nullptr; + + const size_t bufbytes{power_of_two * elem_sz}; + RingBufferPtr rb{new (al_calloc(16, sizeof(*rb) + bufbytes)) RingBuffer{bufbytes}}; + rb->mWriteSize = limit_writes ? sz : (power_of_two-1); + rb->mSizeMask = power_of_two - 1; + rb->mElemSize = elem_sz; + + return rb; +} + +void RingBuffer::reset() noexcept +{ + mWritePtr.store(0, std::memory_order_relaxed); + mReadPtr.store(0, std::memory_order_relaxed); + std::fill_n(mBuffer.begin(), (mSizeMask+1)*mElemSize, al::byte{}); +} + + +size_t RingBuffer::readSpace() const noexcept +{ + size_t w = mWritePtr.load(std::memory_order_acquire); + size_t r = mReadPtr.load(std::memory_order_acquire); + return (w-r) & mSizeMask; +} + +size_t RingBuffer::writeSpace() const noexcept +{ + size_t w = mWritePtr.load(std::memory_order_acquire); + size_t r = mReadPtr.load(std::memory_order_acquire) + mWriteSize - mSizeMask; + return (r-w-1) & mSizeMask; +} + + +size_t RingBuffer::read(void *dest, size_t cnt) noexcept +{ + const size_t free_cnt{readSpace()}; + if(free_cnt == 0) return 0; + + const size_t to_read{std::min(cnt, free_cnt)}; + size_t read_ptr{mReadPtr.load(std::memory_order_relaxed) & mSizeMask}; + + size_t n1, n2; + const size_t cnt2{read_ptr + to_read}; + if(cnt2 > mSizeMask+1) + { + n1 = mSizeMask+1 - read_ptr; + n2 = cnt2 & mSizeMask; + } + else + { + n1 = to_read; + n2 = 0; + } + + auto outiter = std::copy_n(mBuffer.begin() + read_ptr*mElemSize, n1*mElemSize, + static_cast<al::byte*>(dest)); + read_ptr += n1; + if(n2 > 0) + { + std::copy_n(mBuffer.begin(), n2*mElemSize, outiter); + read_ptr += n2; + } + mReadPtr.store(read_ptr, std::memory_order_release); + return to_read; +} + +size_t RingBuffer::peek(void *dest, size_t cnt) const noexcept +{ + const size_t free_cnt{readSpace()}; + if(free_cnt == 0) return 0; + + const size_t to_read{std::min(cnt, free_cnt)}; + size_t read_ptr{mReadPtr.load(std::memory_order_relaxed) & mSizeMask}; + + size_t n1, n2; + const size_t cnt2{read_ptr + to_read}; + if(cnt2 > mSizeMask+1) + { + n1 = mSizeMask+1 - read_ptr; + n2 = cnt2 & mSizeMask; + } + else + { + n1 = to_read; + n2 = 0; + } + + auto outiter = std::copy_n(mBuffer.begin() + read_ptr*mElemSize, n1*mElemSize, + static_cast<al::byte*>(dest)); + if(n2 > 0) + std::copy_n(mBuffer.begin(), n2*mElemSize, outiter); + return to_read; +} + +size_t RingBuffer::write(const void *src, size_t cnt) noexcept +{ + const size_t free_cnt{writeSpace()}; + if(free_cnt == 0) return 0; + + const size_t to_write{std::min(cnt, free_cnt)}; + size_t write_ptr{mWritePtr.load(std::memory_order_relaxed) & mSizeMask}; + + size_t n1, n2; + const size_t cnt2{write_ptr + to_write}; + if(cnt2 > mSizeMask+1) + { + n1 = mSizeMask+1 - write_ptr; + n2 = cnt2 & mSizeMask; + } + else + { + n1 = to_write; + n2 = 0; + } + + auto srcbytes = static_cast<const al::byte*>(src); + std::copy_n(srcbytes, n1*mElemSize, mBuffer.begin() + write_ptr*mElemSize); + write_ptr += n1; + if(n2 > 0) + { + std::copy_n(srcbytes + n1*mElemSize, n2*mElemSize, mBuffer.begin()); + write_ptr += n2; + } + mWritePtr.store(write_ptr, std::memory_order_release); + return to_write; +} + + +void RingBuffer::readAdvance(size_t cnt) noexcept +{ + mReadPtr.fetch_add(cnt, std::memory_order_acq_rel); +} + +void RingBuffer::writeAdvance(size_t cnt) noexcept +{ + mWritePtr.fetch_add(cnt, std::memory_order_acq_rel); +} + + +ll_ringbuffer_data_pair RingBuffer::getReadVector() const noexcept +{ + ll_ringbuffer_data_pair ret; + + size_t w{mWritePtr.load(std::memory_order_acquire)}; + size_t r{mReadPtr.load(std::memory_order_acquire)}; + w &= mSizeMask; + r &= mSizeMask; + const size_t free_cnt{(w-r) & mSizeMask}; + + const size_t cnt2{r + free_cnt}; + if(cnt2 > mSizeMask+1) + { + /* Two part vector: the rest of the buffer after the current read ptr, + * plus some from the start of the buffer. */ + ret.first.buf = const_cast<al::byte*>(mBuffer.data() + r*mElemSize); + ret.first.len = mSizeMask+1 - r; + ret.second.buf = const_cast<al::byte*>(mBuffer.data()); + ret.second.len = cnt2 & mSizeMask; + } + else + { + /* Single part vector: just the rest of the buffer */ + ret.first.buf = const_cast<al::byte*>(mBuffer.data() + r*mElemSize); + ret.first.len = free_cnt; + ret.second.buf = nullptr; + ret.second.len = 0; + } + + return ret; +} + +ll_ringbuffer_data_pair RingBuffer::getWriteVector() const noexcept +{ + ll_ringbuffer_data_pair ret; + + size_t w{mWritePtr.load(std::memory_order_acquire)}; + size_t r{mReadPtr.load(std::memory_order_acquire) + mWriteSize - mSizeMask}; + w &= mSizeMask; + r &= mSizeMask; + const size_t free_cnt{(r-w-1) & mSizeMask}; + + const size_t cnt2{w + free_cnt}; + if(cnt2 > mSizeMask+1) + { + /* Two part vector: the rest of the buffer after the current write ptr, + * plus some from the start of the buffer. */ + ret.first.buf = const_cast<al::byte*>(mBuffer.data() + w*mElemSize); + ret.first.len = mSizeMask+1 - w; + ret.second.buf = const_cast<al::byte*>(mBuffer.data()); + ret.second.len = cnt2 & mSizeMask; + } + else + { + ret.first.buf = const_cast<al::byte*>(mBuffer.data() + w*mElemSize); + ret.first.len = free_cnt; + ret.second.buf = nullptr; + ret.second.len = 0; + } + + return ret; +} diff --git a/alc/ringbuffer.h b/alc/ringbuffer.h new file mode 100644 index 00000000..84139b66 --- /dev/null +++ b/alc/ringbuffer.h @@ -0,0 +1,99 @@ +#ifndef RINGBUFFER_H +#define RINGBUFFER_H + +#include <stddef.h> + +#include <atomic> +#include <memory> +#include <utility> + +#include "albyte.h" +#include "almalloc.h" + + +/* NOTE: This lockless ringbuffer implementation is copied from JACK, extended + * to include an element size. Consequently, parameters and return values for a + * size or count is in 'elements', not bytes. Additionally, it only supports + * single-consumer/single-provider operation. + */ + +struct ll_ringbuffer_data { + al::byte *buf; + size_t len; +}; +using ll_ringbuffer_data_pair = std::pair<ll_ringbuffer_data,ll_ringbuffer_data>; + + +struct RingBuffer { + std::atomic<size_t> mWritePtr{0u}; + std::atomic<size_t> mReadPtr{0u}; + size_t mWriteSize{0u}; + size_t mSizeMask{0u}; + size_t mElemSize{0u}; + + al::FlexArray<al::byte, 16> mBuffer; + + RingBuffer(const size_t count) : mBuffer{count} { } + RingBuffer(const RingBuffer&) = delete; + RingBuffer& operator=(const RingBuffer&) = delete; + + /** Reset the read and write pointers to zero. This is not thread safe. */ + void reset() noexcept; + + /** + * The non-copying data reader. Returns two ringbuffer data pointers that + * hold the current readable data. If the readable data is in one segment + * the second segment has zero length. + */ + ll_ringbuffer_data_pair getReadVector() const noexcept; + /** + * The non-copying data writer. Returns two ringbuffer data pointers that + * hold the current writeable data. If the writeable data is in one segment + * the second segment has zero length. + */ + ll_ringbuffer_data_pair getWriteVector() const noexcept; + + /** + * Return the number of elements available for reading. This is the number + * of elements in front of the read pointer and behind the write pointer. + */ + size_t readSpace() const noexcept; + /** + * The copying data reader. Copy at most `cnt' elements into `dest'. + * Returns the actual number of elements copied. + */ + size_t read(void *dest, size_t cnt) noexcept; + /** + * The copying data reader w/o read pointer advance. Copy at most `cnt' + * elements into `dest'. Returns the actual number of elements copied. + */ + size_t peek(void *dest, size_t cnt) const noexcept; + /** Advance the read pointer `cnt' places. */ + void readAdvance(size_t cnt) noexcept; + + /** + * Return the number of elements available for writing. This is the number + * of elements in front of the write pointer and behind the read pointer. + */ + size_t writeSpace() const noexcept; + /** + * The copying data writer. Copy at most `cnt' elements from `src'. Returns + * the actual number of elements copied. + */ + size_t write(const void *src, size_t cnt) noexcept; + /** Advance the write pointer `cnt' places. */ + void writeAdvance(size_t cnt) noexcept; + + DEF_PLACE_NEWDEL() +}; +using RingBufferPtr = std::unique_ptr<RingBuffer>; + + +/** + * Create a new ringbuffer to hold at least `sz' elements of `elem_sz' bytes. + * The number of elements is rounded up to the next power of two (even if it is + * already a power of two, to ensure the requested amount can be written). + */ +RingBufferPtr CreateRingBuffer(size_t sz, size_t elem_sz, int limit_writes); + +#endif /* RINGBUFFER_H */ diff --git a/alc/uhjfilter.cpp b/alc/uhjfilter.cpp new file mode 100644 index 00000000..55999647 --- /dev/null +++ b/alc/uhjfilter.cpp @@ -0,0 +1,131 @@ + +#include "config.h" + +#include "uhjfilter.h" + +#include <algorithm> + +#include "alu.h" + +namespace { + +/* This is the maximum number of samples processed for each inner loop + * iteration. */ +#define MAX_UPDATE_SAMPLES 128 + + +constexpr ALfloat Filter1CoeffSqr[4] = { + 0.479400865589f, 0.876218493539f, 0.976597589508f, 0.997499255936f +}; +constexpr ALfloat Filter2CoeffSqr[4] = { + 0.161758498368f, 0.733028932341f, 0.945349700329f, 0.990599156685f +}; + +void allpass_process(AllPassState *state, ALfloat *dst, const ALfloat *src, const ALfloat aa, ALsizei todo) +{ + ALfloat z1{state->z[0]}; + ALfloat z2{state->z[1]}; + auto proc_sample = [aa,&z1,&z2](ALfloat input) noexcept -> ALfloat + { + ALfloat output = input*aa + z1; + z1 = z2; z2 = output*aa - input; + return output; + }; + std::transform(src, src+todo, dst, proc_sample); + state->z[0] = z1; + state->z[1] = z2; +} + +} // namespace + + +/* NOTE: There seems to be a bit of an inconsistency in how this encoding is + * supposed to work. Some references, such as + * + * http://members.tripod.com/martin_leese/Ambisonic/UHJ_file_format.html + * + * specify a pre-scaling of sqrt(2) on the W channel input, while other + * references, such as + * + * https://en.wikipedia.org/wiki/Ambisonic_UHJ_format#Encoding.5B1.5D + * and + * https://wiki.xiph.org/Ambisonics#UHJ_format + * + * do not. The sqrt(2) scaling is in line with B-Format decoder coefficients + * which include such a scaling for the W channel input, however the original + * source for this equation is a 1985 paper by Michael Gerzon, which does not + * apparently include the scaling. Applying the extra scaling creates a louder + * result with a narrower stereo image compared to not scaling, and I don't + * know which is the intended result. + */ + +void Uhj2Encoder::encode(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, FloatBufferLine *InSamples, const ALsizei SamplesToDo) +{ + alignas(16) ALfloat D[MAX_UPDATE_SAMPLES], S[MAX_UPDATE_SAMPLES]; + alignas(16) ALfloat temp[MAX_UPDATE_SAMPLES]; + + ASSUME(SamplesToDo > 0); + + auto winput = InSamples[0].cbegin(); + auto xinput = InSamples[1].cbegin(); + auto yinput = InSamples[2].cbegin(); + for(ALsizei base{0};base < SamplesToDo;) + { + const ALsizei todo{mini(SamplesToDo - base, MAX_UPDATE_SAMPLES)}; + ASSUME(todo > 0); + + /* D = 0.6554516*Y */ + std::transform(yinput, yinput+todo, std::begin(temp), + [](const float y) noexcept -> float { return 0.6554516f*y; }); + allpass_process(&mFilter1_Y[0], temp, temp, Filter1CoeffSqr[0], todo); + allpass_process(&mFilter1_Y[1], temp, temp, Filter1CoeffSqr[1], todo); + allpass_process(&mFilter1_Y[2], temp, temp, Filter1CoeffSqr[2], todo); + allpass_process(&mFilter1_Y[3], temp, temp, Filter1CoeffSqr[3], todo); + /* NOTE: Filter1 requires a 1 sample delay for the final output, so + * take the last processed sample from the previous run as the first + * output sample. + */ + D[0] = mLastY; + for(ALsizei i{1};i < todo;i++) + D[i] = temp[i-1]; + mLastY = temp[todo-1]; + + /* D += j(-0.3420201*W + 0.5098604*X) */ + std::transform(winput, winput+todo, xinput, std::begin(temp), + [](const float w, const float x) noexcept -> float + { return -0.3420201f*w + 0.5098604f*x; }); + allpass_process(&mFilter2_WX[0], temp, temp, Filter2CoeffSqr[0], todo); + allpass_process(&mFilter2_WX[1], temp, temp, Filter2CoeffSqr[1], todo); + allpass_process(&mFilter2_WX[2], temp, temp, Filter2CoeffSqr[2], todo); + allpass_process(&mFilter2_WX[3], temp, temp, Filter2CoeffSqr[3], todo); + for(ALsizei i{0};i < todo;i++) + D[i] += temp[i]; + + /* S = 0.9396926*W + 0.1855740*X */ + std::transform(winput, winput+todo, xinput, std::begin(temp), + [](const float w, const float x) noexcept -> float + { return 0.9396926f*w + 0.1855740f*x; }); + allpass_process(&mFilter1_WX[0], temp, temp, Filter1CoeffSqr[0], todo); + allpass_process(&mFilter1_WX[1], temp, temp, Filter1CoeffSqr[1], todo); + allpass_process(&mFilter1_WX[2], temp, temp, Filter1CoeffSqr[2], todo); + allpass_process(&mFilter1_WX[3], temp, temp, Filter1CoeffSqr[3], todo); + S[0] = mLastWX; + for(ALsizei i{1};i < todo;i++) + S[i] = temp[i-1]; + mLastWX = temp[todo-1]; + + /* Left = (S + D)/2.0 */ + ALfloat *RESTRICT left = al::assume_aligned<16>(LeftOut.data()+base); + for(ALsizei i{0};i < todo;i++) + left[i] += (S[i] + D[i]) * 0.5f; + /* Right = (S - D)/2.0 */ + ALfloat *RESTRICT right = al::assume_aligned<16>(RightOut.data()+base); + for(ALsizei i{0};i < todo;i++) + right[i] += (S[i] - D[i]) * 0.5f; + + winput += todo; + xinput += todo; + yinput += todo; + base += todo; + } +} diff --git a/alc/uhjfilter.h b/alc/uhjfilter.h new file mode 100644 index 00000000..53e4f89e --- /dev/null +++ b/alc/uhjfilter.h @@ -0,0 +1,54 @@ +#ifndef UHJFILTER_H +#define UHJFILTER_H + +#include "AL/al.h" + +#include "alcmain.h" +#include "almalloc.h" + + +struct AllPassState { + ALfloat z[2]{0.0f, 0.0f}; +}; + +/* Encoding 2-channel UHJ from B-Format is done as: + * + * S = 0.9396926*W + 0.1855740*X + * D = j(-0.3420201*W + 0.5098604*X) + 0.6554516*Y + * + * Left = (S + D)/2.0 + * Right = (S - D)/2.0 + * + * where j is a wide-band +90 degree phase shift. + * + * The phase shift is done using a Hilbert transform, described here: + * https://web.archive.org/web/20060708031958/http://www.biochem.oulu.fi/~oniemita/dsp/hilbert/ + * It works using 2 sets of 4 chained filters. The first filter chain produces + * a phase shift of varying magnitude over a wide range of frequencies, while + * the second filter chain produces a phase shift 90 degrees ahead of the + * first over the same range. + * + * Combining these two stages requires the use of three filter chains. S- + * channel output uses a Filter1 chain on the W and X channel mix, while the D- + * channel output uses a Filter1 chain on the Y channel plus a Filter2 chain on + * the W and X channel mix. This results in the W and X input mix on the D- + * channel output having the required +90 degree phase shift relative to the + * other inputs. + */ + +struct Uhj2Encoder { + AllPassState mFilter1_Y[4]; + AllPassState mFilter2_WX[4]; + AllPassState mFilter1_WX[4]; + ALfloat mLastY{0.0f}, mLastWX{0.0f}; + + /* Encodes a 2-channel UHJ (stereo-compatible) signal from a B-Format input + * signal. The input must use FuMa channel ordering and scaling. + */ + void encode(FloatBufferLine &LeftOut, FloatBufferLine &RightOut, FloatBufferLine *InSamples, + const ALsizei SamplesToDo); + + DEF_NEWDEL(Uhj2Encoder) +}; + +#endif /* UHJFILTER_H */ diff --git a/alc/vector.h b/alc/vector.h new file mode 100644 index 00000000..1b69d6a7 --- /dev/null +++ b/alc/vector.h @@ -0,0 +1,15 @@ +#ifndef AL_VECTOR_H +#define AL_VECTOR_H + +#include <vector> + +#include "almalloc.h" + +namespace al { + +template<typename T, size_t alignment=alignof(T)> +using vector = std::vector<T, al::allocator<T, alignment>>; + +} // namespace al + +#endif /* AL_VECTOR_H */ |