#+TITLE: Env* OverviewThis document describes =env=, a trigger based envelopegenerator with adjustable attack, hold, and release times.This envelope is produced by feeding a gate signal intoa smoothing filter. The output returns an exponetialenvelope with very nice-sounding curves: a convex rise,and a concave fall. This sort of envelope is very ideal for percussive soundsand for mimicking analogue synthesizers. The drawbacksmainly have to do with precision. Attack and releasetimes are specified as time constants, which are a littlebit less intuitive than using seconds. The signalgenerated is in range 0 to 1, but it will never reach 1exactly.* Tangled Files=env.c= and =env.h=, respectively. #+NAME: env.h#+BEGIN_SRC c :tangle env.h#ifndef SK_ENV_H#define SK_ENV_H#ifndef SKFLT#define SKFLT float#endif <<typedefs>><<funcdefs>> #ifdef SK_ENV_PRIV<<structs>>#endif #endif#+END_SRC #+NAME: env.c#+BEGIN_SRC c :tangle env.c#include <math.h>#define SK_ENV_PRIV#include "env.h"<<macros>><<funcs>>#+END_SRC* InitializationInitialize the envelope with =sk_env_init=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_env_init(sk_env *env, int sr);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_env_init(sk_env *env, int sr){ env->sr = sr; <<init>>}#+END_SRC* Struct Components** Struct#+NAME: typedefs#+BEGIN_SRC ctypedef struct sk_env sk_env;#+END_SRC #+NAME: structs#+BEGIN_SRC cstruct sk_env { int sr; <<sk_env>>};#+END_SRC** TimerA timer is used to produce a gate signal of a particularduration. This kicks on during the hold portion ofthe signal. This timer is stored in a @!(ref "normalized_range""normalized range")!@, and is updated by a normalizedfloating-point incrementor value. #+NAME: sk_env#+BEGIN_SRC cfloat timer;float inc;#+END_SRC #+NAME: init#+BEGIN_SRC cenv->timer = 0;env->inc = 0;#+END_SRC** FilterThe filter used is a pretty typical@!(ref "smoother" "one-pole lowpass filter")!@, but withtwo parameters for the release and attack portions ofthe envelope. #+NAME: sk_env#+BEGIN_SRC cSKFLT atk_env;SKFLT rel_env;#+END_SRC #+NAME: init#+BEGIN_SRC cenv->atk_env = 0;env->rel_env = 0;#+END_SRC** State ManagementThe envelope has three main states: attack, hold, andrelease. There is also a zero state to inidicate thatenvelope is not playing anything. #+NAME: sk_env#+BEGIN_SRC cint mode;#+END_SRC #+NAME: macros#+BEGIN_SRC cenum { MODE_ZERO, MODE_ATTACK, MODE_HOLD, MODE_RELEASE};#+END_SRC #+NAME: init#+BEGIN_SRC cenv->mode = MODE_ZERO;#+END_SRC ** Previous OutputThe output from the previous sample is stored. #+NAME: sk_env#+BEGIN_SRC cSKFLT prev;#+END_SRC #+NAME: init#+BEGIN_SRC cenv->prev = 0;#+END_SRC* ParametersAttack, release, and hold time utilize @!(ref"parameter_caching" "parameter caching")!@ in order toreduce needless computations.** Attack Time (in tau units)Set the attack time with =sk_env_attack=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_env_attack(sk_env *env, SKFLT atk);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_env_attack(sk_env *env, SKFLT atk){ env->atk = atk;}#+END_SRC #+NAME: sk_env#+BEGIN_SRC cSKFLT atk;SKFLT patk;#+END_SRC Set to arbitrary default value. #+NAME: init#+BEGIN_SRC csk_env_attack(env, 0.1);env->patk = -1;#+END_SRC** Release Time (in tau units)Set the release time with =sk_env_release=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_env_release(sk_env *env, SKFLT rel);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_env_release(sk_env *env, SKFLT rel){ env->rel= rel;}#+END_SRC #+NAME: sk_env#+BEGIN_SRC cSKFLT rel;SKFLT prel;#+END_SRC Set to arbitrary default value. #+NAME: init#+BEGIN_SRC csk_env_release(env, 0.1);env->prel= -1;#+END_SRC** Hold Time (in seconds)The hold time is set with =sk_env_hold=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_env_hold(sk_env *env, SKFLT hold);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_env_hold(sk_env *env, SKFLT hold){ env->hold = hold;}#+END_SRC #+NAME: sk_env#+BEGIN_SRC cSKFLT hold;SKFLT phold;#+END_SRC #+NAME: init#+BEGIN_SRC csk_env_hold(env, 0.1);env->phold = -1;#+END_SRC* A Decent Epsilon ValueTo break out of attack mode, and to have a clean silenceafter release mode, a very small value known as an =epsilon=will be used. This value should be small enough to beperceptually perfect, and large enough to be numericallystable. The ideal epsilon value is around=5e-8=. High-quality tends to be 24-bit, so anything lessthan =1/2^24= can be ignored, which is roughly =5e-8=(rounded down), whichI *think* should be reasonable enough for 32-bitfloating-point numbers. #+NAME: macros#+BEGIN_SRC c#define EPS 5e-8#+END_SRC* ComputationComputation is done with =sk_env_tick=. It takes in oneinput value, which expects to be a trigger. #+NAME: funcdefs#+BEGIN_SRC cSKFLT sk_env_tick(sk_env *env, SKFLT trig);#+END_SRC #+NAME: funcs#+BEGIN_SRC cSKFLT sk_env_tick(sk_env *env, SKFLT trig){ SKFLT out; out = 0; <<check_for_trigger>> switch (env->mode) { <<zero_mode>> <<attack_mode>> <<hold_mode>> <<release_mode>> default: break; } return out;}#+END_SRC The envelope will wait for a trigger. A trigger at anypoint will cause a retrigger. Some work will be done towrite in behavior that will prevent clicks from happening. When the trigger occurs, the state is set to attack,and the gate signal is turned on. The attack time parametersare updated at this point, and the filter is configuredto use the attack time parameters. #+NAME: check_for_trigger#+BEGIN_SRC cif (trig != 0) { env->mode = MODE_ATTACK; if (env->patk != env->atk) { env->patk = env->atk; env->atk_env = exp(-1.0 / (env->atk * env->sr)); }}#+END_SRC In attack mode, the on-gate signal is put through thefilter. The difference between the current and previoussamples is measured.If it falls under an epsilon value, the current value isheld and it set to be hold mode. Figuring out when the attack is done is tricky.Using threshold detection has proven to be unreliable withdifferent attack times, so the delta technique used belowwas used instead. #+NAME: attack_mode#+BEGIN_SRC ccase MODE_ATTACK: { out = env->atk_env*env->prev + (1.0 - env->atk_env); if ((out - env->prev) <= EPS) { env->mode = MODE_HOLD; env->timer = 0; if (env->phold != env->hold) { if (env->hold <= 0) { env->inc = 1.0; } else { env->phold = env->hold; env->inc = 1.0 / (env->hold * env->sr); } } } env->prev = out; break;}#+END_SRC The timer is stored in a @!(ref "normalized_range""normalized range")!@, which allows the hold time to beadjustable while it is in hold mode. During the periodin hold mode, the output signal will be the last returnedvalue of the attack envelope. When timer reaches the end, the envelope goes into releasemode. #+NAME: hold_mode#+BEGIN_SRC ccase MODE_HOLD: { out = env->prev; env->timer += env->inc; if (env->timer >= 1.0) { env->mode = MODE_RELEASE; if (env->prel != env->rel) { env->prel = env->rel; env->rel_env = exp(-1 / (env->rel * env->sr)); } } break;}#+END_SRC Release mode is very similar to to attack mode, except thatit uses release time coefficients and the inputsignal is a off-gate. #+NAME: release_mode#+BEGIN_SRC ccase MODE_RELEASE: { out = env->rel_env*env->prev; env->prev = out; if (out <= EPS) { env->mode = MODE_ZERO; } break;}#+END_SRC The envelope will remain in release mode until the outputsignal goes below the epsilon threshold. At this point,the envelope will go into zero mode, where no computationhappens and zero is returned. #+NAME: zero_mode#+BEGIN_SRC ccase MODE_ZERO: break;#+END_SRC
#+TITLE: Env
* Overview
This document describes =env=, a trigger based envelope
generator with adjustable attack, hold, and release times.
This envelope is produced by feeding a gate signal into
a smoothing filter. The output returns an exponetial
envelope with very nice-sounding curves: a convex rise,
and a concave fall.
This sort of envelope is very ideal for percussive sounds
and for mimicking analogue synthesizers. The drawbacks
mainly have to do with precision. Attack and release
times are specified as time constants, which are a little
bit less intuitive than using seconds. The signal
generated is in range 0 to 1, but it will never reach 1
exactly.
* Tangled Files
=env.c= and =env.h=, respectively.
#+NAME: env.h
#+BEGIN_SRC c :tangle env.h
#ifndef SK_ENV_H
#define SK_ENV_H
#ifndef SKFLT
#define SKFLT float
#endif
<<typedefs>>
<<funcdefs>>
#ifdef SK_ENV_PRIV
<<structs>>
#endif
#endif
#+END_SRC
#+NAME: env.c
#+BEGIN_SRC c :tangle env.c
#include <math.h>
#define SK_ENV_PRIV
#include "env.h"
<<macros>>
<<funcs>>
#+END_SRC
* Initialization
Initialize the envelope with =sk_env_init=.
#+NAME: funcdefs
#+BEGIN_SRC c
void sk_env_init(sk_env *env, int sr);
#+END_SRC
#+NAME: funcs
#+BEGIN_SRC c
void sk_env_init(sk_env *env, int sr)
{
env->sr = sr;
<<init>>
}
#+END_SRC
* Struct Components
** Struct
#+NAME: typedefs
#+BEGIN_SRC c
typedef struct sk_env sk_env;
#+END_SRC
#+NAME: structs
#+BEGIN_SRC c
struct sk_env {
int sr;
<<sk_env>>
};
#+END_SRC
** Timer
A timer is used to produce a gate signal of a particular
duration. This kicks on during the hold portion of
the signal.
This timer is stored in a @!(ref "normalized_range"
"normalized range")!@, and is updated by a normalized
floating-point incrementor value.
#+NAME: sk_env
#+BEGIN_SRC c
float timer;
float inc;
#+END_SRC
#+NAME: init
#+BEGIN_SRC c
env->timer = 0;
env->inc = 0;
#+END_SRC
** Filter
The filter used is a pretty typical
@!(ref "smoother" "one-pole lowpass filter")!@, but with
two parameters for the release and attack portions of
the envelope.
#+NAME: sk_env
#+BEGIN_SRC c
SKFLT atk_env;
SKFLT rel_env;
#+END_SRC
#+NAME: init
#+BEGIN_SRC c
env->atk_env = 0;
env->rel_env = 0;
#+END_SRC
** State Management
The envelope has three main states: attack, hold, and
release. There is also a zero state to inidicate that
envelope is not playing anything.
#+NAME: sk_env
#+BEGIN_SRC c
int mode;
#+END_SRC
#+NAME: macros
#+BEGIN_SRC c
enum {
MODE_ZERO,
MODE_ATTACK,
MODE_HOLD,
MODE_RELEASE
};
#+END_SRC
#+NAME: init
#+BEGIN_SRC c
env->mode = MODE_ZERO;
#+END_SRC
** Previous Output
The output from the previous sample is stored.
#+NAME: sk_env
#+BEGIN_SRC c
SKFLT prev;
#+END_SRC
#+NAME: init
#+BEGIN_SRC c
env->prev = 0;
#+END_SRC
* Parameters
Attack, release, and hold time utilize @!(ref
"parameter_caching" "parameter caching")!@ in order to
reduce needless computations.
** Attack Time (in tau units)
Set the attack time with =sk_env_attack=.
#+NAME: funcdefs
#+BEGIN_SRC c
void sk_env_attack(sk_env *env, SKFLT atk);
#+END_SRC
#+NAME: funcs
#+BEGIN_SRC c
void sk_env_attack(sk_env *env, SKFLT atk)
{
env->atk = atk;
}
#+END_SRC
#+NAME: sk_env
#+BEGIN_SRC c
SKFLT atk;
SKFLT patk;
#+END_SRC
Set to arbitrary default value.
#+NAME: init
#+BEGIN_SRC c
sk_env_attack(env, 0.1);
env->patk = -1;
#+END_SRC
** Release Time (in tau units)
Set the release time with =sk_env_release=.
#+NAME: funcdefs
#+BEGIN_SRC c
void sk_env_release(sk_env *env, SKFLT rel);
#+END_SRC
#+NAME: funcs
#+BEGIN_SRC c
void sk_env_release(sk_env *env, SKFLT rel)
{
env->rel= rel;
}
#+END_SRC
#+NAME: sk_env
#+BEGIN_SRC c
SKFLT rel;
SKFLT prel;
#+END_SRC
Set to arbitrary default value.
#+NAME: init
#+BEGIN_SRC c
sk_env_release(env, 0.1);
env->prel= -1;
#+END_SRC
** Hold Time (in seconds)
The hold time is set with =sk_env_hold=.
#+NAME: funcdefs
#+BEGIN_SRC c
void sk_env_hold(sk_env *env, SKFLT hold);
#+END_SRC
#+NAME: funcs
#+BEGIN_SRC c
void sk_env_hold(sk_env *env, SKFLT hold)
{
env->hold = hold;
}
#+END_SRC
#+NAME: sk_env
#+BEGIN_SRC c
SKFLT hold;
SKFLT phold;
#+END_SRC
#+NAME: init
#+BEGIN_SRC c
sk_env_hold(env, 0.1);
env->phold = -1;
#+END_SRC
* A Decent Epsilon Value
To break out of attack mode, and to have a clean silence
after release mode, a very small value known as an =epsilon=
will be used. This value should be small enough to be
perceptually perfect, and large enough to be numerically
stable.
The ideal epsilon value is around
=5e-8=. High-quality tends to be 24-bit, so anything less
than =1/2^24= can be ignored, which is roughly =5e-8=
(rounded down), which
I *think* should be reasonable enough for 32-bit
floating-point numbers.
#+NAME: macros
#+BEGIN_SRC c
#define EPS 5e-8
#+END_SRC
* Computation
Computation is done with =sk_env_tick=. It takes in one
input value, which expects to be a trigger.
#+NAME: funcdefs
#+BEGIN_SRC c
SKFLT sk_env_tick(sk_env *env, SKFLT trig);
#+END_SRC
#+NAME: funcs
#+BEGIN_SRC c
SKFLT sk_env_tick(sk_env *env, SKFLT trig)
{
SKFLT out;
out = 0;
<<check_for_trigger>>
switch (env->mode) {
<<zero_mode>>
<<attack_mode>>
<<hold_mode>>
<<release_mode>>
default:
break;
}
return out;
}
#+END_SRC
The envelope will wait for a trigger. A trigger at any
point will cause a retrigger. Some work will be done to
write in behavior that will prevent clicks from happening.
When the trigger occurs, the state is set to attack,
and the gate signal is turned on. The attack time parameters
are updated at this point, and the filter is configured
to use the attack time parameters.
#+NAME: check_for_trigger
#+BEGIN_SRC c
if (trig != 0) {
env->mode = MODE_ATTACK;
if (env->patk != env->atk) {
env->patk = env->atk;
env->atk_env = exp(-1.0 / (env->atk * env->sr));
}
}
#+END_SRC
In attack mode, the on-gate signal is put through the
filter. The difference between the current and previous
samples is measured.
If it falls under an epsilon value, the current value is
held and it set to be hold mode.
Figuring out when the attack is done is tricky.
Using threshold detection has proven to be unreliable with
different attack times, so the delta technique used below
was used instead.
#+NAME: attack_mode
#+BEGIN_SRC c
case MODE_ATTACK: {
out = env->atk_env*env->prev + (1.0 - env->atk_env);
if ((out - env->prev) <= EPS) {
env->mode = MODE_HOLD;
env->timer = 0;
if (env->phold != env->hold) {
if (env->hold <= 0) {
env->inc = 1.0;
} else {
env->phold = env->hold;
env->inc = 1.0 / (env->hold * env->sr);
}
}
}
env->prev = out;
break;
}
#+END_SRC
The timer is stored in a @!(ref "normalized_range"
"normalized range")!@, which allows the hold time to be
adjustable while it is in hold mode. During the period
in hold mode, the output signal will be the last returned
value of the attack envelope.
When timer reaches the end, the envelope goes into release
mode.
#+NAME: hold_mode
#+BEGIN_SRC c
case MODE_HOLD: {
out = env->prev;
env->timer += env->inc;
if (env->timer >= 1.0) {
env->mode = MODE_RELEASE;
if (env->prel != env->rel) {
env->prel = env->rel;
env->rel_env = exp(-1 / (env->rel * env->sr));
}
}
break;
}
#+END_SRC
Release mode is very similar to to attack mode, except that
it uses release time coefficients and the input
signal is a off-gate.
#+NAME: release_mode
#+BEGIN_SRC c
case MODE_RELEASE: {
out = env->rel_env*env->prev;
env->prev = out;
if (out <= EPS) {
env->mode = MODE_ZERO;
}
break;
}
#+END_SRC
The envelope will remain in release mode until the output
signal goes below the epsilon threshold. At this point,
the envelope will go into zero mode, where no computation
happens and zero is returned.
#+NAME: zero_mode
#+BEGIN_SRC c
case MODE_ZERO:
break;
#+END_SRC