diff --git a/src/AudioEffectSOS.h b/src/AudioEffectSOS.h index 841f1f8..1ee8f99 100644 --- a/src/AudioEffectSOS.h +++ b/src/AudioEffectSOS.h @@ -46,6 +46,8 @@ public: // *** CONSTRUCTORS *** AudioEffectSOS() = delete; + AudioEffectSOS(float maxDelayMs); + AudioEffectSOS(size_t numSamples); /// 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. @@ -66,6 +68,9 @@ public: /// Note that audio still passes through when bypass is enabled. void bypass(bool byp) { m_bypass = byp; } + /// Activate the gate automation. Input gate will open, then close. + void trigger() { m_inputGateAuto.trigger(); } + /// 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 @@ -74,7 +79,7 @@ public: // ** ENABLE / DISABLE ** /// Enables audio processing. Note: when not enabled, CPU load is nearly zero. - void enable() { m_enable = true; } + void enable(); /// Disables audio process. When disabled, CPU load is nearly zero. void disable() { m_enable = false; } @@ -123,12 +128,12 @@ private: float m_volume = 1.0f; // Automated Controls - BALibrary::ParameterAutomation m_inputGateAuto = - BALibrary::ParameterAutomation(0.0f, 1.0f, 0.0f, BALibrary::ParameterAutomation::Function::LINEAR); + BALibrary::ParameterAutomationSequence m_inputGateAuto = + BALibrary::ParameterAutomationSequence(3); // 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); + void m_postProcessing(audio_block_t *out, audio_block_t *input); }; } diff --git a/src/BAHardware.h b/src/BAHardware.h index 5637bca..bb3670d 100644 --- a/src/BAHardware.h +++ b/src/BAHardware.h @@ -74,11 +74,17 @@ constexpr size_t MEM_MAX_ADDR[NUM_MEM_SLOTS] = { 131071, 131071 }; /**************************************************************************//** * General Purpose SPI Interfaces *****************************************************************************/ -enum SpiDeviceId : unsigned { +enum class SpiDeviceId : unsigned { SPI_DEVICE0 = 0, ///< Arduino SPI device SPI_DEVICE1 = 1 ///< Arduino SPI1 device }; constexpr int SPI_MAX_ADDR = 131071; ///< Max address size per chip +constexpr size_t SPI_MEM0_SIZE_BYTES = 131072; +constexpr size_t SPI_MEM0_MAX_AUDIO_SAMPLES = SPI_MEM0_SIZE_BYTES/sizeof(int16_t); + +constexpr size_t SPI_MEM1_SIZE_BYTES = 131072; +constexpr size_t SPI_MEM1_MAX_AUDIO_SAMPLES = SPI_MEM1_SIZE_BYTES/sizeof(int16_t); + #else diff --git a/src/LibBasicFunctions.h b/src/LibBasicFunctions.h index 0afa625..140022f 100644 --- a/src/LibBasicFunctions.h +++ b/src/LibBasicFunctions.h @@ -153,6 +153,12 @@ public: /// @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. /// @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. @@ -167,6 +173,8 @@ public: /// @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 @@ -183,6 +191,7 @@ private: 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. }; /**************************************************************************//** @@ -317,6 +326,7 @@ class ParameterAutomation public: enum class Function : unsigned { NOT_CONFIGURED = 0, ///< Initial, unconfigured stage + HOLD, ///< f(x) = constant LINEAR, ///< f(x) = x EXPONENTIAL, ///< f(x) = e^x LOGARITHMIC, ///< f(x) = ln(x) @@ -350,9 +360,10 @@ private: T m_startValue; T m_endValue; bool m_running = false; - T m_currentValueX; ///< the current value of x in f(x) + float m_currentValueX; ///< the current value of x in f(x) size_t m_duration; - T m_coeffs[3]; ///< some general coefficient storage + float m_coeffs[3]; ///< some general coefficient storage + bool m_positiveSlope = true; }; @@ -380,6 +391,7 @@ private: ParameterAutomation *m_paramArray[MAX_PARAMETER_SEQUENCES]; int m_currentIndex = 0; int m_numStages = 0; + bool m_running = false; }; } // BALibrary diff --git a/src/common/AudioDelay.cpp b/src/common/AudioDelay.cpp index 704233c..b3b4e39 100644 --- a/src/common/AudioDelay.cpp +++ b/src/common/AudioDelay.cpp @@ -34,6 +34,7 @@ AudioDelay::AudioDelay(size_t maxSamples) // INTERNAL memory consisting of audio_block_t data structures. QueuePosition pos = calcQueuePosition(maxSamples); m_ringBuffer = new RingBuffer(pos.index+2); // If the delay is in queue x, we need to overflow into x+1, thus x+2 total buffers. + m_maxDelaySamples = maxSamples; } AudioDelay::AudioDelay(float maxDelayTimeMs) @@ -46,6 +47,7 @@ AudioDelay::AudioDelay(ExtMemSlot *slot) { m_type = (MemType::MEM_EXTERNAL); m_slot = slot; + m_maxDelaySamples = (slot->size() / sizeof(int16_t)) - AUDIO_BLOCK_SAMPLES; } AudioDelay::~AudioDelay() @@ -93,6 +95,11 @@ audio_block_t* AudioDelay::getBlock(size_t index) return ret; } +size_t AudioDelay::getMaxDelaySamples() +{ + return m_maxDelaySamples; +} + bool AudioDelay::getSamples(audio_block_t *dest, size_t offsetSamples, size_t numSamples) { if (!dest) { @@ -159,8 +166,9 @@ bool AudioDelay::getSamples(audio_block_t *dest, size_t offsetSamples, size_t nu return true; } else { - // numSampmles is > than total slot size + // numSamples is > than total slot size Serial.println("getSamples(): ERROR numSamples > total slot size"); + Serial.println(numSamples + String(" > ") + m_slot->size()); return false; } } diff --git a/src/common/ExternalSramManager.cpp b/src/common/ExternalSramManager.cpp index cc0f7f9..98465fc 100644 --- a/src/common/ExternalSramManager.cpp +++ b/src/common/ExternalSramManager.cpp @@ -37,8 +37,8 @@ ExternalSramManager::ExternalSramManager(unsigned numMemories) // Initialize the static memory configuration structs if (!m_configured) { for (unsigned i=0; i < NUM_MEM_SLOTS; i++) { - m_memConfig[i].size = MEM_MAX_ADDR[i]; - m_memConfig[i].totalAvailable = MEM_MAX_ADDR[i]; + m_memConfig[i].size = MEM_MAX_ADDR[i]+1; + m_memConfig[i].totalAvailable = MEM_MAX_ADDR[i]+1; m_memConfig[i].nextAvailable = 0; m_memConfig[i].m_spi = nullptr; @@ -111,7 +111,9 @@ bool ExternalSramManager::requestMemory(ExtMemSlot *slot, size_t sizeBytes, BAGu return true; } else { // there is not enough memory available for the request - + Serial.println(String("ExternalSramManager::requestMemory(): Insufficient memory in slot, request/available: ") + + sizeBytes + String(" : ") + + m_memConfig[mem].totalAvailable); return false; } } diff --git a/src/common/ParameterAutomation.cpp b/src/common/ParameterAutomation.cpp index 0a59647..f56ce99 100644 --- a/src/common/ParameterAutomation.cpp +++ b/src/common/ParameterAutomation.cpp @@ -28,6 +28,7 @@ namespace BALibrary { // ParameterAutomation /////////////////////////////////////////////////////////////////////////////// constexpr int LINEAR_SLOPE = 0; + template ParameterAutomation::ParameterAutomation() { @@ -64,10 +65,20 @@ void ParameterAutomation::reconfigure(T startValue, T endValue, size_t durati m_function = function; m_startValue = startValue; m_endValue = endValue; - m_currentValueX = startValue; + m_currentValueX = static_cast(startValue); m_duration = durationSamples; m_running = false; + if (endValue >= startValue) { + // value is increasing + m_positiveSlope = true; + } else { + // value is decreasing + m_positiveSlope = false; + } + + float duration = m_duration / static_cast(AUDIO_BLOCK_SAMPLES); + // Pre-compute any necessary coefficients switch(m_function) { case Function::EXPONENTIAL : @@ -80,9 +91,14 @@ void ParameterAutomation::reconfigure(T startValue, T endValue, size_t durati break; // Default will be same as LINEAR + case Function::HOLD : + m_coeffs[LINEAR_SLOPE] = (1.0f / static_cast(duration)); // convert duration from ms to sec + break; case Function::LINEAR : default : - m_coeffs[LINEAR_SLOPE] = (endValue - startValue) / static_cast(m_duration); + // The number of parameter updates will be duration in samples divided by audio sample block size since + // we only update once per block. + m_coeffs[LINEAR_SLOPE] = static_cast(endValue - startValue) / duration; // convert duration from ms to sec break; } } @@ -91,13 +107,33 @@ void ParameterAutomation::reconfigure(T startValue, T endValue, size_t durati template void ParameterAutomation::trigger() { - m_currentValueX = m_startValue; + if (m_function == Function::HOLD) { + // The HOLD function will move currentValueX from 0 to 1.0 over the desired duration, + // but will always return the startValue. + m_currentValueX = 0.0f; + } else { + m_currentValueX = static_cast(m_startValue); + } m_running = true; + //Serial.println("ParameterAutomation::trigger() called"); } template T ParameterAutomation::getNextValue() { + if (m_running == false) { + return m_startValue; + } + + if (m_function == Function::HOLD) { + // HOLD is treated as a special case + m_currentValueX += m_coeffs[LINEAR_SLOPE]; + if (m_currentValueX >= 1.0) { + m_running = false; + } + return m_startValue; + } + switch(m_function) { case Function::EXPONENTIAL : break; @@ -107,24 +143,28 @@ T ParameterAutomation::getNextValue() 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; + + // Check if the automation is finished. + if ( ( m_positiveSlope && (m_currentValueX >= m_endValue)) || + (!m_positiveSlope && (m_currentValueX <= m_endValue)) ) { + m_running = false; + return m_endValue; + } else { + return static_cast(m_currentValueX); + } } // Template instantiation //template class MyStack; template class ParameterAutomation; +template class ParameterAutomation; +template class ParameterAutomation; /////////////////////////////////////////////////////////////////////////////// // ParameterAutomationSequence @@ -132,7 +172,6 @@ template class ParameterAutomation; template ParameterAutomationSequence::ParameterAutomationSequence(int numStages) { - //m_paramArray = malloc(sizeof(ParameterAutomation*) * numStages); if (numStages < MAX_PARAMETER_SEQUENCES) { for (int i=0; i(); @@ -147,35 +186,63 @@ ParameterAutomationSequence::ParameterAutomationSequence(int numStages) template ParameterAutomationSequence::~ParameterAutomationSequence() { - //if (m_paramArray) { - for (int i=0; i void ParameterAutomationSequence::setupParameter(int index, T startValue, T endValue, size_t durationSamples, typename ParameterAutomation::Function function) { m_paramArray[index]->reconfigure(startValue, endValue, durationSamples, function); + m_currentIndex = 0; } template void ParameterAutomationSequence::setupParameter(int index, T startValue, T endValue, float durationMilliseconds, typename ParameterAutomation::Function function) { m_paramArray[index]->reconfigure(startValue, endValue, durationMilliseconds, function); + m_currentIndex = 0; } template void ParameterAutomationSequence::trigger(void) { m_currentIndex = 0; - for (int i=0; itrigger(); + m_paramArray[0]->trigger(); + m_running = true; + //Serial.println("ParameterAutomationSequence::trigger() called"); +} + +template +T ParameterAutomationSequence::getNextValue() +{ + // Get the next value + T nextValue = m_paramArray[m_currentIndex]->getNextValue(); + + if (m_running) { + //Serial.println(String("ParameterAutomationSequence::getNextValue() is ") + nextValue + // + String(" from stage ") + m_currentIndex); + + // If current stage is done, trigger the next + if (m_paramArray[m_currentIndex]->isFinished()) { + Serial.println(String("Finished stage ") + m_currentIndex); + m_currentIndex++; + + if (m_currentIndex >= m_numStages) { + // Last stage already finished + m_running = false; + m_currentIndex = 0; + } else { + // trigger the next stage + m_paramArray[m_currentIndex]->trigger(); + } + } } + + return nextValue; } template @@ -188,6 +255,7 @@ bool ParameterAutomationSequence::isFinished() break; } } + m_running = !finished; return finished; } diff --git a/src/effects/AudioEffectAnalogDelay.cpp b/src/effects/AudioEffectAnalogDelay.cpp index 6cf1ead..5a15d4d 100644 --- a/src/effects/AudioEffectAnalogDelay.cpp +++ b/src/effects/AudioEffectAnalogDelay.cpp @@ -166,6 +166,11 @@ void AudioEffectAnalogDelay::delay(float milliseconds) { size_t delaySamples = calcAudioSamples(milliseconds); + 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) { diff --git a/src/effects/AudioEffectSOS.cpp b/src/effects/AudioEffectSOS.cpp index f8153ce..bd7ba33 100644 --- a/src/effects/AudioEffectSOS.cpp +++ b/src/effects/AudioEffectSOS.cpp @@ -19,18 +19,51 @@ constexpr int MIDI_CONTROL = 1; constexpr float MAX_GATE_OPEN_TIME_MS = 3000.0f; constexpr float MAX_GATE_CLOSE_TIME_MS = 3000.0f; +constexpr int GATE_OPEN_STAGE = 0; +constexpr int GATE_HOLD_STAGE = 1; +constexpr int GATE_CLOSE_STAGE = 2; + +AudioEffectSOS::AudioEffectSOS(float maxDelayMs) +: AudioStream(1, m_inputQueueArray) +{ + m_memory = new AudioDelay(maxDelayMs); + m_maxDelaySamples = calcAudioSamples(maxDelayMs); + m_externalMemory = false; +} + +AudioEffectSOS::AudioEffectSOS(size_t numSamples) +: AudioStream(1, m_inputQueueArray) +{ + m_memory = new AudioDelay(numSamples); + m_maxDelaySamples = numSamples; + m_externalMemory = false; +} + 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() { + if (m_memory) delete m_memory; +} +void AudioEffectSOS::enable(void) +{ + m_enable = true; + if (m_externalMemory) { + // Because we hold the previous output buffer for an update cycle, the maximum delay is actually + // 1 audio block mess then the max delay returnable from the memory. + m_maxDelaySamples = m_memory->getMaxDelaySamples(); + Serial.println(String("SOS Enabled with delay length ") + m_maxDelaySamples + String(" samples")); + } + m_delaySamples = m_maxDelaySamples; + m_inputGateAuto.setupParameter(GATE_OPEN_STAGE, 0.0f, 1.0f, 1000.0f, ParameterAutomation::Function::LINEAR); + m_inputGateAuto.setupParameter(GATE_HOLD_STAGE, 1.0f, 1.0f, 1000.0f, ParameterAutomation::Function::HOLD); + m_inputGateAuto.setupParameter(GATE_CLOSE_STAGE, 1.0f, 0.0f, 1000.0f, ParameterAutomation::Function::LINEAR); } void AudioEffectSOS::update(void) @@ -58,7 +91,7 @@ void AudioEffectSOS::update(void) } // Check is block is bypassed, if so either transmit input directly or create silence - if (m_bypass == true) { + if ( (m_bypass == true) || (!inputAudioBlock) ) { // transmit the input directly if (!inputAudioBlock) { // create silence @@ -73,6 +106,8 @@ void AudioEffectSOS::update(void) return; } + if (!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. @@ -83,6 +118,8 @@ void AudioEffectSOS::update(void) // get the data. If using external memory with DMA, this won't be filled until // later. m_memory->getSamples(blockToOutput, m_delaySamples); + //Serial.println(String("Delay samples:") + m_delaySamples); + //Serial.println(String("Use dma: ") + m_memory->getSlot()->isUseDma()); // If using DMA, we need something else to do while that read executes, so // move on to input preprocessing @@ -95,6 +132,8 @@ void AudioEffectSOS::update(void) // 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); + //audio_block_t *blockToRelease = m_memory->addBlock(inputAudioBlock); + //Serial.println("Done adding new block"); // BACK TO OUTPUT PROCESSING @@ -105,13 +144,19 @@ void AudioEffectSOS::update(void) } // perform the wet/dry mix mix - //m_postProcessing(blockToOutput, inputAudioBlock, blockToOutput); + m_postProcessing(blockToOutput, blockToOutput); transmit(blockToOutput); release(inputAudioBlock); - release(m_previousBlock); + + if (m_previousBlock) + release(m_previousBlock); m_previousBlock = blockToOutput; + if (m_blockToRelease == m_previousBlock) { + Serial.println("ERROR: POINTER COLLISION"); + } + if (m_blockToRelease) release(m_blockToRelease); m_blockToRelease = blockToRelease; } @@ -121,12 +166,13 @@ void AudioEffectSOS::gateOpenTime(float milliseconds) { // TODO - change the paramter automation to an automation sequence m_openTimeMs = milliseconds; - //m_inputGateAuto.reconfigure(); + m_inputGateAuto.setupParameter(GATE_OPEN_STAGE, 0.0f, 1.0f, m_openTimeMs, ParameterAutomation::Function::LINEAR); } void AudioEffectSOS::gateCloseTime(float milliseconds) { m_closeTimeMs = milliseconds; + m_inputGateAuto.setupParameter(GATE_CLOSE_STAGE, 1.0f, 0.0f, m_closeTimeMs, ParameterAutomation::Function::LINEAR); } //////////////////////////////////////////////////////////////////////// @@ -149,7 +195,7 @@ void AudioEffectSOS::processMidi(int channel, int control, int value) (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); + Serial.println(String("AudioEffectSOS::gate close time (ms): ") + m_closeTimeMs); return; } @@ -180,8 +226,8 @@ void AudioEffectSOS::processMidi(int channel, int control, int value) 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!")); + m_inputGateAuto.trigger(); return; } } @@ -203,17 +249,25 @@ void AudioEffectSOS::m_preProcessing (audio_block_t *out, audio_block_t *input, 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(); + + //float gateVol = 1.0f; 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 + gainAdjust(&tempAudioBuffer, delayedSignal, m_feedback, 0); // last parameter is coeff shift, 0 bits combine(out, out, &tempAudioBuffer); + } else if (input) { memcpy(out->data, input->data, sizeof(int16_t) * AUDIO_BLOCK_SAMPLES); } } +void AudioEffectSOS::m_postProcessing(audio_block_t *out, audio_block_t *in) +{ + gainAdjust(out, out, m_volume, 0); +} + } // namespace BAEffects