Skip to content

Commit 300e736

Browse files
committed
Add exponential envelope decay
This uses computed exponential multipliers for decay and release, so envelopes fall smoothly toward sustain and silence while preserving existing timing feel. It tracks decay/release block rates for introspection, clamp tiny release values to zero for coefficient calculations.
1 parent c94b118 commit 300e736

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
CC ?= gcc
22
CFLAGS = -Wall -Wextra -Wconversion -I include -I .
3+
LDLIBS = # -lm
34

45
SRCS = src/picosynth.c
56
HDRS = include/picosynth.h
@@ -52,11 +53,11 @@ $(MELODY_HDR): $(MELODY_SRC) $(MIDI2C)
5253
$(MIDI2C) $(MELODY_SRC) > $@
5354

5455
$(TARGET): $(EXAMPLE_SRC) $(SRCS) $(HDRS) $(MELODY_HDR)
55-
$(CC) $(CFLAGS) $(EXAMPLE_SRC) $(SRCS) -o $@
56+
$(CC) $(CFLAGS) $(EXAMPLE_SRC) $(SRCS) -o $@ $(LDLIBS)
5657

5758
# Build unit test runner
5859
$(TEST_TARGET): $(TEST_SRCS) $(SRCS) $(HDRS) $(TEST_DIR)/test.h
59-
$(CC) $(CFLAGS) -I $(TEST_DIR) $(TEST_SRCS) $(SRCS) -o $@
60+
$(CC) $(CFLAGS) -I $(TEST_DIR) $(TEST_SRCS) $(SRCS) -o $@ $(LDLIBS)
6061

6162
# Run the example program
6263
run: $(TARGET)

