Basic working chorus, still needs range tweaking, LFO filtering, etc.

feature/AudioEffectAnalogChorus
Steve Lascos 5 years ago
parent 9b09021ef0
commit 7bf2ed6906
  1. 26
      examples/Modulation/AnalogChorusDemoExpansion/AnalogChorusDemoExpansion.ino
  2. 22
      src/AudioEffectAnalogChorus.h
  3. 14
      src/LibBasicFunctions.h
  4. 16
      src/common/AudioDelay.cpp
  5. 135
      src/effects/AudioEffectAnalogChorus.cpp

@ -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<AudioEffectAnalogChorus::Filter>(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<AudioEffectAnalogChorus::Filter>(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<Waveform>(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);

@ -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<float> 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

@ -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.

@ -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<numSamples; i++) {
int16_t fracAsInt = static_cast<int16_t>(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;
}
}

@ -5,24 +5,30 @@
* Author: slascos
*/
#include <new>
#include <cmath>
#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<float>(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<float>(calcAudioSamples(m_DEFAULT_AVERAGE_DELAY_MS));
m_delayRange = static_cast<float>(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<float>(calcAudioSamples(m_DEFAULT_AVERAGE_DELAY_MS));
m_delayRange = static_cast<float>(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<const int32_t *>(&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<unsigned>(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<AUDIO_BLOCK_SAMPLES; i++,j--) {
// each output sample will be an interpolated value between two samples
// the precise delay value will be based on the LFO vector values.
// For each output sample, calculate the floating point delay offset from the reference delay.
// This will be an offset from location AUDIO_BLOCK_SAMPLES/2 (e.g. 64) in the buffer.
float offsetDelayFromRef = m_averageDelaySamples + (lfoValues[i] * m_delayRange) - referenceDelay;
float bufferPosition = DELAY_REFERENCE_F + offsetDelayFromRef;
// Get the interpolation coefficients from the fractional part of the buffer position
float fraction1 = modf(bufferPosition, &bufferIndexFloat);
float fraction2 = 1.0f - fraction1;
//fraction1 = 0.5f;
//fraction2 = 0.5f;
delayIndex = static_cast<unsigned>(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<int16_t>(
(static_cast<float>(extendedBuffer[j+delayIndex]) * fraction1) +
(static_cast<float>(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();

Loading…
Cancel
Save