/**************************************************************************//** * @file * @author Steve Lascos * @company Blackaddr Audio * * LibBasicFunctions is a collection of helpful functions and classes that make * it easier to perform common tasks in Audio applications. * * @copyright This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version.* * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . *****************************************************************************/ #include #include #include #include #include "Arduino.h" #include "Audio.h" #include "BATypes.h" #include "LibMemoryManagement.h" #ifndef __BALIBRARY_LIBBASICFUNCTIONS_H #define __BALIBRARY_LIBBASICFUNCTIONS_H namespace BALibrary { /**************************************************************************//** * QueuePosition is used for storing the index (in an array of queues) and the * offset within an audio_block_t data buffer. Useful for dealing with large * windows of audio spread across multiple audio data blocks. *****************************************************************************/ struct QueuePosition { int offset; ///< offset in samples within an audio_block_t data buffer int index; ///< index in an array of audio data blocks }; /// Calculate the exact sample position in an array of audio blocks that corresponds /// to a particular offset given as time. /// @param milliseconds length of the interval in milliseconds /// @returns a struct containing the index and offset QueuePosition calcQueuePosition(float milliseconds); /// Calculate the exact sample position in an array of audio blocks that corresponds /// to a particular offset given as a number of samples /// @param milliseconds length of the interval in milliseconds /// @returns a struct containing the index and offset QueuePosition calcQueuePosition(size_t numSamples); /// Calculate the number of audio samples (rounded up) that correspond to a /// given length of time. /// @param milliseconds length of the interval in milliseconds /// @returns the number of corresonding audio samples. size_t calcAudioSamples(float milliseconds); /// Calculate a length of time in milliseconds from the number of audio samples. /// @param numSamples Number of audio samples to convert to time /// @return the equivalent time in milliseconds. float calcAudioTimeMs(size_t numSamples); /// Calculate the number of audio samples (usually an offset) from /// a queue position. /// @param position specifies the index and offset within a queue /// @returns the number of samples from the start of the queue array to the /// specified position. size_t calcOffset(QueuePosition position); /// Clear the contents of an audio block to zero /// @param block pointer to the audio block to clear void clearAudioBlock(audio_block_t *block); /// Perform an alpha blend between to audio blocks. Performs
/// out = dry*(1-mix) + wet*(mix) /// @param out pointer to the destination audio block /// @param dry pointer to the dry audio /// @param wet pointer to the wet audio /// @param mix float between 0.0 and 1.0. void alphaBlend(audio_block_t *out, audio_block_t *dry, audio_block_t* wet, float mix); /// Applies a gain to the audio via fixed-point scaling accoring to
/// out = int * (vol * 2^coeffShift) /// @param out pointer to output audio block /// @param in pointer to input audio block /// @param vol volume cofficient between -1.0 and +1.0 /// @param coeffShift number of bits to shift the coefficient void gainAdjust(audio_block_t *out, audio_block_t *in, float vol, int coeffShift = 0); /// Combine two audio blocks through vector addition /// out[n] = in0[n] + in1[n] /// @param out pointer to output audio block /// @param in0 pointer to first input audio block to combine /// @param in1 pointer to second input audio block to combine void combine(audio_block_t *out, audio_block_t *in0, audio_block_t *in1); template class RingBuffer; // forward declare so AudioDelay can use it. /**************************************************************************//** * Audio delays are a very common function in audio processing. In addition to * being used for simply create a delay effect, it can also be used for buffering * a sliding window in time of audio samples. This is useful when combining * several audio_block_t data buffers together to form one large buffer for * FFTs, etc. * @details The buffer works like a queue. You add new audio_block_t when available, * and the class will return an old buffer when it is to be discarded from the queue.
* Note that using INTERNAL memory means the class will only store a queue * of pointers to audio_block_t buffers, since the Teensy Audio uses a shared memory * approach. When using EXTERNAL memory, data is actually copyied to/from an external * SRAM device. *****************************************************************************/ constexpr size_t AUDIO_BLOCK_SIZE = sizeof(int16_t)*AUDIO_BLOCK_SAMPLES; class AudioDelay { public: AudioDelay() = delete; /// Construct an audio buffer using INTERNAL memory by specifying the max number /// of audio samples you will want. /// @param maxSamples equal or greater than your longest delay requirement AudioDelay(size_t maxSamples); /// Construct an audio buffer using INTERNAL memory by specifying the max amount of /// time you will want available in the buffer. /// @param maxDelayTimeMs max length of time you want in the buffer specified in milliseconds AudioDelay(float maxDelayTimeMs); /// Construct an audio buffer using a slot configured with the BALibrary::ExternalSramManager /// @param slot a pointer to the slot representing the memory you wish to use for the buffer. AudioDelay(ExtMemSlot *slot); ~AudioDelay(); /// Add a new audio block into the buffer. When the buffer is filled, /// adding a new block will push out the oldest once which is returned. /// @param blockIn pointer to the most recent block of audio /// @returns the buffer to be discarded, or nullptr if not filled (INTERNAL), or /// not applicable (EXTERNAL). audio_block_t *addBlock(audio_block_t *blockIn); /// When using INTERNAL memory, returns the pointer for the specified index into buffer. /// @details, the most recent block is 0, 2nd most recent is 1, ..., etc. /// @param index the specifies how many buffers older than the current to retrieve /// @returns a pointer to the requested audio_block_t audio_block_t *getBlock(size_t index); /// Returns the max possible delay samples. For INTERNAL memory, the delay can be equal to /// the full maxValue specified. For EXTERNAL memory, the max delay is actually one audio /// block less then the full size to prevent wrapping. /// @returns the maximum delay offset in units of samples. size_t getMaxDelaySamples(); /// Retrieve an audio block (or samples) from the buffer into a destination block /// @details when using INTERNAL memory, only supported size is AUDIO_BLOCK_SAMPLES. When using /// EXTERNAL, a size smaller than AUDIO_BLOCK_SAMPLES can be requested. /// @param dest pointer to the target audio block to write the samples to. /// @param offsetSamples data will start being transferred offset samples from the start of the audio buffer /// @param numSamples default value is AUDIO_BLOCK_SAMPLES, so typically you don't have to specify this parameter. /// @returns true on success, false on error. bool getSamples(audio_block_t *dest, size_t offsetSamples, size_t numSamples = AUDIO_BLOCK_SAMPLES); /// Retrieve an audio block (or samples) from the buffer into a destination sample array /// @details when using INTERNAL memory, only supported size is AUDIO_BLOCK_SAMPLES. When using /// EXTERNAL, a size smaller than AUDIO_BLOCK_SAMPLES can be requested. /// @param dest pointer to the target sample array to write the samples to. /// @param offsetSamples data will start being transferred offset samples from the start of the audio buffer /// @param numSamples number of samples to transfer /// @returns true on success, false on error. bool getSamples(int16_t *dest, size_t offsetSamples, size_t numSamples); /// Provides linearly interpolated samples between discrete samples in the sample buffer. The SOURCE buffer MUST BE OVERSIZED /// to numSamples+1. This is because the last output sample is interpolated from between NUM_SAMPLES and NUM_SAMPLES+1. /// @details this function is typically not used with audio blocks directly since you need AUDIO_BLOCK_SAMPLES+1 as the source size /// even though output size is still only AUDIO_BLOCK_SAMPLES. Manually create an oversized buffer and fill it with AUDIO_BLOCK_SAMPLES+1. /// e.g. 129 instead of 128 samples. The destBuffer does not need to be oversized. /// @param extendedSourceBuffer A source array that contains one more input sample than output samples needed. /// @param dest pointer to the target sample array to write the samples to. /// @param fraction a value between 0.0f and 1.0f that sets the interpolation point between the discrete samples. /// @param numSamples number of samples to transfer /// @returns true on success, false on error. bool interpolateDelay(int16_t *extendedSourceBuffer, int16_t *destBuffer, float fraction, size_t numSamples = AUDIO_BLOCK_SAMPLES); /// When using EXTERNAL memory, this function can return a pointer to the underlying ExtMemSlot object associated /// with the buffer. /// @returns pointer to the underlying ExtMemSlot. ExtMemSlot *getSlot() const { return m_slot; } /// Ween using INTERNAL memory, thsi function can return a pointer to the underlying RingBuffer that contains /// audio_block_t * pointers. /// @returns pointer to the underlying RingBuffer RingBuffer *getRingBuffer() const { return m_ringBuffer; } private: /// enumerates whether the underlying memory buffer uses INTERNAL or EXTERNAL memory enum class MemType : unsigned { MEM_INTERNAL = 0, ///< internal audio_block_t from the Teensy Audio Library is used MEM_EXTERNAL ///< external SPI based ram is used }; MemType m_type; ///< when 0, INTERNAL memory, when 1, external MEMORY. RingBuffer *m_ringBuffer = nullptr; ///< When using INTERNAL memory, a RingBuffer will be created. ExtMemSlot *m_slot = nullptr; ///< When using EXTERNAL memory, an ExtMemSlot must be provided. size_t m_maxDelaySamples = 0; ///< stores the number of audio samples in the AudioDelay. bool m_getSamples(int16_t *dest, size_t offsetSamples, size_t numSamples); ///< operates directly on int16_y buffers }; /**************************************************************************//** * IIR BiQuad Filter - Direct Form I
* y[n] = b0 * x[n] + b1 * x[n-1] + b2 * x[n-2] + a1 * y[n-1] + a2 * y[n-2]
* Some design tools (like Matlab assume the feedback coefficients 'a' are negated. You * may have to negate your 'a' coefficients. * @details Note that the ARM CMSIS-DSP library requires an extra zero between first * and second 'b' coefficients. E.g.
* {b10, 0, b11, b12, a11, a12, b20, 0, b21, b22, a21, a22, ...} *****************************************************************************/ class IirBiQuadFilter { public: IirBiQuadFilter() = delete; /// Construct a Biquad filter with specified number of stages, coefficients and scaling. /// @details See CMSIS-DSP documentation for more details /// @param numStages number of biquad stages. Each stage has 5 coefficients. /// @param coeffs pointer to an array of Q31 fixed-point coefficients (range -1 to +0.999...) /// @param coeffShift coeffs are multiplied by 2^coeffShift to support coefficient range scaling IirBiQuadFilter(unsigned numStages, const int32_t *coeffs, int coeffShift = 0); virtual ~IirBiQuadFilter(); /// Reconfigure the filter coefficients. /// @details See CMSIS-DSP documentation for more details /// @param numStages number of biquad stages. Each stage has 5 coefficients. /// @param coeffs pointer to an array of Q31 fixed-point coefficients (range -1 to +0.999...) /// @param coeffShift coeffs are multiplied by 2^coeffShift to support coefficient range scaling void changeFilterCoeffs(unsigned numStages, const int32_t *coeffs, int coeffShift = 0); /// Process the data using the configured IIR filter /// @details output and input can be the same pointer if in-place modification is desired /// @param output pointer to where the output results will be written /// @param input pointer to where the input data will be read from /// @param numSampmles number of samples to process bool process(int16_t *output, int16_t *input, size_t numSamples); private: const unsigned NUM_STAGES; int32_t *m_coeffs = nullptr; // ARM DSP Math library filter instance arm_biquad_casd_df1_inst_q31 m_iirCfg; int32_t *m_state = nullptr; }; /**************************************************************************//** * A High-precision version of IirBiQuadFilter often necessary for complex, multistage * filters. This class uses CMSIS-DSP biquads with 64-bit internal precision instead * of 32-bit. *****************************************************************************/ class IirBiQuadFilterHQ { public: IirBiQuadFilterHQ() = delete; /// Construct a Biquad filter with specified number of stages, coefficients and scaling. /// @details See CMSIS-DSP documentation for more details /// @param numStages number of biquad stages. Each stage has 5 coefficients. /// @param coeffs pointer to an array of Q31 fixed-point coefficients (range -1 to +0.999...) /// @param coeffShift coeffs are multiplied by 2^coeffShift to support coefficient range scaling IirBiQuadFilterHQ(unsigned maxNumStages, const int32_t *coeffs, int coeffShift = 0); virtual ~IirBiQuadFilterHQ(); /// Reconfigure the filter coefficients. /// @details See CMSIS-DSP documentation for more details /// @param numStages number of biquad stages. Each stage has 5 coefficients. /// @param coeffs pointer to an array of Q31 fixed-point coefficients (range -1 to +0.999...) /// @param coeffShift coeffs are multiplied by 2^coeffShift to support coefficient range scaling void changeFilterCoeffs(unsigned numStages, const int32_t *coeffs, int coeffShift = 0); /// Process the data using the configured IIR filter /// @details output and input can be the same pointer if in-place modification is desired /// @param output poinvoid combine(audio_block_t *out, audio_block_t *in0, audio_block_t *in1)ter to where the output results will be written /// @param input pointer to where the input data will be read from /// @param numSampmles number of samples to process bool process(int16_t *output, int16_t *input, size_t numSamples); private: const unsigned NUM_STAGES; int32_t *m_coeffs = nullptr; // ARM DSP Math library filter instance arm_biquad_cas_df1_32x64_ins_q31 m_iirCfg; int64_t *m_state = nullptr; }; /**************************************************************************//** * A single-precision floating-point biquad using CMSIS-DSP hardware instructions. * @details Use this when IirBiQuadFilterHQ is insufficient, since that version * is still faster with 64-bit fixed-point arithmetic. *****************************************************************************/ class IirBiQuadFilterFloat { public: IirBiQuadFilterFloat() = delete; /// Construct a Biquad filter with specified number of stages and coefficients /// @details See CMSIS-DSP documentation for more details /// @param numStages number of biquad stages. Each stage has 5 coefficients. /// @param coeffs pointer to an array of single-precision floating-point coefficients IirBiQuadFilterFloat(unsigned maxNumStages, const float *coeffs); virtual ~IirBiQuadFilterFloat(); /// Reconfigure the filter coefficients. /// @details See CMSIS-DSP documentation for more details /// @param numStages number of biquad stages. Each stage has 5 coefficients. /// @param coeffs pointer to an array of single-precision floating-point coefficients void changeFilterCoeffs(unsigned numStages, const float *coeffs); /// Process the data using the configured IIR filter /// @details output and input can be the same pointer if in-place modification is desired /// @param output pointer to where the output results will be written /// @param input pointer to where the input data will be read from /// @param numberSampmles number of samples to process bool process(float *output, float *input, size_t numSamples); private: const unsigned NUM_STAGES; float *m_coeffs = nullptr; // ARM DSP Math library filter instance arm_biquad_cascade_df2T_instance_f32 m_iirCfg; float *m_state = nullptr; }; } // namespace BALibrary namespace BALibrary { /**************************************************************************//** * The class will automate a parameter using a trigger from a start value to an * end value, using either a preprogrammed function or a user-provided LUT. *****************************************************************************/ template class ParameterAutomation { public: enum class Function : unsigned { NOT_CONFIGURED = 0, ///< Initial, unconfigured stage HOLD, ///< f(x) = constant LINEAR, ///< f(x) = x EXPONENTIAL, ///< f(x) = exp(-k*x) LOGARITHMIC, ///< f(x) = PARABOLIC, ///< f(x) = x^2 LOOKUP_TABLE ///< f(x) = lut(x) }; ParameterAutomation(); ParameterAutomation(T startValue, T endValue, size_t durationSamples, Function function = Function::LINEAR); ParameterAutomation(T startValue, T endValue, float durationMilliseconds, Function function = Function::LINEAR); virtual ~ParameterAutomation(); /// set the start and end values for the automation /// @param function select which automation curve (function) to use /// @param startValue after reset, parameter automation start from this value /// @param endValue after the automation duration, paramter will finish at this value /// @param durationSamples number of samples to transition from startValue to endValue void reconfigure(T startValue, T endValue, size_t durationSamples, Function function = Function::LINEAR); void reconfigure(T startValue, T endValue, float durationMilliseconds, Function function = Function::LINEAR); /// Start the automation from startValue void trigger(); /// Retrieve the next calculated automation value /// @returns the calculated parameter value of templated type T T getNextValue(); bool isFinished() { return !m_running; } private: Function m_function; T m_startValue; T m_endValue; bool m_running = false; float m_currentValueX; ///< the current value of x in f(x) size_t m_duration; //float m_coeffs[3]; ///< some general coefficient storage float m_slopeX; float m_scaleY; bool m_positiveSlope = true; }; // TODO: initialize with const number of sequences with null type that automatically skips // then register each new sequence. constexpr int MAX_PARAMETER_SEQUENCES = 32; template class ParameterAutomationSequence { public: ParameterAutomationSequence() = delete; ParameterAutomationSequence(int numStages); virtual ~ParameterAutomationSequence(); void setupParameter(int index, T startValue, T endValue, size_t durationSamples, typename ParameterAutomation::Function function); void setupParameter(int index, T startValue, T endValue, float durationMilliseconds, typename ParameterAutomation::Function function); /// Trigger a the automation sequence until numStages is reached or a Function is ParameterAutomation::Function::NOT_CONFIGURED void trigger(); T getNextValue(); bool isFinished(); private: ParameterAutomation *m_paramArray[MAX_PARAMETER_SEQUENCES]; int m_currentIndex = 0; int m_numStages = 0; bool m_running = false; }; /// Supported LFO waveforms enum class Waveform : unsigned { SINE, ///< sinewave TRIANGLE, ///< triangle wave SQUARE, ///< square wave SAWTOOTH, ///< sawtooth wave RANDOM, ///< a non-repeating random waveform NUM_WAVEFORMS, ///< the number of defined waveforms }; /**************************************************************************//** * The LFO is commonly used on modulation effects where some parameter (delay, * volume, etc.) is modulated via waveform at a frequency below 20 Hz. Waveforms * vary between -1.0f and +1.0f. * @details this LFO is for operating on vectors of audio block samples. *****************************************************************************/ template class LowFrequencyOscillatorVector { public: /// Default constructor, uses SINE as default waveform LowFrequencyOscillatorVector() {} /// Specifies the desired waveform at construction time. /// @param waveform specifies desired waveform LowFrequencyOscillatorVector(Waveform waveform) : m_waveform(waveform) {}; /// Default destructor ~LowFrequencyOscillatorVector() {} /// Change the waveform /// @param waveform specifies desired waveform void setWaveform(Waveform waveform) { m_waveform = waveform; } /// Set the LFO rate in Hertz /// @param frequencyHz the LFO frequency in Hertz void setRateAudio(float frequencyHz); /// Set the LFO rate as a fraction /// @param ratio the radians/sample will be 2*pi*ratio void setRateRatio(float ratio); /// Get the next waveform value /// @returns the next vector of phase values T *getNextVector(); private: void m_updatePhase(); ///< called internally when updating the phase vector void m_initPhase(T radiansPerSample); ///< called internally to reset phase upon rate change Waveform m_waveform = Waveform::SINE; ///< LFO waveform T m_phaseVec[AUDIO_BLOCK_SAMPLES]; ///< the vector of next phase values T m_radiansPerBlock = 0.0f; ///< stores the change in radians over one block of data T m_outputVec[AUDIO_BLOCK_SAMPLES]; ///< stores the output LFO values std::atomic_flag m_phaseLock = ATOMIC_FLAG_INIT; ///< used for thread-safety on m_phaseVec const T PI_F = 3.14159265358979323846264338327950288419716939937510582097494459230781640628620899; ///< 2*PI const T TWO_PI_F = 2.0 * 3.14159265358979323846264338327950288419716939937510582097494459230781640628620899; ///< 2*PI const T PI_DIV2_F = 0.5 * 3.14159265358979323846264338327950288419716939937510582097494459230781640628620899; ///< PI/2 const T THREE_PI_DIV2_F = 1.5 * 3.14159265358979323846264338327950288419716939937510582097494459230781640628620899; ///< 3*PI/2 const T TRIANGE_POS_SLOPE = 2.0f/PI_F; const T TRIANGE_NEG_SLOPE = -2.0f/PI_F; const T SAWTOOTH_SLOPE = -1.0f/PI_F; }; } // BALibrary #endif /* __BALIBRARY_LIBBASICFUNCTIONS_H */