include/picosynth.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,12 @@ typedef struct {
7979
* Use synth_init_env_ms().
8080
*/
8181
typedef struct {
82-
int32_t attack; /* Ramp-up rate */
83-
int32_t decay; /* Ramp-down rate to sustain */
84-
q15_t sustain; /* Hold level (negative inverts output) */
85-
int32_t release; /* Ramp-down rate after note-off */
82+
int32_t attack; /* Ramp-up rate */
83+
int32_t decay; /* Ramp-down rate to sustain */
84+
q15_t sustain; /* Hold level (negative inverts output) */
85+
int32_t release; /* Ramp-down rate after note-off */
86+
q15_t decay_coeff; /* Exponential multiplier for decay */
87+
q15_t release_coeff; /* Exponential multiplier for release */
8688
/* Block processing state (computed at block boundaries) */
8789
int32_t block_rate; /* Current per-sample rate */
8890
uint8_t block_counter; /* Samples until next rate computation */

src/picosynth.c

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* Lightweight software synthesizer */
22

3+
#include <math.h>
34
#include <stdlib.h>
45
#include <string.h>
56

@@ -240,6 +241,7 @@ void picosynth_note_off(picosynth_t *s, uint8_t voice)
240241
#define PICOSYNTH_ENV_RATE_FROM_MS(ms) \
241242
(PICOSYNTH_MS(ms) > 0 ? (((int64_t) Q15_MAX << 4) / PICOSYNTH_MS(ms)) \
242243
: ((int32_t) Q15_MAX << 4))
244+
#define PICOSYNTH_ENV_MIN_RATIO 1e-4
243245

244246
/* Phase increments for octave 8; shift right for lower octaves */
245247
#define BASE_OCTAVE 8
@@ -259,6 +261,50 @@ static const q15_t octave8_freq[NOTES_PER_OCTAVE] = {
259261
PICOSYNTH_HZ_TO_FREQ(7902.13), /* B8 */
260262
};
261263

264+
/* Calculate exponential envelope multiplier for a given duration in samples
265+
* toward a target ratio relative to the starting value. */
266+
static q15_t env_calc_exp_coeff(uint32_t samples, double target_ratio)
267+
{
268+
if (samples < 10)
269+
return Q15_MAX >> 1; /* Very fast decay */
270+
if (target_ratio < PICOSYNTH_ENV_MIN_RATIO)
271+
target_ratio = PICOSYNTH_ENV_MIN_RATIO;
272+
if (target_ratio > 0.9999)
273+
target_ratio = 0.9999;
274+
275+
double coeff = exp(log(target_ratio) / (double) samples);
276+
int32_t q = (int32_t) (coeff * (double) Q15_MAX + 0.5);
277+
if (q < 0)
278+
q = 0;
279+
if (q > Q15_MAX)
280+
q = Q15_MAX;
281+
return (q15_t) q;
282+
}
283+
284+
/* Recalculate decay/release exponential coefficients to roughly match the
285+
* linear timing implied by the configured rates. */
286+
static void env_update_exp_coeffs(picosynth_env_t *env)
287+
{
288+
int32_t sus_abs = env->sustain < 0 ? -env->sustain : env->sustain;
289+
uint32_t sus_level = (uint32_t) sus_abs << 4;
290+
uint32_t peak = (uint32_t) Q15_MAX << 4;
291+
uint32_t decay_span = peak > sus_level ? peak - sus_level : 1;
292+
293+
uint32_t decay_samples =
294+
env->decay > 0
295+
? (decay_span + (uint32_t) env->decay - 1) / (uint32_t) env->decay
296+
: 1;
297+
double target = (double) sus_level / (double) peak;
298+
env->decay_coeff = env_calc_exp_coeff(decay_samples, target);
299+
300+
uint32_t release_samples =
301+
env->release > 0
302+
? (peak + (uint32_t) env->release - 1) / (uint32_t) env->release
303+
: 1;
304+
env->release_coeff =
305+
env_calc_exp_coeff(release_samples, PICOSYNTH_ENV_MIN_RATIO);
306+
}
307+
262308
q15_t picosynth_midi_to_freq(uint8_t note)
263309
{
264310
if (note > 119)
@@ -301,6 +347,7 @@ void picosynth_init_env(picosynth_node_t *n,
301347
n->env.decay = decay;
302348
n->env.sustain = sustain;
303349
n->env.release = release;
350+
env_update_exp_coeffs(&n->env);
304351
}
305352

306353
void picosynth_init_env_ms(picosynth_node_t *n,
@@ -471,10 +518,10 @@ q15_t picosynth_process(picosynth_t *s)
471518
if (n->env.block_counter == 0) {
472519
n->env.block_counter = PICOSYNTH_BLOCK_SIZE;
473520
if (!v->gate) {
474-
n->env.block_rate = -n->env.release;
521+
n->env.block_rate = -n->env.release; /* Informational */
475522
} else if (((uint32_t) n->state) &
476523
ENVELOPE_STATE_MODE_BIT) {
477-
n->env.block_rate = -n->env.decay;
524+
n->env.block_rate = -n->env.decay; /* Informational */
478525
} else {
479526
n->env.block_rate = n->env.attack;
480527
}
@@ -483,20 +530,24 @@ q15_t picosynth_process(picosynth_t *s)
483530

484531
/* Apply rate */
485532
int32_t val = n->state & ENVELOPE_STATE_VALUE_MASK;
486-
val += n->env.block_rate;
487-
488533
if (v->gate) {
489534
uint32_t mode =
490535
((uint32_t) n->state) & ENVELOPE_STATE_MODE_BIT;
491536
if (mode) {
492-
/* Decay: clamp to sustain level */
493537
q15_t sus_abs = n->env.sustain < 0 ? -n->env.sustain
494538
: n->env.sustain;
495539
int32_t sus_level = sus_abs << 4;
540+
int32_t delta = val - sus_level;
541+
/* Exponential decay of delta toward sustain */
542+
val =
543+
sus_level +
544+
(int32_t) (((int64_t) delta * n->env.decay_coeff) >>
545+
15);
496546
if (val < sus_level)
497547
val = sus_level;
498548
} else {
499549
/* Attack: check for transition to decay */
550+
val += n->env.block_rate;
500551
if (val >= (int32_t) Q15_MAX << 4) {
501552
val = (int32_t) Q15_MAX << 4;
502553
mode = ENVELOPE_STATE_MODE_BIT;
@@ -506,8 +557,10 @@ q15_t picosynth_process(picosynth_t *s)
506557
}
507558
n->state = (int32_t) (((uint32_t) val) | mode);
508559
} else {
509-
/* Release: clamp to zero */
510-
if (val < 0)
560+
/* Exponential release */
561+
val = (int32_t) (((int64_t) val * n->env.release_coeff) >>
562+
15);
563+
if (val < 16)
511564
val = 0;
512565
n->state = val;
513566
}

0 commit comments

Comments
 (0)