From 7bf2ed6906de1019c268fe8d02176459a106d5b0 Mon Sep 17 00:00:00 2001 From: Steve Lascos Date: Sat, 2 Feb 2019 10:16:54 -0500 Subject: [PATCH] Basic working chorus, still needs range tweaking, LFO filtering, etc. --- .../AnalogChorusDemoExpansion.ino | 26 ++-- src/AudioEffectAnalogChorus.h | 22 ++- src/LibBasicFunctions.h | 14 ++ src/common/AudioDelay.cpp | 16 +++ src/effects/AudioEffectAnalogChorus.cpp | 135 ++++++++++++------ 5 files changed, 152 insertions(+), 61 deletions(-) diff --git a/examples/Modulation/AnalogChorusDemoExpansion/AnalogChorusDemoExpansion.ino b/examples/Modulation/AnalogChorusDemoExpansion/AnalogChorusDemoExpansion.ino index 5f293d6..b4deed4 100644 --- a/examples/Modulation/AnalogChorusDemoExpansion/AnalogChorusDemoExpansion.ino +++ b/examples/Modulation/AnalogChorusDemoExpansion/AnalogChorusDemoExpansion.ino @@ -87,7 +87,7 @@ int bypassHandle, filterHandle, rateHandle, depthHandle, mixHandle, led1Handle, void setup() { delay(100); // wait a bit for serial to be available Serial.begin(57600); // Start the serial port - delay(100); + delay(500); // Setup the controls. The return value is the handle to use when checking for control changes, etc. // pushbuttons @@ -137,6 +137,7 @@ void setup() { //analogChorus.setFilter(AudioEffectAnalogChorus::Filter::CE2); // The default filter. Naturally bright echo (highs stay, lows fade away) //analogChorus.setFilter(AudioEffectAnalogChorus::Filter::WARM); // A warm filter with a smooth frequency rolloff above 2Khz //analogChorus.setFilter(AudioEffectAnalogChorus::Filter::DARK); // A very dark filter, with a sharp rolloff above 1Khz + analogChorus.setFilter(AudioEffectAnalogChorus::Filter::WARM); // A very dark filter, with a sharp rolloff above 1Khz // Guitar cabinet: Setup 2-stages of LPF, cutoff 4500 Hz, Q-factor 0.7071 (a 'normal' Q-factor) cabFilter.setLowpass(0, 4500, .7071); @@ -157,30 +158,39 @@ void loop() { } // Use SW2 to cycle through the filters +// controls.setOutput(led2Handle, controls.getSwitchValue(led2Handle)); +// if (controls.isSwitchToggled(filterHandle)) { +// filterIndex = (filterIndex + 1) % 3; // update and potentionall roll the counter 0, 1, 2, 0, 1, 2, ... +// // cast the index between 0 to 2 to the enum class AudioEffectAnalogChorus::Filter +// analogChorus.setFilter(static_cast(filterIndex)); // will cycle through 0 to 2 +// Serial.println(String("Filter set to ") + filterIndex); +// } + + // Use SW2 to cycle through the waveforms controls.setOutput(led2Handle, controls.getSwitchValue(led2Handle)); if (controls.isSwitchToggled(filterHandle)) { - filterIndex = (filterIndex + 1) % 3; // update and potentionall roll the counter 0, 1, 2, 0, 1, 2, ... - // cast the index between 0 to 2 to the enum class AudioEffectAnalogChorus::Filter - analogChorus.setFilter(static_cast(filterIndex)); // will cycle through 0 to 2 - Serial.println(String("Filter set to ") + filterIndex); + filterIndex = (filterIndex + 1) % 4; // update and potentionall roll the counter 0, 1, 2, 0, 1, 2, ... + // cast the index between 0 to 3 to the enum class AudioEffectAnalogChorus::Filter + analogChorus.setWaveform(static_cast(filterIndex)); // will cycle through 0 to 2 + Serial.println(String("Waveform set to ") + filterIndex); } // Use POT1 (left) to control the rate setting - if (controls.checkPotValue(rateHandle, potValue)) { + if ( (controls.checkPotValue(rateHandle, potValue)) || (loopCount == 0) ) { // Pot has changed Serial.println(String("New RATE setting: ") + potValue); analogChorus.rate(potValue); } // Use POT2 (right) to control the depth setting - if (controls.checkPotValue(depthHandle, potValue)) { + if ( (controls.checkPotValue(depthHandle, potValue)) || (loopCount == 0) ) { // Pot has changed Serial.println(String("New DEPTH setting: ") + potValue); analogChorus.depth(potValue); } // Use POT3 (centre) to control the mix setting - if (controls.checkPotValue(mixHandle, potValue)) { + if ( (controls.checkPotValue(mixHandle, potValue)) || (loopCount == 0) ) { // Pot has changed Serial.println(String("New MIX setting: ") + potValue); analogChorus.mix(potValue); diff --git a/src/AudioEffectAnalogChorus.h b/src/AudioEffectAnalogChorus.h index 2d87cfb..d0ee022 100644 --- a/src/AudioEffectAnalogChorus.h +++ b/src/AudioEffectAnalogChorus.h @@ -43,8 +43,8 @@ public: ///< List of AudioEffectAnalogChorus MIDI controllable parameters enum { BYPASS = 0, ///< controls effect bypass - RATE, ///< controls the modulate rate of the LFO - DEPTH, ///< controls the depth of modulation of the LFO + RATE, ///< controls the modulate rate of the LFO + DEPTH, ///< controls the depth of modulation of the LFO MIX, ///< controls the the mix of input and chorus signals VOLUME, ///< controls the output volume level NUM_CONTROLS ///< this can be used as an alias for the number of MIDI controls @@ -90,6 +90,10 @@ public: /// Toggle the bypass effect void toggleBypass() { m_bypass = !m_bypass; } + /// Set the LFO waveform + /// @param waveform the LFO waveform to modulate chorus delay with + void setWaveform(BALibrary::Waveform waveform); + /// Set the LFO frequency where 0.0f is MIN and 1.0f is MAX void rate(float rate); @@ -160,7 +164,7 @@ public: virtual void update(void); ///< update automatically called by the Teesny Audio Library private: - static constexpr float m_DEFAULT_DELAY_MS = 20.0f; ///< default average delay of chorus in milliseconds + static constexpr float m_DEFAULT_AVERAGE_DELAY_MS = 20.0f; ///< default average delay of chorus in milliseconds static constexpr float m_DELAY_RANGE = 15.0f; ///< default range of delay variation in milliseconds static constexpr float m_LFO_MIN_RATE = 2.0f; ///< slowest possible LFO rate in milliseconds static constexpr float m_LFO_RANGE = 8.0f; ///< fastest possible LFO rate in milliseconds @@ -171,20 +175,24 @@ private: bool m_enable = false; bool m_externalMemory = false; BALibrary::AudioDelay *m_memory = nullptr; + audio_block_t *m_previousBlock = nullptr; + audio_block_t *m_blockToRelease = nullptr; + BALibrary::LowFrequencyOscillatorVector m_lfo; size_t m_maxDelaySamples = 0; - audio_block_t *m_previousBlock = nullptr; - audio_block_t *m_blockToRelease = nullptr; + //size_t m_currentDelayOffset = 0; + float m_delayRange = 0; + BALibrary::IirBiQuadFilterHQ *m_iir = nullptr; // Controls int m_midiConfig[NUM_CONTROLS][2]; // stores the midi parameter mapping - size_t m_delaySamples = 0; + float m_averageDelaySamples = 0; float m_lfoDepth = 0.0f; float m_mix = 0.0f; float m_volume = 1.0f; - void m_preProcessing(audio_block_t *out, audio_block_t *dry, audio_block_t *wet); + void m_preProcessing (audio_block_t *out, audio_block_t *dry, audio_block_t *wet); void m_postProcessing(audio_block_t *out, audio_block_t *dry, audio_block_t *wet); // Coefficients diff --git a/src/LibBasicFunctions.h b/src/LibBasicFunctions.h index ee4ac5b..407e5c5 100644 --- a/src/LibBasicFunctions.h +++ b/src/LibBasicFunctions.h @@ -191,6 +191,20 @@ public: /// @returns true on success, false on error. bool interpolateDelay(int16_t *extendedSourceBuffer, int16_t *destBuffer, float fraction, size_t numSamples = AUDIO_BLOCK_SAMPLES); + /// Provides linearly interpolated samples between discrete samples in the sample buffer. The interpolation point for each samples + /// comes from a provided vector of floats which values between 0.0f and 1.0f; + /// 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 vector of values 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 interpolateDelayVector(int16_t *extendedSourceBuffer, int16_t *destBuffer, float *fractionVector, 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. diff --git a/src/common/AudioDelay.cpp b/src/common/AudioDelay.cpp index 3c1f704..7b30c5f 100644 --- a/src/common/AudioDelay.cpp +++ b/src/common/AudioDelay.cpp @@ -209,6 +209,22 @@ bool AudioDelay::interpolateDelay(int16_t *extendedSourceBuffer, int16_t *destBu return true; } +bool AudioDelay::interpolateDelayVector(int16_t *extendedSourceBuffer, int16_t *destBuffer, float *fractionVector, size_t numSamples) +{ + int16_t frac1Vec[numSamples]; + int16_t frac2Vec[numSamples]; + + for (int i=0; i(32767.0f * fractionVector[i]); + frac1Vec[i] = fracAsInt; + frac2Vec[i] = 32767-frac1Vec[i]; + + destBuffer[i] = (( frac1Vec[i] * extendedSourceBuffer[i]) >> 16) + ((frac2Vec[i] * extendedSourceBuffer[i+1]) >> 16); + } + + return true; +} + } diff --git a/src/effects/AudioEffectAnalogChorus.cpp b/src/effects/AudioEffectAnalogChorus.cpp index 5d67f6b..81b559d 100644 --- a/src/effects/AudioEffectAnalogChorus.cpp +++ b/src/effects/AudioEffectAnalogChorus.cpp @@ -5,24 +5,30 @@ * Author: slascos */ #include +#include #include "AudioEffectAnalogChorusFilters.h" #include "AudioEffectAnalogChorus.h" using namespace BALibrary; -//#define INTERPOLATED_DELAY Uncomment this line to test the inteprolated delay which adds 1/10th of a sample - namespace BAEffects { constexpr int MIDI_CHANNEL = 0; constexpr int MIDI_CONTROL = 1; +constexpr float DELAY_REFERENCE_F = static_cast(AUDIO_BLOCK_SAMPLES/2); + AudioEffectAnalogChorus::AudioEffectAnalogChorus() : AudioStream(1, m_inputQueueArray) { - m_memory = new AudioDelay(m_DEFAULT_DELAY_MS + m_DELAY_RANGE); - m_maxDelaySamples = calcAudioSamples(m_DEFAULT_DELAY_MS + m_DELAY_RANGE); + m_memory = new AudioDelay(m_DEFAULT_AVERAGE_DELAY_MS + m_DELAY_RANGE); + m_maxDelaySamples = calcAudioSamples(m_DEFAULT_AVERAGE_DELAY_MS + m_DELAY_RANGE); + m_averageDelaySamples = static_cast(calcAudioSamples(m_DEFAULT_AVERAGE_DELAY_MS)); + m_delayRange = static_cast(calcAudioSamples(m_DELAY_RANGE)); + m_constructFilter(); + m_lfo.setWaveform(Waveform::TRIANGLE); + m_lfo.setRateAudio(4.0f); // Default to 4 Hz } // requires preallocated memory large enough @@ -31,8 +37,13 @@ AudioEffectAnalogChorus::AudioEffectAnalogChorus(ExtMemSlot *slot) { m_memory = new AudioDelay(slot); m_maxDelaySamples = (slot->size() / sizeof(int16_t)); + m_averageDelaySamples = static_cast(calcAudioSamples(m_DEFAULT_AVERAGE_DELAY_MS)); + m_delayRange = static_cast(calcAudioSamples(m_DELAY_RANGE)); + m_externalMemory = true; m_constructFilter(); + m_lfo.setWaveform(Waveform::TRIANGLE); + m_lfo.setRateAudio(4.0f); // Default to 4 Hz } AudioEffectAnalogChorus::~AudioEffectAnalogChorus() @@ -48,6 +59,19 @@ void AudioEffectAnalogChorus::m_constructFilter(void) m_iir = new IirBiQuadFilterHQ(CE2_NUM_STAGES, reinterpret_cast(&CE2), CE2_COEFF_SHIFT); } +void AudioEffectAnalogChorus::setWaveform(BALibrary::Waveform waveform) +{ + switch(waveform) { + case Waveform::SINE : + case Waveform::TRIANGLE : + case Waveform::SAWTOOTH : + m_lfo.setWaveform(waveform); + break; + default : + Serial.println("AudioEffectAnalogChorus::setWaveform: Unsupported Waveform"); + } +} + void AudioEffectAnalogChorus::setFilterCoeffs(int numStages, const int32_t *coeffs, int coeffShift) { m_iir->changeFilterCoeffs(numStages, coeffs, coeffShift); @@ -75,7 +99,7 @@ void AudioEffectAnalogChorus::update(void) // Check is block is disabled if (m_enable == false) { - // do not transmit or process any audio, return as quickly as possible. + // do not transmit or proess any audio, return as quickly as possible. if (inputAudioBlock) release(inputAudioBlock); // release all held memory resources @@ -118,14 +142,29 @@ void AudioEffectAnalogChorus::update(void) // get the data. If using external memory with DMA, this won't be filled until // later. -#ifdef INTERPOLATED_DELAY - int16_t extendedBuffer[AUDIO_BLOCK_SAMPLES+1]; // need one more sample for intepolating between 128th and 129th (last sample) - m_memory->getSamples(extendedBuffer, m_delaySamples, AUDIO_BLOCK_SAMPLES+1); -#else - m_memory->getSamples(blockToOutput, m_delaySamples); -#endif - - + // We need to grab two blocks of audio since the modulating delay value from the LFO + // can exceed the length of one audio block during the time frame of one audio block. + int16_t extendedBuffer[(2*AUDIO_BLOCK_SAMPLES)]; // need one more sample for interpolating between 128th and 129th (last sample) + + // Get next vector of lfo values, they will range range from -1.0 to +1.0f. + float *lfoValues = m_lfo.getNextVector(); + //float lfoValues[128]; + for (int i=0; i<128; i++) { lfoValues[i] = lfoValues[i] * m_lfoDepth; } + + // Calculate the starting delay from the first lfo sample. This will represent the 'reference' delay + // for this output block + float referenceDelay = m_averageDelaySamples + (lfoValues[0] * m_delayRange); + unsigned delaySamples = static_cast(referenceDelay); // round down to the nearest audio sample for indexing into AudioDelay class + + // From a given current delay value, while reading out the next 128, the delay could slew up or down + // AUDIO_BLOCK_SAMPLES/2 cycles of delay. For example... + // Pitching up : current + 128 + 64 + // Pitching down: current - 64 + 128 + // We need to grab 2*AUDIO_BLOCK_SAMPLES. Be aware that audio samples are stored BACKWARDS in the buffers. +// m_memory->getSamples(extendedBuffer , delaySamples - (AUDIO_BLOCK_SAMPLES/2), AUDIO_BLOCK_SAMPLES); +// m_memory->getSamples(extendedBuffer + AUDIO_BLOCK_SAMPLES, delaySamples +( AUDIO_BLOCK_SAMPLES/2), AUDIO_BLOCK_SAMPLES); + m_memory->getSamples(extendedBuffer + AUDIO_BLOCK_SAMPLES, delaySamples - (AUDIO_BLOCK_SAMPLES/2), AUDIO_BLOCK_SAMPLES); + m_memory->getSamples(extendedBuffer , delaySamples +( AUDIO_BLOCK_SAMPLES/2), AUDIO_BLOCK_SAMPLES); // If using DMA, we need something else to do while that read executes, so // move on to input preprocessing @@ -142,16 +181,39 @@ void AudioEffectAnalogChorus::update(void) // BACK TO OUTPUT PROCESSING // Check if external DMA, if so, we need to be sure the read is completed if (m_externalMemory && m_memory->getSlot()->isUseDma()) { - // Using DMA + // Using DMA so we have to busy-wait here until DMA is done while (m_memory->getSlot()->isReadBusy()) {} } -#ifdef INTERPOLATED_DELAY - // TODO: partial delay testing - // extendedBuffer is oversized - //memcpy(blockToOutput->data, &extendedBuffer[1], sizeof(int16_t)*AUDIO_BLOCK_SAMPLES); - m_memory->interpolateDelay(extendedBuffer, blockToOutput->data, 0.1f, AUDIO_BLOCK_SAMPLES); -#endif + double bufferIndexFloat; + int delayIndex; + for (int i=0, j=AUDIO_BLOCK_SAMPLES-1; i(bufferIndexFloat); + if ( (delayIndex < 0) || (delayIndex > 256) ) { + Serial.println(String("lfoValues[") + i + String("]:") + lfoValues[i] + + String(" referenceDelay:") + referenceDelay + + String(" bufferPosition:") + bufferPosition + + String(" delayIndex:") + delayIndex) ; + } + + //delayIndex = 64+i; + blockToOutput->data[j] = static_cast( + (static_cast(extendedBuffer[j+delayIndex]) * fraction1) + + (static_cast(extendedBuffer[j+delayIndex+1]) * fraction2) ); + //blockToOutput->data[i] = extendedBuffer[64+i]; + } // perform the wet/dry mix mix m_postProcessing(blockToOutput, inputAudioBlock, blockToOutput); @@ -174,6 +236,7 @@ void AudioEffectAnalogChorus::update(void) void AudioEffectAnalogChorus::m_preProcessing(audio_block_t *out, audio_block_t *dry, audio_block_t *wet) { memcpy(out->data, dry->data, sizeof(int16_t) * AUDIO_BLOCK_SAMPLES); + // TODO: Clean this up with proper preprocessing // if ( out && dry && wet) { // alphaBlend(out, dry, wet, m_feedback); // m_iir->process(out->data, out->data, AUDIO_BLOCK_SAMPLES); @@ -200,45 +263,25 @@ void AudioEffectAnalogChorus::m_postProcessing(audio_block_t *out, audio_block_t void AudioEffectAnalogChorus::setDelayConfig(float averageDelayMs, float delayRangeMs) { - size_t delaySamples = calcAudioSamples(averageDelayMs + delayRangeMs); - - if (delaySamples > m_memory->getMaxDelaySamples()) { - // this exceeds max delay value, limit it. - delaySamples = m_memory->getMaxDelaySamples(); - } - - if (!m_memory) { Serial.println("delay(): m_memory is not valid"); } - - if (!m_externalMemory) { - // internal memory - // Do nothing - } else { - // external memory - ExtMemSlot *slot = m_memory->getSlot(); - - if (!slot) { Serial.println("ERROR: slot ptr is not valid"); } - if (!slot->isEnabled()) { - slot->enable(); - Serial.println("WEIRD: slot was not enabled"); - } - } + setDelayConfig(calcAudioSamples(averageDelayMs), calcAudioSamples(delayRangeMs)); } void AudioEffectAnalogChorus::setDelayConfig(size_t averageDelayNumSamples, size_t delayRangeNumSamples) { size_t delaySamples = averageDelayNumSamples + delayRangeNumSamples; + m_averageDelaySamples = averageDelayNumSamples; + m_delayRange = delayRangeNumSamples; if (delaySamples > m_memory->getMaxDelaySamples()) { // this exceeds max delay value, limit it. delaySamples = m_memory->getMaxDelaySamples(); + m_averageDelaySamples = delaySamples/2; + m_delayRange = delaySamples/2; } if (!m_memory) { Serial.println("delay(): m_memory is not valid"); } - if (!m_externalMemory) { - // internal memory - // Do nothing - } else { + if (m_externalMemory) { // external memory ExtMemSlot *slot = m_memory->getSlot();