Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 177 additions & 2 deletions plugins/env/adsr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ int module_init (thPlugin *plugin)
return 0;
}

int module_callback (thNode *node, thSynthTree *mod, unsigned int windowlen,
unsigned int samples)
/* The envelope as it always ran, for a trigger written as a constant -- or
not written at all. Such a graph wants a one-shot: attack, decay, and at
a trigger of 0 straight into the release, which is what brass.dsp's bend
and bd10.dsp's pitch sweep are built on. Kept to the bit. */
static int freeRunning (thNode *node, thSynthTree *mod, unsigned int windowlen,
unsigned int samples)
{
/* User args */
thArg *in_a, *in_d, *in_s, *in_r, *in_p, *in_trigger, *in_reset;
Expand Down Expand Up @@ -245,3 +249,174 @@ int module_callback (thNode *node, thSynthTree *mod, unsigned int windowlen,
*/
return 0;
}

/* The envelope for a trigger that is wired -- to the voice's, or to
* anything else that moves. It follows the trigger in every phase:
*
* - IT WAITS FOR THE TRIGGER TO RISE. A voice's own trigger is up from
* its first sample, so that is at once; a gate that opens later, a
* player in a section coming in behind the first, starts then.
* - A FALL RELEASES IT FROM WHEREVER IT IS, attack and decay included,
* and from the level it had reached. The free-running envelope played
* the whole attack and decay of a note let go during them, and then
* jumped to the sustain level to release from it.
* - ONLY A RISING EDGE RETRIGGERS, from the level it is at. A trigger
* held up was read as a new note every sample in the release and
* after it, so a decay to a sustain of 0 started over for as long as
* the key was down.
*
* Its shapes are the free-running envelope's -- a linear attack, the same
* log curves down -- so a note held past its decay and released from the
* sustain comes out the same either way.
*/
static int gated (thNode *node, thSynthTree *mod, unsigned int windowlen)
{
enum { ATTACK, DECAY, SUSTAIN, RELEASE, DONE, WAITING };

thArg *in_a = mod->getArg(node, args[IN_A]);
thArg *in_d = mod->getArg(node, args[IN_D]);
thArg *in_s = mod->getArg(node, args[IN_S]);
thArg *in_r = mod->getArg(node, args[IN_R]);
thArg *in_p = mod->getArg(node, args[IN_P]);
thArg *in_trigger = mod->getArg(node, args[IN_TRIGGER]);
thArg *in_reset = mod->getArg(node, args[IN_RESET]);
thArg *inout = mod->getArg(node, args[INOUT_POSITION]);

/* [0] position in the phase, [1] the phase, [2] whether the trigger
was up, [3] the level, [4] where the attack or release started. A
new node's state reads zeros, which is attack at position 0 -- so
a fresh one starts out WAITING instead. */
const bool fresh = inout->len() < 5;
float position = (*inout)[0];
int phase = fresh ? WAITING : (int)(*inout)[1];
bool was = (*inout)[2] > 0;
float level = (*inout)[3];
float from = (*inout)[4];
float *state = inout->allocate(5);

float *out = mod->getArg(node, args[OUT_ARG])->allocate(windowlen);
float *play = mod->getArg(node, args[OUT_PLAY])->allocate(windowlen);

for (unsigned int i = 0; i < windowlen; i++)
{
const bool up = (*in_trigger)[i] > 0;
const float peak = (*in_p)[i] == 0 ? TH_MAX : (*in_p)[i];
const float sus = (*in_s)[i];

/* A voice's first rise, and `reset', start the attack on this
sample, as the free-running envelope's first sample does. That
rise is then spent: a zero-length attack has moved on to the
decay by the end of the sample, and must not be started again. */
const bool first = up && !was && phase == WAITING;

if (first || (*in_reset)[i] > 0)
{
phase = ATTACK;
position = 0;
from = level;
}

/* Zero-length segments complete at once rather than dividing by
their own length, as in the free-running envelope. */
if (phase == ATTACK && (*in_a)[i] <= 0)
{
phase = DECAY;
position = 0;
}
if (phase == DECAY && (*in_d)[i] <= 0)
{
phase = SUSTAIN;
position = 0;
}

switch (phase)
{
case ATTACK:
level = from + (peak - from) * (position++ / (*in_a)[i]);
if (position >= (*in_a)[i])
{
phase = DECAY;
position = 0;
}
break;
case DECAY:
level = sus + log(M_E - (M_E - 1) *
(position++ / (*in_d)[i])) * (peak - sus);
if (position >= (*in_d)[i])
{
phase = SUSTAIN;
position = 0;
}
break;
case SUSTAIN:
level = sus;
/* A sustain of 0 is the end of a one-shot. */
if (sus == 0)
phase = DONE;
break;
case RELEASE:
if ((*in_r)[i] <= 0 || position >= (*in_r)[i])
{
level = 0;
phase = DONE;
}
else
{
level = (1 - log(1 + (M_E - 1) *
(position++ / (*in_r)[i]))) * from;
}
break;
default:
level = 0;
break;
}

out[i] = level;
/* Not sounding while it waits. A gate may never open -- a note
shorter than the delay it was waiting out -- and an envelope
that kept its voice alive meanwhile would hold that voice for
ever. So the voice's own `play' belongs to an envelope on the
voice's trigger, which rises with the note; a delayed one adds
to it (dsp/violins.dsp's max of its players). */
play[i] = (phase == DONE || phase == WAITING) ? 0 : 1;

/* Every other edge from the next sample, which is when the
free-running envelope's release began: a note held into its
sustain comes out the same, sample for sample. */
if (up && !was && !first)
{
phase = ATTACK;
position = 0;
from = level;
}
else if (!up && was && phase <= SUSTAIN)
{
phase = RELEASE;
position = 0;
from = level;
}

was = up;
}

state[0] = position;
state[1] = phase;
state[2] = was ? 1 : 0;
state[3] = level;
state[4] = from;

return 0;
}

int module_callback (thNode *node, thSynthTree *mod, unsigned int windowlen,
unsigned int samples)
{
/* The arg as the graph wrote it, before it is followed: a number, or
nothing, is a constant; anything else moves. */
const thArg *trigger = node->getArg((int)args[IN_TRIGGER]);

if (trigger == NULL || trigger->type() == thArg::ARG_VALUE)
return freeRunning(node, mod, windowlen, samples);

return gated(node, mod, windowlen);
}
122 changes: 122 additions & 0 deletions scripts/statecheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4082,6 +4082,127 @@ static void checkBlep (const string &pluginPath)
"five hundred");
}

