Skip to content

Commit 61b7f8b

Browse files
committed
Add 2-pole SVF filter and fix overflow issues
- Add State Variable Filter (SVF) with LP/HP/BP outputs (-12dB/oct) - Fix SVF HP output scaling to match internal state dynamics - Fix HP calculation overflow using int64_t with proper clamping - Add Nyquist guards for 2nd/3rd partials to prevent aliasing - Clarify Q parameter documentation (damping factor, not 1/Q) - Update picosynth_svf_freq() doc to reflect actual valid range
1 parent 28a19da commit 61b7f8b

File tree

6 files changed

+676
-292
lines changed

6 files changed

+676
-292
lines changed

include/picosynth.h

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ typedef struct {
111111
q15_t coeff_target; /* Target cutoff for smoothing */
112112
} picosynth_filter_t;
113113

114+
/* Two-pole State Variable Filter (SVF) state.
115+
* Provides -12dB/octave rolloff with resonance control.
116+
* Topology: hp = in - lp - q*bp; lp += f*bp; bp += f*hp
117+
*/
118+
typedef struct {
119+
const q15_t *in; /* Input signal pointer */
120+
int32_t lp; /* Low-pass state (Q15 scaled <<8 for precision) */
121+
int32_t bp; /* Band-pass state (Q15 scaled <<8) */
122+
q15_t f; /* Frequency coefficient: 2*sin(pi*fc/fs) in Q15 */
123+
q15_t f_target; /* Target frequency for smoothing */
124+
q15_t q; /* Damping factor. Higher means more damping (less resonance).
125+
* Q15_MAX = maximum damping (no resonance), 0 = self-oscillation.
126+
* Note: This is NOT 1/Q since 1/Q can exceed Q15 range.
127+
*/
128+
} picosynth_svf_t;
129+
114130
/* 3-input mixer state */
115131
typedef struct {
116132
const q15_t *in[3]; /* Input signal pointers (NULL = unused) */
@@ -124,6 +140,9 @@ typedef enum {
124140
PICOSYNTH_NODE_LP,
125141
PICOSYNTH_NODE_HP,
126142
PICOSYNTH_NODE_MIX,
143+
PICOSYNTH_NODE_SVF_LP, /* 2-pole SVF low-pass (-12dB/oct) */
144+
PICOSYNTH_NODE_SVF_HP, /* 2-pole SVF high-pass */
145+
PICOSYNTH_NODE_SVF_BP, /* 2-pole SVF band-pass */
127146
} picosynth_node_type_t;
128147

129148
/* Audio processing node */
@@ -136,6 +155,7 @@ typedef struct {
136155
picosynth_osc_t osc;
137156
picosynth_env_t env;
138157
picosynth_filter_t flt;
158+
picosynth_svf_t svf;
139159
picosynth_mixer_t mix;
140160
};
141161
} picosynth_node_t;
@@ -208,6 +228,41 @@ void picosynth_init_hp(picosynth_node_t *n,
208228
/* Set filter cutoff with smoothing */
209229
void picosynth_filter_set_coeff(picosynth_node_t *n, q15_t coeff);
210230

231+
/* Initialize 2-pole SVF low-pass filter (-12dB/octave).
232+
* @f_coeff : frequency coefficient (use picosynth_svf_freq() to calculate)
233+
* @q : damping factor in Q15. Q15_MAX/2 (16384) = Butterworth (Q=0.707)
234+
* Higher values = more resonance, lower values = overdamped
235+
*/
236+
void picosynth_init_svf_lp(picosynth_node_t *n,
237+
const q15_t *gain,
238+
const q15_t *in,
239+
q15_t f_coeff,
240+
q15_t q);
241+
242+
/* Initialize 2-pole SVF high-pass filter */
243+
void picosynth_init_svf_hp(picosynth_node_t *n,
244+
const q15_t *gain,
245+
const q15_t *in,
246+
q15_t f_coeff,
247+
q15_t q);
248+
249+
/* Initialize 2-pole SVF band-pass filter */
250+
void picosynth_init_svf_bp(picosynth_node_t *n,
251+
const q15_t *gain,
252+
const q15_t *in,
253+
q15_t f_coeff,
254+
q15_t q);
255+
256+
/* Set SVF frequency coefficient with smoothing */
257+
void picosynth_svf_set_freq(picosynth_node_t *n, q15_t f_coeff);
258+
259+
/* Calculate SVF frequency coefficient from cutoff frequency (Hz).
260+
* Returns f = 2*sin(pi*fc/fs) in Q15 format.
261+
* Valid range: 1 Hz to SAMPLE_RATE/4 (higher values clamped for stability).
262+
* Note: Very low frequencies (<20 Hz) may have reduced precision.
263+
*/
264+
q15_t picosynth_svf_freq(uint16_t fc_hz);
265+
211266
/* Initialize 3-input mixer node */
212267
void picosynth_init_mix(picosynth_node_t *n,
213268
const q15_t *gain,

src/sinewave.h renamed to src/dsp-math.h

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/*
2-
* Sine waveform implementation
2+
* DSP math implementation
33
*
4-
* Configuration (define before including picosynth.h):
4+
* Sine waveform configuration (define before including picosynth.h):
55
* PICOSYNTH_SINE_LUT_8BIT - 8-bit 129-entry LUT (default, smallest)
66
* PICOSYNTH_SINE_LUT_16BIT - 16-bit 257-entry LUT (higher quality)
77
* PICOSYNTH_USE_SINF - Use sinf() (highest quality, needs FPU)
88
*/
99

10-
#ifndef PICOSYNTH_MATH_H_
11-
#define PICOSYNTH_MATH_H_
10+
#ifndef PICOSYNTH_DSP_MATH_H_
11+
#define PICOSYNTH_DSP_MATH_H_
1212

1313
#include "picosynth.h"
1414

@@ -78,6 +78,16 @@ static const q15_t sine_lut16[257] = {
7878
};
7979
#endif
8080

81+
/* Pre-computed sine table for SVF frequency calculation.
82+
* sin(pi * i / 64) * 32767 for i = 0..32 (quarter wave)
83+
* Covers 0 to pi/2 which maps to fc/fs = 0 to 0.5
84+
*/
85+
static const q15_t svf_sin_table[33] = {
86+
0, 1608, 3212, 4808, 6393, 7962, 9512, 11039, 12540,
87+
14010, 15447, 16846, 18205, 19520, 20788, 22006, 23170, 24279,
88+
25330, 26320, 27246, 28106, 28899, 29622, 30274, 30853, 31357,
89+
31786, 32138, 32413, 32610, 32729, 32767};
90+
8191
/* Internal sine generator (static inline for performance)
8292
* Input: phase in [0, Q15_MAX]
8393
* Output: sine value in [-Q15_MAX, Q15_MAX]
@@ -108,4 +118,4 @@ static inline q15_t picosynth_sine_impl(q15_t phase)
108118
#endif
109119
}
110120

111-
#endif /* PICOSYNTH_MATH_H_ */
121+
#endif /* PICOSYNTH_DSP_MATH_H_ */

src/picosynth.c

Lines changed: 197 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
#include <stdlib.h>
44
#include <string.h>
55

6+
#include "dsp-math.h"
67
#include "picosynth.h"
7-
#include "sinewave.h"
88

99
/* Envelope state: bit 31 = decay mode, bits 0-30 = value */
1010
#define ENVELOPE_STATE_MODE_BIT 0x80000000u
@@ -48,6 +48,14 @@ static void voice_note_on(picosynth_voice_t *v, uint8_t note)
4848
n->flt.accum = 0;
4949
n->flt.coeff = n->flt.coeff_target;
5050
}
51+
/* Reset SVF states */
52+
if (n->type == PICOSYNTH_NODE_SVF_LP ||
53+
n->type == PICOSYNTH_NODE_SVF_HP ||
54+
n->type == PICOSYNTH_NODE_SVF_BP) {
55+
n->svf.lp = 0;
56+
n->svf.bp = 0;
57+
n->svf.f = n->svf.f_target;
58+
}
5159
/* Reset envelope block state to force immediate rate calculation */
5260
if (n->type == PICOSYNTH_NODE_ENV) {
5361
n->env.block_counter = 0;
@@ -86,7 +94,7 @@ static int ptr_to_node_idx(picosynth_voice_t *v, const q15_t *ptr)
8694
}
8795

8896
/* Recursively mark node and all its dependencies as used.
89-
* NOTE: mask is 8-bit, so only nodes 0-7 can be tracked. For voices with >8
97+
* Note: mask is 8-bit, so only nodes 0-7 can be tracked. For voices with >8
9098
* nodes, the mask optimization is disabled (mask stays 0, all nodes processed).
9199
*/
92100
static void mark_node_used(picosynth_voice_t *v, int idx)
@@ -129,6 +137,13 @@ static void mark_node_used(picosynth_voice_t *v, int idx)
129137
if (dep >= 0)
130138
mark_node_used(v, dep);
131139
break;
140+
case PICOSYNTH_NODE_SVF_LP:
141+
case PICOSYNTH_NODE_SVF_HP:
142+
case PICOSYNTH_NODE_SVF_BP:
143+
dep = ptr_to_node_idx(v, n->svf.in);
144+
if (dep >= 0)
145+
mark_node_used(v, dep);
146+
break;
132147
case PICOSYNTH_NODE_MIX:
133148
for (int j = 0; j < 3; j++) {
134149
dep = ptr_to_node_idx(v, n->mix.in[j]);
@@ -443,6 +458,106 @@ void picosynth_filter_set_coeff(picosynth_node_t *n, q15_t coeff)
443458
n->flt.coeff_target = q15_sat(coeff);
444459
}
445460

461+
q15_t picosynth_svf_freq(uint16_t fc_hz)
462+
{
463+
/* f = 2 * sin(pi * fc / fs)
464+
* For stability, fc should be < fs/4 (Nyquist/2)
465+
* Map fc_hz to table index: idx = fc_hz * 64 / SAMPLE_RATE
466+
*/
467+
if (fc_hz == 0)
468+
return 0;
469+
470+
/* Clamp to safe range (fs/4 max for stability) */
471+
uint32_t max_fc = SAMPLE_RATE / 4;
472+
if (fc_hz > max_fc)
473+
fc_hz = (uint16_t) max_fc;
474+
475+
/* Calculate table index with interpolation
476+
* idx = fc * 64 / fs, but we use fc * 64 * 256 / fs for fractional part
477+
*/
478+
uint32_t scaled = ((uint32_t) fc_hz * 64 * 256) / SAMPLE_RATE;
479+
uint8_t idx = (uint8_t) (scaled >> 8);
480+
uint8_t frac = (uint8_t) (scaled & 0xFF);
481+
482+
if (idx >= 32) {
483+
idx = 32;
484+
frac = 0;
485+
}
486+
487+
/* Linear interpolation between table entries */
488+
int32_t s0 = svf_sin_table[idx];
489+
int32_t s1 = svf_sin_table[idx + 1];
490+
int32_t sin_val = s0 + (((s1 - s0) * frac) >> 8);
491+
492+
/* f = 2 * sin(...), but we limit to avoid instability */
493+
int32_t f = sin_val * 2;
494+
if (f > Q15_MAX)
495+
f = Q15_MAX;
496+
497+
return (q15_t) f;
498+
}
499+
500+
void picosynth_init_svf_lp(picosynth_node_t *n,
501+
const q15_t *gain,
502+
const q15_t *in,
503+
q15_t f_coeff,
504+
q15_t q)
505+
{
506+
memset(n, 0, sizeof(picosynth_node_t));
507+
n->gain = gain;
508+
n->type = PICOSYNTH_NODE_SVF_LP;
509+
n->svf.in = in;
510+
n->svf.f = f_coeff;
511+
n->svf.f_target = f_coeff;
512+
n->svf.q = q;
513+
n->svf.lp = 0;
514+
n->svf.bp = 0;
515+
}
516+
517+
void picosynth_init_svf_hp(picosynth_node_t *n,
518+
const q15_t *gain,
519+
const q15_t *in,
520+
q15_t f_coeff,
521+
q15_t q)
522+
{
523+
memset(n, 0, sizeof(picosynth_node_t));
524+
n->gain = gain;
525+
n->type = PICOSYNTH_NODE_SVF_HP;
526+
n->svf.in = in;
527+
n->svf.f = f_coeff;
528+
n->svf.f_target = f_coeff;
529+
n->svf.q = q;
530+
n->svf.lp = 0;
531+
n->svf.bp = 0;
532+
}
533+
534+
void picosynth_init_svf_bp(picosynth_node_t *n,
535+
const q15_t *gain,
536+
const q15_t *in,
537+
q15_t f_coeff,
538+
q15_t q)
539+
{
540+
memset(n, 0, sizeof(picosynth_node_t));
541+
n->gain = gain;
542+
n->type = PICOSYNTH_NODE_SVF_BP;
543+
n->svf.in = in;
544+
n->svf.f = f_coeff;
545+
n->svf.f_target = f_coeff;
546+
n->svf.q = q;
547+
n->svf.lp = 0;
548+
n->svf.bp = 0;
549+
}
550+
551+
void picosynth_svf_set_freq(picosynth_node_t *n, q15_t f_coeff)
552+
{
553+
if (!n)
554+
return;
555+
if (n->type != PICOSYNTH_NODE_SVF_LP && n->type != PICOSYNTH_NODE_SVF_HP &&
556+
n->type != PICOSYNTH_NODE_SVF_BP)
557+
return;
558+
n->svf.f_target = f_coeff;
559+
}
560+
446561
void picosynth_init_mix(picosynth_node_t *n,
447562
const q15_t *gain,
448563
const q15_t *in1,
@@ -530,6 +645,30 @@ q15_t picosynth_process(picosynth_t *s)
530645
tmp[i] = 0;
531646
}
532647
break;
648+
case PICOSYNTH_NODE_SVF_LP:
649+
/* SVF low-pass output: scaled down from internal precision */
650+
tmp[i] = n->svf.lp >> 8;
651+
break;
652+
case PICOSYNTH_NODE_SVF_HP: {
653+
/* SVF high-pass: hp = in - lp - q*bp
654+
* Computed at internal <<8 scale for consistency with state,
655+
* then scaled down. Uses int64_t to prevent overflow.
656+
*/
657+
int64_t in_val = n->svf.in ? ((int32_t) *n->svf.in) << 8 : 0;
658+
int64_t q_bp = ((int64_t) n->svf.bp * n->svf.q) >> 15;
659+
int64_t hp = in_val - n->svf.lp - q_bp;
660+
/* Clamp to int32_t range before scaling */
661+
if (hp > 0x7FFFFFFF)
662+
hp = 0x7FFFFFFF;
663+
else if (hp < (-0x7FFFFFFF - 1))
664+
hp = (-0x7FFFFFFF - 1);
665+
tmp[i] = (int32_t) (hp >> 8);
666+
break;
667+
}
668+
case PICOSYNTH_NODE_SVF_BP:
669+
/* SVF band-pass output */
670+
tmp[i] = n->svf.bp >> 8;
671+
break;
533672
case PICOSYNTH_NODE_MIX: {
534673
int32_t sum = 0;
535674
for (int j = 0; j < 3; j++)
@@ -623,8 +762,8 @@ q15_t picosynth_process(picosynth_t *s)
623762
}
624763
case PICOSYNTH_NODE_LP:
625764
case PICOSYNTH_NODE_HP: {
626-
/* Smooth cutoff changes to avoid zipper noise (~4ms time
627-
* constant: delta/256 per sample).
765+
/* Smooth cutoff changes to avoid zipper noise.
766+
* Time constant: ~256 samples (~23ms @ 11kHz, ~6ms @ 44kHz).
628767
*/
629768
int32_t coeff_delta =
630769
(int32_t) n->flt.coeff_target - n->flt.coeff;
@@ -650,6 +789,60 @@ q15_t picosynth_process(picosynth_t *s)
650789
n->flt.accum = (int32_t) acc;
651790
break;
652791
}
792+
case PICOSYNTH_NODE_SVF_LP:
793+
case PICOSYNTH_NODE_SVF_HP:
794+
case PICOSYNTH_NODE_SVF_BP: {
795+
/* Smooth frequency changes to avoid zipper noise */
796+
int32_t f_delta = (int32_t) n->svf.f_target - n->svf.f;
797+
if (f_delta) {
798+
int32_t step = f_delta >> 8;
799+
if (step == 0)
800+
step = f_delta > 0 ? 1 : -1;
801+
n->svf.f = q15_sat((int32_t) n->svf.f + step);
802+
}
803+
804+
/* State Variable Filter update:
805+
* hp = in - lp - q*bp
806+
* lp_new = lp + f*bp
807+
* bp_new = bp + f*hp
808+
*
809+
* States stored with <<8 scaling for precision.
810+
*/
811+
int32_t in_val = n->svf.in ? ((int32_t) *n->svf.in) << 8 : 0;
812+
int32_t lp = n->svf.lp;
813+
int32_t bp = n->svf.bp;
814+
815+
/* Compute high-pass: hp = in - lp - q*bp
816+
* Uses int64_t to prevent overflow with large <<8 scaled
817+
* values.
818+
*/
819+
int64_t q_bp = ((int64_t) bp * n->svf.q) >> 15;
820+
int64_t hp64 = (int64_t) in_val - lp - q_bp;
821+
if (hp64 > 0x7FFFFFFF)
822+
hp64 = 0x7FFFFFFF;
823+
else if (hp64 < (-0x7FFFFFFF - 1))
824+
hp64 = (-0x7FFFFFFF - 1);
825+
int32_t hp = (int32_t) hp64;
826+
827+
/* Update low-pass: lp += f*bp */
828+
int32_t f_bp = (int32_t) (((int64_t) bp * n->svf.f) >> 15);
829+
int64_t lp_new = (int64_t) lp + f_bp;
830+
if (lp_new > 0x7FFFFFFF)
831+
lp_new = 0x7FFFFFFF;
832+
else if (lp_new < (-0x7FFFFFFF - 1))
833+
lp_new = (-0x7FFFFFFF - 1);
834+
n->svf.lp = (int32_t) lp_new;
835+
836+
/* Update band-pass: bp += f*hp */
837+
int32_t f_hp = (int32_t) (((int64_t) hp * n->svf.f) >> 15);
838+
int64_t bp_new = (int64_t) bp + f_hp;
839+
if (bp_new > 0x7FFFFFFF)
840+
bp_new = 0x7FFFFFFF;
841+
else if (bp_new < (-0x7FFFFFFF - 1))
842+
bp_new = (-0x7FFFFFFF - 1);
843+
n->svf.bp = (int32_t) bp_new;
844+
break;
845+
}
653846
default:
654847
break;
655848
}

0 commit comments

Comments
 (0)