Skip to content

Commit 0351d13

Browse files
committed
Add Hold phase to envelope and API refactoring
This converts 4-stage ADSR to 5-stage AHDSR envelope with Hold phase between Attack and Decay. The Hold phase maintains peak level for a configurable duration before decay begins. - Replace positional parameters with picosynth_env_params_t struct - Replace picosynth_env_ms_params_t for millisecond-based init - Add hold/hold_ms fields to both parameter structs
1 parent 92ddec2 commit 0351d13

File tree

7 files changed

+281
-90
lines changed

7 files changed

+281
-90
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ picosynth_node_t *env = picosynth_voice_get_node(v, 0);
7676
picosynth_node_t *osc = picosynth_voice_get_node(v, 1);
7777

7878
/* Initialize nodes */
79-
picosynth_init_env(env, NULL, 5000, 500, Q15_MAX/2, 500);
79+
picosynth_init_env(env, NULL,
80+
&(picosynth_env_params_t){.attack=5000, .decay=500, .sustain=Q15_MAX/2,
81+
.release=500,
82+
});
8083
picosynth_init_osc(osc, &env->out, picosynth_voice_freq_ptr(v),
8184
picosynth_wave_sine);
8285
picosynth_voice_set_out(v, 1);

include/picosynth.h

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
* picosynth_node_t *osc = picosynth_voice_get_node(v, 1);
1212
* picosynth_node_t *flt = picosynth_voice_get_node(v, 2);
1313
*
14-
* picosynth_init_env_ms(env, NULL, 10, 100, 80, 50);
14+
* picosynth_init_env_ms(env, NULL,
15+
* &(picosynth_env_ms_params_t){.atk_ms=10, .dec_ms=100,
16+
* .sus_pct=80, .rel_ms=50,
17+
* });
1518
* picosynth_init_osc(osc, &env->out, picosynth_voice_freq_ptr(v),
1619
* picosynth_wave_sine);
1720
* picosynth_init_lp(flt, NULL, &osc->out, 5000);
@@ -81,18 +84,30 @@ static inline q15_t q15_sat(int32_t x)
8184
/* Waveform generator function pointer */
8285
typedef q15_t (*picosynth_wave_func_t)(q15_t phase);
8386

87+
/* AHDSR envelope parameters for initialization.
88+
* Using a struct avoids long parameter lists and enables named initialization.
89+
*/
90+
typedef struct {
91+
int32_t attack; /* Attack rate (higher = faster attack) */
92+
int32_t hold; /* Hold duration in samples (0 = no hold) */
93+
int32_t decay; /* Decay rate (higher = faster decay) */
94+
q15_t sustain; /* Sustain level (negative inverts output) */
95+
int32_t release; /* Release rate (higher = faster release) */
96+
} picosynth_env_params_t;
97+
8498
/* Oscillator state */
8599
typedef struct {
86100
const q15_t *freq; /* Phase increment (frequency control) */
87101
const q15_t *detune; /* Optional FM/detune offset */
88102
picosynth_wave_func_t wave; /* Waveform generator (phase -> sample) */
89103
} picosynth_osc_t;
90104

91-
/* ADSR envelope state. Rates are step values scaled <<4 internally.
92-
* Use synth_init_env_ms().
105+
/* AHDSR envelope state (Attack-Hold-Decay-Sustain-Release).
106+
* Rates are step values scaled <<4 internally. Use synth_init_env_ms().
93107
*/
94108
typedef struct {
95109
int32_t attack; /* Ramp-up rate */
110+
int32_t hold; /* Hold duration in samples (at peak before decay) */
96111
int32_t decay; /* Ramp-down rate to sustain */
97112
q15_t sustain; /* Hold level (negative inverts output) */
98113
int32_t release; /* Ramp-down rate after note-off */
@@ -101,6 +116,7 @@ typedef struct {
101116
/* Block processing state (computed at block boundaries) */
102117
int32_t block_rate; /* Current per-sample rate */
103118
uint8_t block_counter; /* Samples until next rate computation */
119+
int32_t hold_counter; /* Remaining hold samples (runtime state) */
104120
} picosynth_env_t;
105121

106122
/* Single-pole filter state */
@@ -197,21 +213,29 @@ void picosynth_init_osc(picosynth_node_t *n,
197213
const q15_t *freq,
198214
picosynth_wave_func_t wave);
199215

200-
/* Initialize envelope node (rates as increments per sample, scaled) */
216+
/* Initialize AHDSR envelope node with parameter struct.
217+
* @params: Pointer to envelope parameters (attack, hold, decay, sustain,
218+
* release). Rates are increments per sample, scaled <<4 internally.
219+
*/
201220
void picosynth_init_env(picosynth_node_t *n,
202221
const q15_t *gain,
203-
int32_t attack,
204-
int32_t decay,
205-
q15_t sustain,
206-
int32_t release);
222+
const picosynth_env_params_t *params);
207223

