/*
* Klang – a node+text-based synthesizer library
*
* This file is part of the *wellen* library (https://github.com/dennisppaul/wellen).
* Copyright (c) 2022 Dennis P Paul.
*
* This library 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, version 3.
*
* This library 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 <http://www.gnu.org/licenses/>.
*/
/**
* [ NODE_ADSR ]
* +---------------------+
* | |
* IN00--| SIGNAL SIGNAL |--OUT00
* IN01--| TRIGGER TRIGGER |--OUT01
* | |
* +---------------------+
*
* @description(
* the ADSR node provides a envelope with four different stages:
* (A)ttack (D)ecay (S)ustain (R)elease. it is usually used to
* control the amplitude of an *oscillator*.
*
* |----->|-->| |-->|
* |---A--|-D-|-S-|-R-|
* | /
* | /
* | / _____
* | / ^
* | / |
* | / |
* |/___________|_____
* |
* |Press |Release
* )
*
* @set_value(ATTACK_F32,DECAY_F32,SUSTAIN_F32,RELEASE_F32,START,STOP)
*
*/
#ifndef NodeADSR_hpp
#define NodeADSR_hpp
#include "KlangMath.hpp"
#include "KlangNode.hpp"
namespace klang {
class NodeADSR : public Node {
public:
static const CHANNEL_ID CH_IN_TRIGGER = 1;
static const CHANNEL_ID NUM_CH_IN = 2;
static const CHANNEL_ID CH_OUT_TRIGGER = 1;
static const CHANNEL_ID NUM_CH_OUT = 2;
bool connect(Connection* pConnection, CHANNEL_ID pInChannel) {
if (pInChannel == CH_IN_SIGNAL) {
mConnection_CH_IN_SIGNAL = pConnection;
return true;
} else if (pInChannel == CH_IN_TRIGGER) {
mConnection_CH_IN_TRIGGER = pConnection;
return true;
}
return false;
}
bool disconnect(CHANNEL_ID pInChannel) {
if (pInChannel == CH_IN_SIGNAL) {
mConnection_CH_IN_SIGNAL = nullptr;
return true;
} else if (pInChannel == CH_IN_TRIGGER) {
mConnection_CH_IN_TRIGGER = nullptr;
return true;
}
return false;
}
void start() {
check_scheduled_attack_state();
}
void stop() {
check_scheduled_release_state();
}
void set_attack(float pAttack) { mAttack = pAttack * M_TIME_SCALE; }
void set_decay(float pDecay) { mDecay = pDecay * M_TIME_SCALE; }
void set_sustain(float pSustain) { mSustain = pSustain; }
void set_release(float pRelease) { mRelease = pRelease * M_TIME_SCALE; }
void set_attack_ms(float pAttack) { mAttack = pAttack; }
void set_decay_ms(float pDecay) { mDecay = pDecay; }
void set_release_ms(float pRelease) { mRelease = pRelease; }
float get_attack() { return mAttack / M_TIME_SCALE; }
float get_decay() { return mDecay / M_TIME_SCALE; }
float get_sustain() { return mSustain; }
float get_release() { return mRelease / M_TIME_SCALE; }
void set_command(KLANG_CMD_TYPE pCommand, KLANG_CMD_TYPE* pPayLoad) {
switch (pCommand) {
case KLANG_SET_ATTACK_F32:
set_attack(KlangMath::FLOAT_32(pPayLoad));
break;
case KLANG_SET_DECAY_F32:
set_decay(KlangMath::FLOAT_32(pPayLoad));
break;
case KLANG_SET_SUSTAIN_F32:
set_sustain(KlangMath::FLOAT_32(pPayLoad));
break;
case KLANG_SET_RELEASE_F32:
set_release(KlangMath::FLOAT_32(pPayLoad));
break;
case KLANG_SET_START:
start();
break;
case KLANG_SET_STOP:
stop();
break;
}
}
void update(CHANNEL_ID pChannel, float* pAudioBlock) {
/*
*
* |----->|-->| |-->|
* |---A--|-D-|-S-|-R-|
* | /
* | /
* | / _____
* | / ^
* | / |
* | / |
* |/___________|_____
* |
* |Press |Release
*
*/
if (is_not_updated()) {
if (mConnection_CH_IN_SIGNAL != nullptr) {
mConnection_CH_IN_SIGNAL->update(pAudioBlock);
mNoInputSignal = false;
} else {
mNoInputSignal = true;
}
mBlock_TRIGGER = AudioBlockPool::NO_ID;
if (mConnection_CH_IN_TRIGGER != nullptr) {
mBlock_TRIGGER = AudioBlockPool::instance().request();
mConnection_CH_IN_TRIGGER->update(mBlock_TRIGGER);
float* mBlockData_TRIGGER = AudioBlockPool::instance().data(mBlock_TRIGGER);
for (uint16_t i = 0; i < KLANG_SAMPLES_PER_AUDIO_BLOCK; i++) {
const float mCurrentSample = mBlockData_TRIGGER[i];
mBlockData_TRIGGER[i] = evaluateEdge(mPreviousSample, mCurrentSample);
mPreviousSample = mCurrentSample;
}
}
flag_updated();
}
if (pChannel == CH_OUT_SIGNAL) {
const bool mHasTriggerSignal = (mBlock_TRIGGER != AudioBlockPool::NO_ID);
float* mBlockData_TRIGGER = nullptr;
if (mHasTriggerSignal) {
mBlockData_TRIGGER = AudioBlockPool::instance().data(mBlock_TRIGGER);
}
for (uint16_t i = 0; i < KLANG_SAMPLES_PER_AUDIO_BLOCK; i++) {
if (mHasTriggerSignal) {
const float mTriggerState = mBlockData_TRIGGER[i];
if (mTriggerState == RAMP_RISING_EDGE) {
start();
} else if (mTriggerState == RAMP_FALLING_EDGE) {
stop();
}
}
kernel();
pAudioBlock[i] = mNoInputSignal ? mAmp : pAudioBlock[i] * mAmp;
}
} else if (pChannel == CH_OUT_TRIGGER) {
const bool mHasTriggerSignal = (mBlock_TRIGGER != AudioBlockPool::NO_ID);
if (mHasTriggerSignal) {
float* mBlockData_TRIGGER = AudioBlockPool::instance().data(mBlock_TRIGGER);
KLANG_COPY_AUDIO_BUFFER(pAudioBlock, mBlockData_TRIGGER);
} else {
KLANG_FILL_AUDIO_BUFFER(pAudioBlock, 0.0);
}
}
}
private:
Connection* mConnection_CH_IN_SIGNAL = nullptr;
Connection* mConnection_CH_IN_TRIGGER = nullptr;
AUDIO_BLOCK_ID mBlock_TRIGGER = AudioBlockPool::NO_ID;
static constexpr float RAMP_NO_EDGE = 0.0;
static constexpr float RAMP_RISING_EDGE = 1.0;
static constexpr float RAMP_FALLING_EDGE = -1.0;
static constexpr float mThreshold = 0.0; // @todo(could be made configurable)
static constexpr float M_TIME_SCALE = 1000;
static constexpr float KLANG_AUDIO_RATE_UINT16_INV = 1.0 / KLANG_AUDIO_RATE_UINT16;
float mAttack = 0.01f;
float mDecay = 0.05f;
float mSustain = 0.5f;
float mRelease = 0.25f;
float mPreviousSample = mThreshold;
enum class ENVELOPE_STATE {
IDLE,
ATTACK,
DECAY,
SUSTAIN,
RELEASE,
PRE_ATTACK_FADE_TO_ZERO
};
ENVELOPE_STATE mState = ENVELOPE_STATE::IDLE;
float mAmp = 0.0f;
float mDelta = 0.0f;
bool mNoInputSignal = true;
const float evaluateEdge(const float pPreviousSample, const float pCurrentSample) {
if ((pPreviousSample < mThreshold) && (pCurrentSample > mThreshold)) {
return RAMP_RISING_EDGE;
} else if ((pPreviousSample > mThreshold) && (pCurrentSample < mThreshold)) {
return RAMP_FALLING_EDGE;
} else {
return RAMP_NO_EDGE;
}
}
float compute_delta_fraction(const float pDelta, const float pDuration) {
if (pDuration > 0) {
const float c = pDelta * M_TIME_SCALE;
const float a = c * KLANG_AUDIO_RATE_UINT16_INV;
return a / pDuration;
} else {
return pDelta;
}
}
inline void setState(ENVELOPE_STATE pState) {
mState = pState;
}
void check_scheduled_attack_state() {
mDelta = compute_delta_fraction(1.0f, mAttack);
setState(ENVELOPE_STATE::ATTACK);
}
void check_scheduled_release_state() {
if (mState != ENVELOPE_STATE::RELEASE) {
mDelta = compute_delta_fraction(-mAmp, mRelease);
setState(ENVELOPE_STATE::RELEASE);
}
}
inline void kernel() {
switch (mState) {
case ENVELOPE_STATE::IDLE:
case ENVELOPE_STATE::SUSTAIN:
break;
case ENVELOPE_STATE::ATTACK:
// increase amp to sustain_level in ATTACK sec
mAmp += mDelta;
if (mAmp >= 1.0f) {
mAmp = 1.0f;
mDelta = compute_delta_fraction(-(1.0f - mSustain), mDecay);
setState(ENVELOPE_STATE::DECAY);
}
break;
case ENVELOPE_STATE::DECAY:
// decrease amp to sustain_level in DECAY sec
mAmp += mDelta;
if (mAmp <= mSustain) {
mAmp = mSustain;
setState(ENVELOPE_STATE::SUSTAIN);
}
break;
case ENVELOPE_STATE::RELEASE:
// decrease amp to 0.0 in RELEASE sec
mAmp += mDelta;
if (mAmp <= 0.0f) {
mAmp = 0.0f;
setState(ENVELOPE_STATE::IDLE);
}
break;
case ENVELOPE_STATE::PRE_ATTACK_FADE_TO_ZERO:
mAmp += mDelta;
if (mAmp <= 0.0f) {
mAmp = 0.0f;
mDelta = compute_delta_fraction(1.0f, mAttack);
setState(ENVELOPE_STATE::ATTACK);
}
break;
}
}
};
} // namespace klang
#endif /* NodeADSR_hpp */