#+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 <> <> #ifdef SK_ENV_PRIV <> #endif #endif #+END_SRC #+NAME: env.c #+BEGIN_SRC c :tangle env.c #include #define SK_ENV_PRIV #include "env.h" <> <> #+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; <> } #+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; <> }; #+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; <> switch (env->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