memlnaut-nisps/nisps/dsp/env.hpp
w1n5t0n 973455b158 feat(nisps/dsp): lean DSP primitives ported from maximilian (meml-1v6)
Header-only, heap-free, sample-rate-aware DSP primitives for stream 3:
- Biquad (LPF/HPF/BPF/Notch/Peak/LowShelf/HighShelf, denormal flush)
- Delay<N> + DynamicDelay<N> (fixed-length feedback + power-of-two
  ring with fractional read & smoothed offset)
- AllPass / Comb / LpComb (Schroeder-Moorer reverb sections, split per
  role rather than maximilian's one-class-many-roles maxiReverbFilters)
- DCBlocker (one-pole HPF)
- ChamberlinSVF + OnePoleSmoother<NCh> + EnvelopeFollower
- ADSR envelope generator
- SineOsc/SawOsc/SquareOsc, PAFOperator (port of maxiPAFOperator with
  static gauss/cauchy tables), FMOp single-operator FM building block
- PitchShifter<N> granular two-head crossfade (replaces daisysp PitchShifter)

Tests: biquad freq-domain attenuation, delay tap timing, reverb
boundedness, pitch-shifter ratio + finite output. All pass under
-Wall -Wextra -Werror -Wpedantic, C++20.
2026-04-29 16:08:49 +03:00

118 lines
3.6 KiB
C++

// nisps/dsp/env.hpp — ADSR + AR envelope generators.
//
// Direct port of memllib's ADSRLite (which we used in the firmware engines).
// All time arguments in milliseconds; sample-rate is bound at `setup()`.
//
// ADSR semantics
// trigger(velocity) — start ATTACK with given velocity scale
// release() — switch to RELEASE from current value
// reset() — back to WAITTOTRIG (silent)
//
// `play()` advances one sample and returns env * velocity.
#pragma once
#include <cmath>
#include "../core/perf.hpp"
namespace nisps {
class ADSR {
public:
enum class Stage { WaitToTrig, Attack, Decay, Sustain, Release };
ADSR() noexcept = default;
void setup(float attack_ms, float decay_ms,
float sustain_level, float release_ms,
float sample_rate) noexcept {
sample_rate_ = sample_rate;
set_attack(attack_ms);
set_decay(decay_ms);
sustain_level_ = sustain_level;
// decay travels (1 - sustain) per attack-time worth of samples
decay_inc_ *= (1.f - sustain_level_);
release_ms_ = release_ms;
}
void set_attack(float attack_ms) noexcept {
if (attack_ms > 0.f) {
attack_inc_full_ = 1.f / ((attack_ms * 0.001f) * sample_rate_);
} else {
attack_inc_full_ = 0.f;
}
attack_inc_ = attack_inc_full_;
}
void set_decay(float decay_ms) noexcept {
if (decay_ms > 0.f) {
decay_inc_ = 1.f / ((decay_ms * 0.001f) * sample_rate_);
} else {
decay_inc_ = 0.f;
}
}
void set_release(float release_ms) noexcept { release_ms_ = release_ms; }
void set_sustain(float level) noexcept { sustain_level_ = level; }
NISPS_HOT NISPS_FORCE_INLINE float play() noexcept {
switch (stage_) {
case Stage::WaitToTrig: env_ = 0.f; break;
case Stage::Attack:
env_ += attack_inc_;
if (env_ >= 1.f) { env_ = 1.f; stage_ = Stage::Decay; }
break;
case Stage::Decay:
env_ -= decay_inc_;
if (env_ <= sustain_level_) { env_ = sustain_level_; stage_ = Stage::Sustain; }
break;
case Stage::Sustain: break;
case Stage::Release:
env_ -= rel_inc_;
if (env_ <= 0.f) { env_ = 0.f; stage_ = Stage::WaitToTrig; }
break;
}
return env_ * velocity_;
}
void reset() noexcept {
stage_ = Stage::WaitToTrig;
env_ = 0.f;
}
void trigger(float velocity) noexcept {
stage_ = Stage::Attack;
attack_inc_ = attack_inc_full_ * (1.f - env_);
velocity_ = velocity;
}
void trigger_if_ready(float velocity) noexcept {
if (stage_ == Stage::WaitToTrig) trigger(velocity);
}
void release() noexcept {
if (release_ms_ > 0.f) {
rel_inc_ = 1.f / ((release_ms_ * 0.001f) * sample_rate_);
rel_inc_ *= env_;
stage_ = Stage::Release;
} else {
rel_inc_ = 0.f;
stage_ = Stage::WaitToTrig;
}
}
Stage stage() const noexcept { return stage_; }
private:
float sample_rate_ = 48000.f;
float attack_inc_ = 0.f;
float attack_inc_full_ = 0.f;
float decay_inc_ = 0.f;
float sustain_level_ = 0.f;
float rel_inc_ = 0.f;
float release_ms_ = 0.f;
float env_ = 0.f;
float velocity_ = 1.f;
Stage stage_ = Stage::WaitToTrig;
};
} // namespace nisps