diff options
Diffstat (limited to 'alc/effects')
-rw-r--r-- | alc/effects/autowah.cpp | 298 | ||||
-rw-r--r-- | alc/effects/base.h | 196 | ||||
-rw-r--r-- | alc/effects/chorus.cpp | 538 | ||||
-rw-r--r-- | alc/effects/compressor.cpp | 222 | ||||
-rw-r--r-- | alc/effects/dedicated.cpp | 159 | ||||
-rw-r--r-- | alc/effects/distortion.cpp | 269 | ||||
-rw-r--r-- | alc/effects/echo.cpp | 271 | ||||
-rw-r--r-- | alc/effects/equalizer.cpp | 337 | ||||
-rw-r--r-- | alc/effects/fshifter.cpp | 301 | ||||
-rw-r--r-- | alc/effects/modulator.cpp | 279 | ||||
-rw-r--r-- | alc/effects/null.cpp | 164 | ||||
-rw-r--r-- | alc/effects/pshifter.cpp | 405 | ||||
-rw-r--r-- | alc/effects/reverb.cpp | 2102 | ||||
-rw-r--r-- | alc/effects/vmorpher.cpp | 430 |
14 files changed, 5971 insertions, 0 deletions
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; +} |