/*
* 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_RAMP ]
* +---------------------+
* | |
* IN00--| TRIGGER SIGNAL |--OUT00
* | TRIGGER |--OUT01
* | |
* +---------------------+
*
* @description(
* the ramp’s duration, start and end value can be specified. however, the ramping process
* only starts when the `start()` function is called. if duration, start and end values
* are changed during ramping the ramping behavior changes. if the same values are changed
* while the ramp is not ramping no behavior change occurs until the `start()` function is
* called.
* )
*
*/
//@todo(make edge behavior configurable. curerntly `RISING`==`start()` + `FALLING`==`stop()` )
#ifndef NodeRamp_hpp
#define NodeRamp_hpp
#include "KlangMath.hpp"
#include "KlangNode.hpp"
namespace klang {
class NodeRamp : public Node {
public:
static const CHANNEL_ID CH_IN_TRIGGER = 0;
static const CHANNEL_ID NUM_CH_IN = 1;
static const CHANNEL_ID CH_OUT_TRIGGER = 1;
static const CHANNEL_ID NUM_CH_OUT = 1;
bool connect(Connection* pConnection, CHANNEL_ID pInChannel) {
if (pInChannel == CH_IN_TRIGGER) {
mConnection_CH_IN_TRIGGER = pConnection;
return true;
}
return false;
}
bool disconnect(CHANNEL_ID pInChannel) {
if (pInChannel == CH_IN_TRIGGER) {
mConnection_CH_IN_TRIGGER = nullptr;
return true;
}
return false;
}
void start() {
mCurrentValue = mStart;
mState = ENVELOPE_STATE::RAMPING;
}
void resume() {
mState = ENVELOPE_STATE::RAMPING;
}
void stop() {
mState = ENVELOPE_STATE::IDLE;
}
void set_current_value(float pCurrent) {
mCurrentValue = pCurrent;
recompute_delta();
}
void set_ramp(float pDuration, float pStart, float pDestination) {
set_ramp_ms(pDuration * M_TIME_SCALE, pStart, pDestination);
}
void set_ramp_ms(float pDuration, float pStart, float pDestination) {
mDuration = pDuration;
mStart = pStart;
mDestination = pDestination;
recompute_delta();
}
void set_duration(float pDuration) {
set_duration_ms(pDuration * M_TIME_SCALE);
}
void set_duration_ms(float pDuration) {
mDuration = pDuration;
recompute_delta();
}
void set_start(float pStart) {
mStart = pStart;
recompute_delta();
}
void set_destination(float pDestination) {
mDestination = pDestination;
recompute_delta();
}
float get_duration() { return mDuration / M_TIME_SCALE; }
float get_start() { return mCurrentValue; }
float get_destination() { return mDestination; }
void set_command(KLANG_CMD_TYPE pCommand, KLANG_CMD_TYPE* pPayLoad) {
switch (pCommand) {
case KLANG_SET_START:
start();
break;
case KLANG_SET_STOP:
stop();
break;
// @todo(add set start + desitination + set_ramp)
}
}
void update(CHANNEL_ID pChannel, float* pAudioBlock) {
if (is_not_updated()) {
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] = mCurrentValue;
}
} 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_TRIGGER = nullptr;
AUDIO_BLOCK_ID mBlock_TRIGGER = AudioBlockPool::NO_ID;
static constexpr float M_TIME_SCALE = 1;
static constexpr float KLANG_AUDIO_RATE_UINT16_INV = 1.0 / KLANG_AUDIO_RATE_UINT16;
enum class ENVELOPE_STATE {
IDLE,
RAMPING
};
ENVELOPE_STATE mState = ENVELOPE_STATE::IDLE;
float mCurrentValue = 0.0;
float mDeltaFraction = 0.0;
float mStart = 0.0;
float mDestination = 0.0;
float mDuration = 0.0;
static constexpr float mThreshold = 0.0; // @todo(could be made configurable)
float mPreviousSample = mThreshold;
void kernel() {
if (mState == ENVELOPE_STATE::RAMPING) {
mCurrentValue += mDeltaFraction;
const bool mDirection = mDeltaFraction > 0;
const bool mEvalCondition = mDirection ? (mCurrentValue >= mDestination) : (mCurrentValue <= mDestination);
if (mEvalCondition) {
mCurrentValue = mDestination;
mState = ENVELOPE_STATE::IDLE;
}
}
}
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;
}
}
void recompute_delta() {
const float mDelta = mDestination - mStart;
mDeltaFraction = compute_delta_fraction(mDelta, mDuration);
}
static constexpr float RAMP_NO_EDGE = 0.0;
static constexpr float RAMP_RISING_EDGE = 1.0;
static constexpr float RAMP_FALLING_EDGE = -1.0;
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;
}
}
};
} // namespace klang
#endif /* NodeRamp_hpp */