208-
/* Initialize envelope with millisecond timings and percentage sustain */
224+
/* Millisecond-based envelope parameters for picosynth_init_env_ms(). */
225+
typedef struct {
226+
uint16_t atk_ms; /* Attack time in milliseconds */
227+
uint16_t hold_ms; /* Hold time in milliseconds (0 = no hold) */
228+
uint16_t dec_ms; /* Decay time in milliseconds */
229+
uint8_t sus_pct; /* Sustain level as percentage (0-100) */
230+
uint16_t rel_ms; /* Release time in milliseconds */
231+
} picosynth_env_ms_params_t;
232+
233+
/* Initialize envelope with millisecond timings and percentage sustain.
234+
* Converts timing parameters to internal rate values.
235+
*/
209236
void picosynth_init_env_ms(picosynth_node_t *n,
210237
const q15_t *gain,
211-
uint16_t atk_ms,
212-
uint16_t dec_ms,
213-
uint8_t sus_pct,
214-
uint16_t rel_ms);
238+
const picosynth_env_ms_params_t *params);
215239

216240
/* Initialize low-pass filter node */
217241
void picosynth_init_lp(picosynth_node_t *n,

src/picosynth.c

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@
66
#include "dsp-math.h"
77
#include "picosynth.h"
88

9-
/* Envelope state: bit 31 = decay mode, bits 0-30 = value */
10-
#define ENVELOPE_STATE_MODE_BIT 0x80000000u
11-
#define ENVELOPE_STATE_VALUE_MASK 0x7FFFFFFF
9+
/* Envelope state: bits 30-31 = mode, bits 0-29 = value
10+
* Mode 0 (0x00): Attack - ramp up to peak
11+
* Mode 1 (0x40): Hold - maintain peak level
12+
* Mode 2 (0x80): Decay/Sustain - exponential decay to sustain
13+
*/
14+
#define ENVELOPE_MODE_ATTACK 0x00000000u
15+
#define ENVELOPE_MODE_HOLD 0x40000000u
16+
#define ENVELOPE_MODE_DECAY 0x80000000u
17+
#define ENVELOPE_MODE_MASK 0xC0000000u
18+
#define ENVELOPE_STATE_VALUE_MASK 0x3FFFFFFF
1219

1320
/* DC blocker coefficient: alpha ≈ 0.995 in Q15 = 32604
1421
* Removes DC offset from waveshaping with ~4 cycles/sample overhead.
@@ -67,6 +74,7 @@ static void voice_note_on(picosynth_voice_t *v, uint8_t note)
6774
if (n->type == PICOSYNTH_NODE_ENV) {
6875
n->env.block_counter = 0;
6976
n->env.block_rate = 0;
77+
n->env.hold_counter = 0;
7078
}
7179
}
7280
}
@@ -403,33 +411,32 @@ void picosynth_init_osc(picosynth_node_t *n,
403411

404412
void picosynth_init_env(picosynth_node_t *n,
405413
const q15_t *gain,
406-
int32_t attack,
407-
int32_t decay,
408-
q15_t sustain,
409-
int32_t release)
414+
const picosynth_env_params_t *params)
410415
{
411416
memset(n, 0, sizeof(picosynth_node_t));
412417
n->gain = gain;
413418
n->type = PICOSYNTH_NODE_ENV;
414-
n->env.attack = attack;
415-
n->env.decay = decay;
416-
n->env.sustain = sustain;
417-
n->env.release = release;
419+
n->env.attack = params->attack;
420+
n->env.hold = params->hold;
421+
n->env.decay = params->decay;
422+
n->env.sustain = params->sustain;
423+
n->env.release = params->release;
424+
n->env.hold_counter = 0;
418425
env_update_exp_coeffs(&n->env);
419426
}
420427

421428
void picosynth_init_env_ms(picosynth_node_t *n,
422429
const q15_t *gain,
423-
uint16_t atk_ms,
424-
uint16_t dec_ms,
425-
uint8_t sus_pct,
426-
uint16_t rel_ms)
430+
const picosynth_env_ms_params_t *params)
427431
{
428-
int32_t atk = (int32_t) PICOSYNTH_ENV_RATE_FROM_MS(atk_ms);
429-
int32_t dec = (int32_t) PICOSYNTH_ENV_RATE_FROM_MS(dec_ms);
430-
q15_t sus = (q15_t) (((int32_t) sus_pct * Q15_MAX) / 100);
431-
int32_t rel = (int32_t) PICOSYNTH_ENV_RATE_FROM_MS(rel_ms);
432-
picosynth_init_env(n, gain, atk, dec, sus, rel);
432+
picosynth_env_params_t p = {
433+
.attack = (int32_t) PICOSYNTH_ENV_RATE_FROM_MS(params->atk_ms),
434+
.hold = (int32_t) PICOSYNTH_MS(params->hold_ms),
435+
.decay = (int32_t) PICOSYNTH_ENV_RATE_FROM_MS(params->dec_ms),
436+
.sustain = (q15_t) (((int32_t) params->sus_pct * Q15_MAX) / 100),
437+
.release = (int32_t) PICOSYNTH_ENV_RATE_FROM_MS(params->rel_ms),
438+
};
439+
picosynth_init_env(n, gain, &p);
433440
}
434441

435442
void picosynth_init_lp(picosynth_node_t *n,
@@ -712,29 +719,30 @@ q15_t picosynth_process(picosynth_t *s)
712719
(int32_t) (((uint32_t) n->state) & (uint32_t) Q15_MAX);
713720
break;
714721
case PICOSYNTH_NODE_ENV: {
715-
/* Block-based envelope: compute rate at block boundaries,
716-
* check for phase transitions per-sample. */
722+
/* Block-based AHDSR envelope: compute rate at block
723+
* boundaries, check for phase transitions per-sample. */
724+
uint32_t mode = ((uint32_t) n->state) & ENVELOPE_MODE_MASK;
717725

718726
/* Recompute rate at block boundary */
719727
if (n->env.block_counter == 0) {
720728
n->env.block_counter = PICOSYNTH_BLOCK_SIZE;
721729
if (!v->gate) {
722730
n->env.block_rate = -n->env.release; /* Informational */
723-
} else if (((uint32_t) n->state) &
724-
ENVELOPE_STATE_MODE_BIT) {
731+
} else if (mode == ENVELOPE_MODE_DECAY) {
725732
n->env.block_rate = -n->env.decay; /* Informational */
733+
} else if (mode == ENVELOPE_MODE_HOLD) {
734+
n->env.block_rate = 0; /* Hold at peak */
726735
} else {
727736
n->env.block_rate = n->env.attack;
728737
}
729738
}
730739
n->env.block_counter--;
731740

732-
/* Apply rate */
741+
/* Apply rate based on mode */
733742
int32_t val = n->state & ENVELOPE_STATE_VALUE_MASK;
734743
if (v->gate) {
735-
uint32_t mode =
736-
((uint32_t) n->state) & ENVELOPE_STATE_MODE_BIT;
737-
if (mode) {
744+
if (mode == ENVELOPE_MODE_DECAY) {
745+
/* Decay/Sustain phase */
738746
q15_t sus_abs = n->env.sustain < 0 ? -n->env.sustain
739747
: n->env.sustain;
740748
int32_t sus_level = sus_abs << 4;
@@ -746,24 +754,40 @@ q15_t picosynth_process(picosynth_t *s)
746754
15);
747755
if (val < sus_level)
748756
val = sus_level;
757+
} else if (mode == ENVELOPE_MODE_HOLD) {
758+
/* Hold phase: maintain peak, count down */
759+
val = (int32_t) Q15_MAX << 4; /* Stay at peak */
760+
if (n->env.hold_counter > 0)
761+
n->env.hold_counter--;
762+
if (n->env.hold_counter == 0) {
763+
/* Transition to decay mode */
764+
mode = ENVELOPE_MODE_DECAY;
765+
n->env.block_counter = 0;
766+
}
749767
} else {
750-
/* Attack: check for transition to decay */
768+
/* Attack phase: ramp up to peak */
751769
val += n->env.block_rate;
752770
if (val >= (int32_t) Q15_MAX << 4) {
753771
val = (int32_t) Q15_MAX << 4;
754-
mode = ENVELOPE_STATE_MODE_BIT;
772+
/* Check if hold phase is configured */
773+
if (n->env.hold > 0) {
774+
mode = ENVELOPE_MODE_HOLD;
775+
n->env.hold_counter = n->env.hold;
776+
} else {
777+
mode = ENVELOPE_MODE_DECAY;
778+
}
755779
/* Force rate recalculation next sample */
756780
n->env.block_counter = 0;
757781
}
758782
}
759783
n->state = (int32_t) (((uint32_t) val) | mode);
760784
} else {
761-
/* Exponential release */
785+
/* Exponential release (mode cleared) */
762786
val = (int32_t) (((int64_t) val * n->env.release_coeff) >>
763787
15);
764788
if (val < 16)
765789
val = 0;
766-
n->state = val;
790+
n->state = val; /* mode bits clear during release */
767791
}
768792
break;
769793
}

