diff --git a/modules/juce_audio_basics/juce_audio_basics.cpp b/modules/juce_audio_basics/juce_audio_basics.cpp index 9fe3e779c5..7f1d3eaeff 100644 --- a/modules/juce_audio_basics/juce_audio_basics.cpp +++ b/modules/juce_audio_basics/juce_audio_basics.cpp @@ -86,3 +86,7 @@ #include "sources/juce_ReverbAudioSource.cpp" #include "sources/juce_ToneGeneratorAudioSource.cpp" #include "synthesisers/juce_Synthesiser.cpp" + +#if JUCE_UNIT_TESTS + #include "utilities/juce_ADSR_test.cpp" +#endif diff --git a/modules/juce_audio_basics/utilities/juce_ADSR.h b/modules/juce_audio_basics/utilities/juce_ADSR.h index a89866d04a..fb0d894233 100644 --- a/modules/juce_audio_basics/utilities/juce_ADSR.h +++ b/modules/juce_audio_basics/utilities/juce_ADSR.h @@ -33,14 +33,13 @@ namespace juce @tags{Audio} */ -class ADSR +class JUCE_API ADSR { public: //============================================================================== ADSR() { - setSampleRate (44100.0); - setParameters ({}); + recalculateRates(); } //============================================================================== @@ -49,19 +48,22 @@ public: @tags{Audio} */ - struct Parameters + struct JUCE_API Parameters { - /** Attack time in seconds. */ - float attack = 0.1f; + Parameters() = default; - /** Decay time in seconds. */ - float decay = 0.1f; + Parameters (float attackTimeSeconds, + float decayTimeSeconds, + float sustainLevel, + float releaseTimeSeconds) + : attack (attackTimeSeconds), + decay (decayTimeSeconds), + sustain (sustainLevel), + release (releaseTimeSeconds) + { + } - /** Sustain level. */ - float sustain = 1.0f; - - /** Release time in seconds. */ - float release = 0.1f; + float attack = 0.1f, decay = 0.1f, sustain = 1.0f, release = 0.1f; }; /** Sets the parameters that will be used by an ADSR object. @@ -73,70 +75,68 @@ public: */ void setParameters (const Parameters& newParameters) { - currentParameters = newParameters; + // need to call setSampleRate() first! + jassert (sampleRate > 0.0); - sustainLevel = newParameters.sustain; - calculateRates (newParameters); - - if (currentState != State::idle) - checkCurrentState(); + parameters = newParameters; + recalculateRates(); } /** Returns the parameters currently being used by an ADSR object. @see setParameters */ - const Parameters& getParameters() const { return currentParameters; } + const Parameters& getParameters() const noexcept { return parameters; } /** Returns true if the envelope is in its attack, decay, sustain or release stage. */ - bool isActive() const noexcept { return currentState != State::idle; } + bool isActive() const noexcept { return state != State::idle; } //============================================================================== /** Sets the sample rate that will be used for the envelope. This must be called before the getNextSample() or setParameters() methods. */ - void setSampleRate (double sampleRate) + void setSampleRate (double newSampleRate) noexcept { - jassert (sampleRate > 0.0); - sr = sampleRate; + jassert (newSampleRate > 0.0); + sampleRate = newSampleRate; } //============================================================================== /** Resets the envelope to an idle state. */ - void reset() + void reset() noexcept { envelopeVal = 0.0f; - currentState = State::idle; + state = State::idle; } /** Starts the attack phase of the envelope. */ - void noteOn() + void noteOn() noexcept { if (attackRate > 0.0f) { - currentState = State::attack; + state = State::attack; } else if (decayRate > 0.0f) { envelopeVal = 1.0f; - currentState = State::decay; + state = State::decay; } else { - currentState = State::sustain; + state = State::sustain; } } /** Starts the release phase of the envelope. */ - void noteOff() + void noteOff() noexcept { - if (currentState != State::idle) + if (state != State::idle) { - if (currentParameters.release > 0.0f) + if (parameters.release > 0.0f) { - releaseRate = static_cast (envelopeVal / (currentParameters.release * sr)); - currentState = State::release; + releaseRate = (float) (envelopeVal / (parameters.release * sampleRate)); + state = State::release; } else { @@ -150,45 +150,41 @@ public: @see applyEnvelopeToBuffer */ - float getNextSample() + float getNextSample() noexcept { - if (currentState == State::idle) + if (state == State::idle) return 0.0f; - if (currentState == State::attack) + if (state == State::attack) { envelopeVal += attackRate; if (envelopeVal >= 1.0f) { envelopeVal = 1.0f; - - if (decayRate > 0.0f) - currentState = State::decay; - else - currentState = State::sustain; + goToNextState(); } } - else if (currentState == State::decay) + else if (state == State::decay) { envelopeVal -= decayRate; - if (envelopeVal <= sustainLevel) + if (envelopeVal <= parameters.sustain) { - envelopeVal = sustainLevel; - currentState = State::sustain; + envelopeVal = parameters.sustain; + goToNextState(); } } - else if (currentState == State::sustain) + else if (state == State::sustain) { - envelopeVal = sustainLevel; + envelopeVal = parameters.sustain; } - else if (currentState == State::release) + else if (state == State::release) { envelopeVal -= releaseRate; if (envelopeVal <= 0.0f) - reset(); + goToNextState(); } return envelopeVal; @@ -204,6 +200,18 @@ public: { jassert (startSample + numSamples <= buffer.getNumSamples()); + if (state == State::idle) + { + buffer.clear (startSample, numSamples); + return; + } + + if (state == State::sustain) + { + buffer.applyGain (startSample, numSamples, parameters.sustain); + return; + } + auto numChannels = buffer.getNumChannels(); while (--numSamples >= 0) @@ -219,30 +227,43 @@ public: private: //============================================================================== - void calculateRates (const Parameters& parameters) + void recalculateRates() noexcept { - // need to call setSampleRate() first! - jassert (sr > 0.0); + auto getRate = [] (float distance, float timeInSeconds, double sr) + { + return timeInSeconds > 0.0f ? (float) (distance / (timeInSeconds * sr)) : -1.0f; + }; - attackRate = (parameters.attack > 0.0f ? static_cast (1.0f / (parameters.attack * sr)) : -1.0f); - decayRate = (parameters.decay > 0.0f ? static_cast ((1.0f - sustainLevel) / (parameters.decay * sr)) : -1.0f); + attackRate = getRate (1.0f, parameters.attack, sampleRate); + decayRate = getRate (1.0f - parameters.sustain, parameters.decay, sampleRate); + releaseRate = getRate (parameters.sustain, parameters.release, sampleRate); + + if ((state == State::attack && attackRate <= 0.0f) + || (state == State::decay && (decayRate <= 0.0f || envelopeVal <= parameters.sustain)) + || (state == State::release && releaseRate <= 0.0f)) + { + goToNextState(); + } } - void checkCurrentState() + void goToNextState() noexcept { - if (currentState == State::attack && attackRate <= 0.0f) currentState = decayRate > 0.0f ? State::decay : State::sustain; - else if (currentState == State::decay && decayRate <= 0.0f) currentState = State::sustain; - else if (currentState == State::release && releaseRate <= 0.0f) reset(); + if (state == State::attack) + state = (decayRate > 0.0f ? State::decay : State::sustain); + else if (state == State::decay) + state = State::sustain; + else if (state == State::release) + reset(); } //============================================================================== enum class State { idle, attack, decay, sustain, release }; - State currentState = State::idle; - Parameters currentParameters; + State state = State::idle; + Parameters parameters; - double sr = 0.0; - float envelopeVal = 0.0f, sustainLevel = 0.0f, attackRate = 0.0f, decayRate = 0.0f, releaseRate = 0.0f; + double sampleRate = 44100.0; + float envelopeVal = 0.0f, attackRate = 0.0f, decayRate = 0.0f, releaseRate = 0.0f; }; } // namespace juce diff --git a/modules/juce_audio_basics/utilities/juce_ADSR_test.cpp b/modules/juce_audio_basics/utilities/juce_ADSR_test.cpp new file mode 100644 index 0000000000..f0789be185 --- /dev/null +++ b/modules/juce_audio_basics/utilities/juce_ADSR_test.cpp @@ -0,0 +1,243 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +struct ADSRTests : public UnitTest +{ + ADSRTests() : UnitTest ("ADSR", UnitTestCategories::audio) {} + + void runTest() override + { + constexpr double sampleRate = 44100.0; + const ADSR::Parameters parameters { 0.1f, 0.1f, 0.5f, 0.1f }; + + ADSR adsr; + adsr.setSampleRate (sampleRate); + adsr.setParameters (parameters); + + beginTest ("Idle"); + { + adsr.reset(); + + expect (! adsr.isActive()); + expectEquals (adsr.getNextSample(), 0.0f); + } + + beginTest ("Attack"); + { + adsr.reset(); + + adsr.noteOn(); + expect (adsr.isActive()); + + auto buffer = getTestBuffer (sampleRate, parameters.attack); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + expect (isIncreasing (buffer)); + } + + beginTest ("Decay"); + { + adsr.reset(); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt (parameters.attack * sampleRate)); + + auto buffer = getTestBuffer (sampleRate, parameters.decay); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + expect (isDecreasing (buffer)); + } + + beginTest ("Sustain"); + { + adsr.reset(); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay + 0.01) * sampleRate)); + + auto random = getRandom(); + + for (int numTests = 0; numTests < 100; ++numTests) + { + const auto sustainLevel = random.nextFloat(); + const auto sustainLength = random.nextFloat(); + + adsr.setParameters ({ parameters.attack, parameters.decay, sustainLevel, parameters.release }); + + auto buffer = getTestBuffer (sampleRate, sustainLength); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + expect (isSustained (buffer, sustainLevel)); + } + } + + beginTest ("Release"); + { + adsr.reset(); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate)); + adsr.noteOff(); + + auto buffer = getTestBuffer (sampleRate, parameters.release); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + expect (isDecreasing (buffer)); + } + + beginTest ("Zero-length attack jumps to decay"); + { + adsr.reset(); + adsr.setParameters ({ 0.0f, parameters.decay, parameters.sustain, parameters.release }); + + adsr.noteOn(); + + auto buffer = getTestBuffer (sampleRate, parameters.decay); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + expect (isDecreasing (buffer)); + } + + beginTest ("Zero-length decay jumps to sustain"); + { + adsr.reset(); + adsr.setParameters ({ parameters.attack, 0.0f, parameters.sustain, parameters.release }); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt (parameters.attack * sampleRate)); + adsr.getNextSample(); + + expectEquals (adsr.getNextSample(), parameters.sustain); + + auto buffer = getTestBuffer (sampleRate, 1); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + expect (isSustained (buffer, parameters.sustain)); + } + + beginTest ("Zero-length attack and decay jumps to sustain"); + { + adsr.reset(); + adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release }); + + adsr.noteOn(); + + expectEquals (adsr.getNextSample(), parameters.sustain); + + auto buffer = getTestBuffer (sampleRate, 1); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + expect (isSustained (buffer, parameters.sustain)); + } + + beginTest ("Zero-length release resets to idle"); + { + adsr.reset(); + adsr.setParameters ({ parameters.attack, parameters.decay, parameters.sustain, 0.0f }); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate)); + adsr.noteOff(); + + expect (! adsr.isActive()); + } + } + + static void advanceADSR (ADSR& adsr, int numSamplesToAdvance) + { + while (--numSamplesToAdvance >= 0) + adsr.getNextSample(); + } + + static AudioBuffer getTestBuffer (double sampleRate, float lengthInSeconds) + { + AudioBuffer buffer { 2, roundToInt (lengthInSeconds * sampleRate) }; + + for (int channel = 0; channel < buffer.getNumChannels(); ++channel) + for (int sample = 0; sample < buffer.getNumSamples(); ++sample) + buffer.setSample (channel, sample, 1.0f); + + return buffer; + } + + static bool isIncreasing (const AudioBuffer& b) + { + jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); + + for (int channel = 0; channel < b.getNumChannels(); ++channel) + { + float previousSample = -1.0f; + + for (int sample = 0; sample < b.getNumSamples(); ++sample) + { + const auto currentSample = b.getSample (channel, sample); + + if (currentSample <= previousSample) + return false; + + previousSample = currentSample; + } + } + + return true; + } + + static bool isDecreasing (const AudioBuffer& b) + { + jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); + + for (int channel = 0; channel < b.getNumChannels(); ++channel) + { + float previousSample = std::numeric_limits::max(); + + for (int sample = 0; sample < b.getNumSamples(); ++sample) + { + const auto currentSample = b.getSample (channel, sample); + + if (currentSample >= previousSample) + return false; + + previousSample = currentSample; + } + } + + return true; + } + + static bool isSustained (const AudioBuffer& b, float sustainLevel) + { + jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); + + for (int channel = 0; channel < b.getNumChannels(); ++channel) + if (b.findMinMax (channel, 0, b.getNumSamples()) != Range { sustainLevel, sustainLevel }) + return false; + + return true; + } +}; + +static ADSRTests adsrTests; + +} // namespace juce