mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
ADSR: Minor refactoring, added some tests
This commit is contained in:
parent
9e64736519
commit
675d93315f
3 changed files with 332 additions and 64 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<float> (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<float> (1.0f / (parameters.attack * sr)) : -1.0f);
|
||||
decayRate = (parameters.decay > 0.0f ? static_cast<float> ((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
|
||||
|
|
|
|||
243
modules/juce_audio_basics/utilities/juce_ADSR_test.cpp
Normal file
243
modules/juce_audio_basics/utilities/juce_ADSR_test.cpp
Normal file
|
|
@ -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<float> getTestBuffer (double sampleRate, float lengthInSeconds)
|
||||
{
|
||||
AudioBuffer<float> 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<float>& 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<float>& b)
|
||||
{
|
||||
jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0);
|
||||
|
||||
for (int channel = 0; channel < b.getNumChannels(); ++channel)
|
||||
{
|
||||
float previousSample = std::numeric_limits<float>::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<float>& 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<float> { sustainLevel, sustainLevel })
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static ADSRTests adsrTests;
|
||||
|
||||
} // namespace juce
|
||||
Loading…
Add table
Add a link
Reference in a new issue