/* ---- env::adsr, gated ----------------------------------------------------
*
* A wired trigger: a square at 2 Hz, low for its first quarter second and
* high for the next, or a constant routed through a node so that it is a
* wire and not a number. A trigger written as a number takes the
* free-running path, which is checked by being what every shipped one-shot
* plays and is not repeated here.
*/

static vector<NodeSpec> adsrGraph (const char *trigger, float a, float d,
float s, float r, float hz = 2)
{
vector<NodeSpec> spec;
NodeSpec sq, one, env;

sq.name = "sq";
sq.spelling = "osc/simple";
sq.values.push_back(Value{ "freq", hz });
sq.values.push_back(Value{ "waveform", 2 });
spec.push_back(sq);

one.name = "one";
one.spelling = "math/add";
one.values.push_back(Value{ "in0", 1 });
one.values.push_back(Value{ "in1", 0 });
spec.push_back(one);

env.name = "env";
env.spelling = "env/adsr";
env.values.push_back(Value{ "a", a });
env.values.push_back(Value{ "d", d });
env.values.push_back(Value{ "s", s });
env.values.push_back(Value{ "r", r });
if (strcmp(trigger, "none") != 0)
env.wires.push_back(Wire{ "trigger", trigger, "out" });
spec.push_back(env);

return spec;
}

static void checkAdsrGated (const string &pluginPath)
{
const float sr = TH_DEFAULT_SAMPLES;
vector<float> got;
string why;

/* Attack half a second, so the square's fall at 0.5 s lands in it. */
if (!render1(pluginPath, adsrGraph("sq", sr * 0.5f, 0, 1, sr * 0.05f),
"env", "out", 256, (unsigned)sr, got, why))
{
fail("env::adsr renders gated", why);
}
else
{
const size_t rise = (size_t)(sr * 0.25f), fall = (size_t)(sr * 0.5f);
const float atFall = got[fall - 1];
float before = 0, after = 0;

for (size_t i = 0; i < rise - 64; i++)
before = fmaxf(before, got[i]);
for (size_t i = fall; i < got.size(); i++)
after = fmaxf(after, got[i]);

okOrFail(before == 0, "env::adsr: a wired trigger that starts low "
"holds the envelope until it rises", "peak " + num(before));
okOrFail(atFall > 0.4f && atFall < 0.6f && after <= atFall + 1e-4f,
"env::adsr: a fall during the attack releases from the "
"level reached, not after the attack",
"at the fall " + num(atFall) + ", after it " + num(after));
okOrFail(got[fall + (size_t)(sr * 0.06f)] == 0,
"env::adsr: and the release runs out in its own time",
"0.06 s on " + num(got[fall + (size_t)(sr * 0.06f)]));
}

/* A decay to 0 under a trigger held up: over, not starting again. */
if (!render1(pluginPath, adsrGraph("one", 0, sr * 0.1f, 0, sr * 0.05f),
"env", "out", 256, (unsigned)sr, got, why))
{
fail("env::adsr renders a one-shot", why);
}
else
{
float late = 0;

for (size_t i = (size_t)(sr * 0.2f); i < got.size(); i++)
late = fmaxf(late, got[i]);

okOrFail(got[0] > 0.99f && late == 0,
"env::adsr: a decay to a sustain of 0 under a held trigger "
"ends rather than starting over",
"first " + num(got[0]) + ", peak after 0.2 s " + num(late));
okOrFail(got[1] < got[0],
"env::adsr: with no attack, the first rise starts the decay "
"once, on the first sample",
"first " + num(got[0]) + ", second " + num(got[1]));
}

/* A trigger that is a number, here none at all: attack, decay, then
the release, as before. */
if (!render1(pluginPath, adsrGraph("none", sr * 0.01f, sr * 0.01f, 0.5f,
sr * 0.01f),
"env", "out", 256, 4410, got, why))
{
fail("env::adsr renders free-running", why);
}
else
{
okOrFail(peak(got, 0) > 0.99f && got.back() == 0,
"env::adsr: an unwired trigger still runs once, attack to "
"release", "peak " + num(peak(got, 0)));
}

/* At 50 Hz the square is up for 441 samples and down for 441, so
inside the 2000 compared there are rises, attacks, decays, sustains
and releases, and a release cut short by the next rise. */
windowsAgree(pluginPath, adsrGraph("sq", 100, 150, 0.6f, 300, 50),
"env", "out",
"env::adsr: the same gated envelope at one sample a window "
"and at five hundred");
}

/* ---- osc::sample -------------------------------------------------------- */

/* The node that plays a file, and the only one here whose input is not a
Expand Down Expand Up @@ -7395,6 +7516,7 @@ int main (int argc, char **argv)
checkFmop(pluginPath);
checkSimple(pluginPath);
checkBlep(pluginPath);
checkAdsrGated(pluginPath);
checkSample(pluginPath);
checkGrain(pluginPath);
checkDrift(pluginPath);
Expand Down
Loading