tests/example.c

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,14 @@ int main(void)
155155
/* Fundamental envelope: VERY slow decay for sustained body
156156
* Real piano fundamental sustains for seconds
157157
*/
158-
picosynth_init_env(
159-
v0_env, NULL, 10000, /* attack - smooth onset */
160-
60, /* decay - VERY slow (sustains long) */
161-
(q15_t) (Q15_MAX * 15 / 100), /* sustain (15%) - strong body */
162-
40); /* release - gentle fade */
158+
picosynth_init_env(v0_env, NULL,
159+
&(picosynth_env_params_t) {
160+
.attack = 10000, /* smooth onset */
161+
.hold = 0, /* none (immediate decay) */
162+
.decay = 60, /* VERY slow (sustains long) */
163+
.sustain = (q15_t) (Q15_MAX * 15 / 100), /* 15% */
164+
.release = 40, /* gentle fade */
165+
});
163166

164167
/* Pure sine at fundamental frequency */
165168
picosynth_init_osc(v0_osc, &v0_env->out, picosynth_voice_freq_ptr(v),
@@ -184,21 +187,29 @@ int main(void)
184187
picosynth_node_t *v1_mix = picosynth_voice_get_node(v, 5);
185188

186189
/* 2nd partial envelope: medium decay (faster than fundamental) */
187-
picosynth_init_env(v1_env1, NULL, 8000, /* attack */
188-
150, /* decay - MEDIUM rate */
189-
(q15_t) (Q15_MAX * 8 / 100), /* sustain (8%) */
190-
50); /* release */
190+
picosynth_init_env(v1_env1, NULL,
191+
&(picosynth_env_params_t) {
192+
.attack = 8000,
193+
.hold = 0,
194+
.decay = 150, /* MEDIUM rate */
195+
.sustain = (q15_t) (Q15_MAX * 8 / 100), /* 8% */
196+
.release = 50,
197+
});
191198

192199
/* 2nd partial: sine at 2*base_freq with inharmonicity stretch */
193200
picosynth_init_osc(v1_osc1, &v1_env1->out, picosynth_voice_freq_ptr(v),
194201
picosynth_wave_sine);
195202
v1_osc1->osc.detune = &partial2_offset; /* offset = f1, so total = 2*f1 */
196203

197204
/* 3rd partial envelope: slightly faster decay than 2nd */
198-
picosynth_init_env(v1_env2, NULL, 7000, /* attack */
199-
300, /* decay - faster than 2nd partial */
200-
(q15_t) (Q15_MAX * 4 / 100), /* sustain (4%) */
201-
40); /* release */
205+
picosynth_init_env(v1_env2, NULL,
206+
&(picosynth_env_params_t) {
207+
.attack = 7000,
208+
.hold = 0,
209+
.decay = 300, /* faster than 2nd partial */
210+
.sustain = (q15_t) (Q15_MAX * 4 / 100), /* 4% */
211+
.release = 40,
212+
});
202213

203214
/* 3rd partial: sine at 3*base_freq with inharmonicity stretch */
204215
picosynth_init_osc(v1_osc2, &v1_env2->out, picosynth_voice_freq_ptr(v),
@@ -225,11 +236,14 @@ int main(void)
225236
/* Upper partials envelope: FAST decay, LOW level
226237
* Subtle contribution that fades quickly
227238
*/
228-
picosynth_init_env(
229-
v2_env, NULL, 5000, /* attack - soft onset */
230-
800, /* decay - fast fade */
231-
(q15_t) (Q15_MAX * 1 / 100), /* sustain (1%) - nearly silent */
232-
20); /* release - quick */
239+
picosynth_init_env(v2_env, NULL,
240+
&(picosynth_env_params_t) {
241+
.attack = 5000, /* soft onset */
242+
.hold = 0,
243+
.decay = 800, /* fast fade */
244+
.sustain = (q15_t) (Q15_MAX * 1 / 100), /* 1% */
245+
.release = 20, /* quick */
246+
});
233247

234248
/* Sine for clean sound - no harsh harmonics */
235249
picosynth_init_osc(v2_osc, &v2_env->out, picosynth_voice_freq_ptr(v),
@@ -253,10 +267,14 @@ int main(void)
253267
/* Hammer noise: very subtle, almost imperceptible
254268
* Just adds slight "thump" texture, not harsh attack
255269
*/
256-
picosynth_init_env(v3_env, NULL, 8000, /* attack - soft */
257-
6000, /* decay - very fast */
258-
0, /* sustain - none */
259-
50); /* release */
270+
picosynth_init_env(v3_env, NULL,
271+
&(picosynth_env_params_t) {
272+
.attack = 8000, /* soft */
273+
.hold = 0,
274+
.decay = 6000, /* very fast */
275+
.sustain = 0, /* none */
276+
.release = 50,
277+
});
260278

261279
/* White noise source */
262280
picosynth_init_osc(v3_noise, &v3_env->out, picosynth_voice_freq_ptr(v),

0 commit comments

Comments
 (0)