diff --git a/src/AudioEffectSOS.h b/src/AudioEffectSOS.h new file mode 100644 index 0000000..841f1f8 --- /dev/null +++ b/src/AudioEffectSOS.h @@ -0,0 +1,136 @@ +/**************************************************************************//** + * @file + * @author Steve Lascos + * @company Blackaddr Audio + * + * AudioEffectSOS is a class f + * + * @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 . + *****************************************************************************/ + +#ifndef __BAGUITAR_BAAUDIOEFFECTSOS_H +#define __BAGUITAR_BAAUDIOEFFECTSOS_H + +#include +#include "LibBasicFunctions.h" + +namespace BAEffects { + +/**************************************************************************//** + * AudioEffectSOS + *****************************************************************************/ +class AudioEffectSOS : public AudioStream { +public: + + ///< List of AudioEffectAnalogDelay MIDI controllable parameters + enum { + BYPASS = 0, ///< controls effect bypass + GATE_TRIGGER, ///< begins the gate sequence + GATE_OPEN_TIME, ///< controls how long it takes to open the gate + GATE_CLOSE_TIME, ///< controls how long it takes to close the gate (release) + FEEDBACK, ///< controls the amount of feedback, more gives longer SOS sustain + VOLUME, ///< controls the output volume level + NUM_CONTROLS ///< this can be used as an alias for the number of MIDI controls + }; + + // *** CONSTRUCTORS *** + AudioEffectSOS() = delete; + + /// Construct an analog delay using external SPI via an ExtMemSlot. The amount of + /// delay will be determined by the amount of memory in the slot. + /// @param slot A pointer to the ExtMemSlot to use for the delay. + AudioEffectSOS(BAGuitar::ExtMemSlot *slot); // requires sufficiently sized pre-allocated memory + + virtual ~AudioEffectSOS(); ///< Destructor + + // *** PARAMETERS *** + void gateOpenTime(float milliseconds); + + void gateCloseTime(float milliseconds); + + void feedback(float feedback) { m_feedback = feedback; } + + /// Bypass the effect. + /// @param byp when true, bypass wil disable the effect, when false, effect is enabled. + /// Note that audio still passes through when bypass is enabled. + void bypass(bool byp) { m_bypass = byp; } + + /// Set the output volume. This affect both the wet and dry signals. + /// @details The default is 1.0. + /// @param vol Sets the output volume between -1.0 and +1.0 + void volume(float vol) {m_volume = vol; } + + // ** ENABLE / DISABLE ** + + /// Enables audio processing. Note: when not enabled, CPU load is nearly zero. + void enable() { m_enable = true; } + + /// Disables audio process. When disabled, CPU load is nearly zero. + void disable() { m_enable = false; } + + // ** MIDI ** + + /// Sets whether MIDI OMNI channel is processig on or off. When on, + /// all midi channels are used for matching CCs. + /// @param isOmni when true, all channels are processed, when false, channel + /// must match configured value. + void setMidiOmni(bool isOmni) { m_isOmni = isOmni; } + + /// Configure an effect parameter to be controlled by a MIDI CC + /// number on a particular channel. + /// @param parameter one of the parameter names in the class enum + /// @param midiCC the CC number from 0 to 127 + /// @param midiChannel the effect will only response to the CC on this channel + /// when OMNI mode is off. + void mapMidiControl(int parameter, int midiCC, int midiChannel = 0); + + /// process a MIDI Continous-Controller (CC) message + /// @param channel the MIDI channel from 0 to 15) + /// @param midiCC the CC number from 0 to 127 + /// @param value the CC value from 0 to 127 + void processMidi(int channel, int midiCC, int value); + + virtual void update(void); ///< update automatically called by the Teesny Audio Library + +private: + audio_block_t *m_inputQueueArray[1]; + bool m_isOmni = false; + bool m_bypass = true; + bool m_enable = false; + BAGuitar::AudioDelay *m_memory = nullptr; + bool m_externalMemory = true; + audio_block_t *m_previousBlock = nullptr; + audio_block_t *m_blockToRelease = nullptr; + size_t m_maxDelaySamples = 0; + + // Controls + int m_midiConfig[NUM_CONTROLS][2]; // stores the midi parameter mapping + size_t m_delaySamples = 0; + float m_openTimeMs = 0.0f; + float m_closeTimeMs = 0.0f; + float m_feedback = 0.0f; + float m_volume = 1.0f; + + // Automated Controls + BALibrary::ParameterAutomation m_inputGateAuto = + BALibrary::ParameterAutomation(0.0f, 1.0f, 0.0f, BALibrary::ParameterAutomation::Function::LINEAR); + + // Private functions + void m_preProcessing (audio_block_t *out, audio_block_t *input, audio_block_t *delayedSignal); + //void m_postProcessing(audio_block_t *out, audio_block_t *dry, audio_block_t *wet); +}; + +} + +#endif /* __BAGUITAR_BAAUDIOEFFECTANALOGDELAY_H */ diff --git a/src/BAGuitar.h b/src/BAGuitar.h index 1fc8319..d4c4622 100644 --- a/src/BAGuitar.h +++ b/src/BAGuitar.h @@ -29,6 +29,7 @@ #include "BAGpio.h" #include "BAAudioEffectDelayExternal.h" #include "AudioEffectAnalogDelay.h" +#include "AudioEffectSOS.h" #include "LibBasicFunctions.h" #include "LibMemoryManagement.h" diff --git a/src/LibBasicFunctions.h b/src/LibBasicFunctions.h index 0507e43..26e9c64 100644 --- a/src/LibBasicFunctions.h +++ b/src/LibBasicFunctions.h @@ -92,9 +92,15 @@ void alphaBlend(audio_block_t *out, audio_block_t *dry, audio_block_t* wet, floa /// @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 shiftt the coefficient +/// @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. @@ -246,7 +252,7 @@ public: /// 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 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); @@ -297,7 +303,65 @@ private: }; -} +} // namespace BAGuitar + +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 { + LINEAR = 0, ///< f(x) = x + EXPONENTIAL, ///< f(x) = e^x + LOGARITHMIC, ///< f(x) = ln(x) + PARABOLIC, ///< f(x) = x^2 + LOOKUP_TABLE ///< f(x) = lut(x) + }; + ParameterAutomation() = delete; + ParameterAutomation(T startValue, T endValue, size_t durationSamples, Function function = Function::LINEAR); + ParameterAutomation(T startValue, T endValue, float durationMilliseconds, Function function = Function::LINEAR); + ~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(); + +private: + Function m_function; + T m_startValue; + T m_endValue; + bool m_running = false; + T m_currentValueX; ///< the current value of x in f(x) + size_t m_duration; + T m_coeffs[3]; ///< some general coefficient storage +}; + + +// TODO: initialize with const number of sequences with null type that automatically skips +// then register each new sequence. +template +class ParameterAutomationSequence +{ + +}; + +} // BALibrary #endif /* __BAGUITAR_LIBBASICFUNCTIONS_H */ diff --git a/src/common/AudioHelpers.cpp b/src/common/AudioHelpers.cpp index ae7cfcf..785e5db 100644 --- a/src/common/AudioHelpers.cpp +++ b/src/common/AudioHelpers.cpp @@ -69,6 +69,11 @@ void gainAdjust(audio_block_t *out, audio_block_t *in, float vol, int coeffShift arm_scale_q15(in->data, scale, coeffShift, out->data, AUDIO_BLOCK_SAMPLES); } +void combine(audio_block_t *out, audio_block_t *in0, audio_block_t *in1) +{ + arm_add_q15 (in0->data, in1->data, out->data, AUDIO_BLOCK_SAMPLES); +} + void clearAudioBlock(audio_block_t *block) { memset(block->data, 0, sizeof(int16_t)*AUDIO_BLOCK_SAMPLES); diff --git a/src/common/ParameterAutomation.cpp b/src/common/ParameterAutomation.cpp new file mode 100644 index 0000000..9bf8177 --- /dev/null +++ b/src/common/ParameterAutomation.cpp @@ -0,0 +1,110 @@ +/* + * ParameterAutomation.cpp + * + * Created on: April 14, 2018 + * Author: slascos + * + * 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 "LibBasicFunctions.h" + +using namespace BAGuitar; + +namespace BALibrary { + +constexpr int LINEAR_SLOPE = 0; + +template +ParameterAutomation::ParameterAutomation(T startValue, T endValue, float durationMilliseconds, Function function) +{ + reconfigure(startValue, endValue, calcAudioSamples(durationMilliseconds), function); +} + +template +ParameterAutomation::ParameterAutomation(T startValue, T endValue, size_t durationSamples, Function function) +{ + reconfigure(startValue, endValue, durationSamples, function); +} + +template +void ParameterAutomation::reconfigure(T startValue, T endValue, float durationMilliseconds, Function function) +{ + reconfigure(startValue, endValue, calcAudioSamples(durationMilliseconds), function); +} + +template +void ParameterAutomation::reconfigure(T startValue, T endValue, size_t durationSamples, Function function) +{ + m_function = function; + m_startValue = startValue; + m_endValue = endValue; + m_currentValueX = startValue; + m_duration = durationSamples; + + // Pre-compute any necessary coefficients + switch(m_function) { + case Function::EXPONENTIAL : + break; + case Function::LOGARITHMIC : + break; + case Function::PARABOLIC : + break; + case Function::LOOKUP_TABLE : + break; + + // Default will be same as LINEAR + case Function::LINEAR : + default : + m_coeffs[LINEAR_SLOPE] = (endValue - startValue) / static_cast(m_duration); + break; + } +} + + +template +void ParameterAutomation::trigger() +{ + m_currentValueX = m_startValue; + m_running = true; +} + +template +T ParameterAutomation::getNextValue() +{ + switch(m_function) { + case Function::EXPONENTIAL : + break; + case Function::LOGARITHMIC : + break; + case Function::PARABOLIC : + break; + case Function::LOOKUP_TABLE : + break; + + // Default will be same as LINEAR + case Function::LINEAR : + default : + // output = m_currentValueX + slope + m_currentValueX += m_coeffs[LINEAR_SLOPE]; + if (m_currentValueX >= m_endValue) { + m_currentValueX = m_endValue; + m_running = false; + } + break; + } + return m_currentValueX; +} + +} diff --git a/src/effects/AudioEffectSOS.cpp b/src/effects/AudioEffectSOS.cpp new file mode 100644 index 0000000..f8153ce --- /dev/null +++ b/src/effects/AudioEffectSOS.cpp @@ -0,0 +1,221 @@ +/* + * AudioEffectSOS.cpp + * + * Created on: Apr 14, 2018 + * Author: blackaddr + */ + +#include "AudioEffectSOS.h" +#include "LibBasicFunctions.h" + +using namespace BAGuitar; +using namespace BALibrary; + +namespace BAEffects { + +constexpr int MIDI_CHANNEL = 0; +constexpr int MIDI_CONTROL = 1; + +constexpr float MAX_GATE_OPEN_TIME_MS = 3000.0f; +constexpr float MAX_GATE_CLOSE_TIME_MS = 3000.0f; + +AudioEffectSOS::AudioEffectSOS(ExtMemSlot *slot) +: AudioStream(1, m_inputQueueArray) +{ + m_memory = new AudioDelay(slot); + m_maxDelaySamples = (slot->size() / sizeof(int16_t)); + m_delaySamples = m_maxDelaySamples; + m_externalMemory = true; +} + +AudioEffectSOS::~AudioEffectSOS() +{ + +} + +void AudioEffectSOS::update(void) +{ + audio_block_t *inputAudioBlock = receiveReadOnly(); // get the next block of input samples + + // Check is block is disabled + if (m_enable == false) { + // do not transmit or process any audio, return as quickly as possible. + if (inputAudioBlock) release(inputAudioBlock); + + // release all held memory resources + if (m_previousBlock) { + release(m_previousBlock); m_previousBlock = nullptr; + } + if (!m_externalMemory) { + // when using internal memory we have to release all references in the ring buffer + while (m_memory->getRingBuffer()->size() > 0) { + audio_block_t *releaseBlock = m_memory->getRingBuffer()->front(); + m_memory->getRingBuffer()->pop_front(); + if (releaseBlock) release(releaseBlock); + } + } + return; + } + + // Check is block is bypassed, if so either transmit input directly or create silence + if (m_bypass == true) { + // transmit the input directly + if (!inputAudioBlock) { + // create silence + inputAudioBlock = allocate(); + if (!inputAudioBlock) { return; } // failed to allocate + else { + clearAudioBlock(inputAudioBlock); + } + } + transmit(inputAudioBlock, 0); + release(inputAudioBlock); + return; + } + + // Otherwise perform normal processing + // In order to make use of the SPI DMA, we need to request the read from memory first, + // then do other processing while it fills in the back. + audio_block_t *blockToOutput = nullptr; // this will hold the output audio + blockToOutput = allocate(); + if (!blockToOutput) return; // skip this update cycle due to failure + + // get the data. If using external memory with DMA, this won't be filled until + // later. + m_memory->getSamples(blockToOutput, m_delaySamples); + + // If using DMA, we need something else to do while that read executes, so + // move on to input preprocessing + + // Preprocessing + audio_block_t *preProcessed = allocate(); + // mix the input with the feedback path in the pre-processing stage + m_preProcessing(preProcessed, inputAudioBlock, m_previousBlock); + + // consider doing the BBD post processing here to use up more time while waiting + // for the read data to come back + audio_block_t *blockToRelease = m_memory->addBlock(preProcessed); + + + // 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 + while (m_memory->getSlot()->isReadBusy()) {} + } + + // perform the wet/dry mix mix + //m_postProcessing(blockToOutput, inputAudioBlock, blockToOutput); + transmit(blockToOutput); + + release(inputAudioBlock); + release(m_previousBlock); + m_previousBlock = blockToOutput; + + if (m_blockToRelease) release(m_blockToRelease); + m_blockToRelease = blockToRelease; +} + + +void AudioEffectSOS::gateOpenTime(float milliseconds) +{ + // TODO - change the paramter automation to an automation sequence + m_openTimeMs = milliseconds; + //m_inputGateAuto.reconfigure(); +} + +void AudioEffectSOS::gateCloseTime(float milliseconds) +{ + m_closeTimeMs = milliseconds; +} + +//////////////////////////////////////////////////////////////////////// +// MIDI PROCESSING +//////////////////////////////////////////////////////////////////////// +void AudioEffectSOS::processMidi(int channel, int control, int value) +{ + + float val = (float)value / 127.0f; + + if ((m_midiConfig[GATE_OPEN_TIME][MIDI_CHANNEL] == channel) && + (m_midiConfig[GATE_OPEN_TIME][MIDI_CONTROL] == control)) { + // Gate Open Time + gateOpenTime(val * MAX_GATE_OPEN_TIME_MS); + Serial.println(String("AudioEffectSOS::gate open time (ms): ") + m_openTimeMs); + return; + } + + if ((m_midiConfig[GATE_CLOSE_TIME][MIDI_CHANNEL] == channel) && + (m_midiConfig[GATE_CLOSE_TIME][MIDI_CONTROL] == control)) { + // Gate Close Time + gateCloseTime(val * MAX_GATE_CLOSE_TIME_MS); + Serial.println(String("AudioEffectSOS::gate close time (ms): ") + m_openTimeMs); + return; + } + + if ((m_midiConfig[FEEDBACK][MIDI_CHANNEL] == channel) && + (m_midiConfig[FEEDBACK][MIDI_CONTROL] == control)) { + // Feedback + Serial.println(String("AudioEffectSOS::feedback: ") + 100*val + String("%")); + feedback(val); + return; + } + + if ((m_midiConfig[VOLUME][MIDI_CHANNEL] == channel) && + (m_midiConfig[VOLUME][MIDI_CONTROL] == control)) { + // Volume + Serial.println(String("AudioEffectSOS::volume: ") + 100*val + String("%")); + volume(val); + return; + } + + if ((m_midiConfig[BYPASS][MIDI_CHANNEL] == channel) && + (m_midiConfig[BYPASS][MIDI_CONTROL] == control)) { + // Bypass + if (value >= 65) { bypass(false); Serial.println(String("AudioEffectSOS::not bypassed -> ON") + value); } + else { bypass(true); Serial.println(String("AudioEffectSOS::bypassed -> OFF") + value); } + return; + } + + if ((m_midiConfig[GATE_TRIGGER][MIDI_CHANNEL] == channel) && + (m_midiConfig[GATE_TRIGGER][MIDI_CONTROL] == control)) { + // The gate is trigged by any value + m_inputGateAuto.trigger(); + Serial.println(String("AudioEffectSOS::Gate Triggered!")); + return; + } +} + +void AudioEffectSOS::mapMidiControl(int parameter, int midiCC, int midiChannel) +{ + if (parameter >= NUM_CONTROLS) { + return ; // Invalid midi parameter + } + m_midiConfig[parameter][MIDI_CHANNEL] = midiChannel; + m_midiConfig[parameter][MIDI_CONTROL] = midiCC; +} + +////////////////////////////////////////////////////////////////////// +// PRIVATE FUNCTIONS +////////////////////////////////////////////////////////////////////// +void AudioEffectSOS::m_preProcessing (audio_block_t *out, audio_block_t *input, audio_block_t *delayedSignal) +{ + if ( out && input && delayedSignal) { + // Multiply the input signal by the automated gate value + // Multiply the delayed signal by the user set feedback value + // then mix together. + float gateVol = m_inputGateAuto.getNextValue(); + audio_block_t tempAudioBuffer; + gainAdjust(out, input, gateVol, 0); // last paremeter is coeff shift, 0 bits + gainAdjust(&tempAudioBuffer, delayedSignal, m_feedback, 0); // last paremeter is coeff shift, 0 bits + combine(out, out, &tempAudioBuffer); + } else if (input) { + memcpy(out->data, input->data, sizeof(int16_t) * AUDIO_BLOCK_SAMPLES); + } +} + + +} // namespace BAEffects + + +