From 56646d0fa8f73a1a4704b2c48cd7440f23d395ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:19:38 +0000 Subject: [PATCH 1/3] Add build-time Goertzel decoder option Co-authored-by: kulp <74288+kulp@users.noreply.github.com> --- Makefile | 1 + README.md | 4 +++ src/decode-impl.h | 7 ++++- src/decode.c | 80 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 438ca28..363ad57 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ endif CPPFLAGS += $(if $(ENCODE_BITS),-DENCODE_BITS=$(ENCODE_BITS)) CPPFLAGS += $(if $(DECODE_BITS),-DDECODE_BITS=$(DECODE_BITS)) +CPPFLAGS += $(if $(filter goertzel,$(DECODE_METHOD)),-DUSE_GOERTZEL) CPPFLAGS += -DSAMPLE_RATE=$(SAMPLE_RATE) diff --git a/README.md b/README.md index 7a54d9e..6cb8992 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ Unless you are building for an AVR target like the [ATTINY412], you probably wan make gen listen +By default, `listen` uses the existing notch-filter based decoder. To build `listen` with a Goertzel-based detector instead: + + make DECODE_METHOD=goertzel listen + ## Basic functionality The `gen` binary takes ASCII data on `stdin` and produces raw monoaural audio on `stdout` as 16-bit signed integers at 8000Hz by default. The `listen` binary does the reverse, so they can be chained together, as illustrated in the examples below. diff --git a/src/decode-impl.h b/src/decode-impl.h index 821b98c..0cfbcd4 100644 --- a/src/decode-impl.h +++ b/src/decode-impl.h @@ -66,11 +66,16 @@ struct filter_state { }; struct decode_state { +#if defined(USE_GOERTZEL) + int8_t signal_window[MAX_RMS_SAMPLES]; + uint8_t signal_ptr; + bool signal_primed; +#else struct power_state power[2]; struct filter_state filt[2]; +#endif struct runs_state run; struct bits_state dec; }; #endif - diff --git a/src/decode.c b/src/decode.c index bb46fd0..8099c7b 100644 --- a/src/decode.c +++ b/src/decode.c @@ -131,6 +131,7 @@ static bool runs(int8_t hysteresis, struct runs_state *s, RUNS_IN_DATA da, RUNS_ return true; } +#if ! defined(USE_GOERTZEL) static bool filter(const struct filter_config * PROGMEM c, struct filter_state *s, FILTER_IN_DATA datum, FILTER_OUT_DATA *out) { s->in[s->ptr] = datum; @@ -162,6 +163,72 @@ static bool filter(const struct filter_config * PROGMEM c, struct filter_state * return true; } +#else +static bool goertzel_window(uint8_t window_size, DECODE_STATE *s, int8_t datum) +{ + s->signal_window[s->signal_ptr] = datum; + + if (s->signal_ptr == window_size - 1) + s->signal_primed = true; + + // Avoid expensive modulo + if (++s->signal_ptr >= window_size) + s->signal_ptr = 0; + + return s->signal_primed; +} + +static bool goertzel_power(const struct filter_config *c, uint8_t window_size, DECODE_STATE *s, RMS_OUT_DATA *out) +{ + if (c->coeff_b0 == 0) + return false; + +#if defined(USE_FLOATING_POINT) + const float coeff = -c->coeff_b1 / c->coeff_b0; + float q1 = 0; + float q2 = 0; +#else + const int32_t coeff = -((int32_t)c->coeff_b1 << COEFF_FRACTIONAL_BITS) / c->coeff_b0; + int32_t s1 = 0; + int32_t s2 = 0; +#endif + uint8_t ptr = s->signal_ptr; + for (uint8_t i = 0; i < window_size; i++) { + const int8_t datum = s->signal_window[ptr]; + +#if defined(USE_FLOATING_POINT) + const float q0 = (float)datum + coeff * q1 - q2; + q2 = q1; + q1 = q0; +#else + const int32_t s0 = (int32_t)datum + (int32_t)(((int64_t)coeff * s1) >> COEFF_FRACTIONAL_BITS) - s2; + s2 = s1; + s1 = s0; +#endif + + if (++ptr >= window_size) + ptr = 0; + } + +#if defined(USE_FLOATING_POINT) + float power = q1 * q1 + q2 * q2 - coeff * q1 * q2; + if (power < 0) + power = 0; + if (power > UINT16_MAX) + power = UINT16_MAX; + *out = (RMS_OUT_DATA)power; +#else + int64_t power = ((int64_t)s1 * s1) + ((int64_t)s2 * s2) - (((int64_t)coeff * s1 * s2) >> COEFF_FRACTIONAL_BITS); + if (power < 0) + power = 0; + if (power > UINT16_MAX) + power = UINT16_MAX; + *out = (RMS_OUT_DATA)power; +#endif + + return true; +} +#endif bool CAT(pump_decoder,DECODE_BITS)( const SERIAL_CONFIG *c, @@ -174,6 +241,17 @@ bool CAT(pump_decoder,DECODE_BITS)( { DECODE_DATA_TYPE *in = (DECODE_DATA_TYPE*)p; +#if defined(USE_GOERTZEL) + if (! goertzel_window(audio->window_size, s, (int8_t)SHRINK(*in, int8_t))) + return false; + + RMS_OUT_DATA ra = 0, rb = 0; + if ( + ! goertzel_power(&coeffs[BIT_ZERO], audio->window_size, s, &ra) + || ! goertzel_power(&coeffs[BIT_ONE ], audio->window_size, s, &rb) + ) + return false; +#else FILTER_OUT_DATA f[2] = { 0 }; if ( ! filter(&coeffs[BIT_ZERO], &s->filt[0], *in, &f[0]) @@ -187,6 +265,7 @@ bool CAT(pump_decoder,DECODE_BITS)( || ! power(audio->window_size, &s->power[1], (int8_t)SHRINK((FILTER_OUT_DATA)(f[1] - *in), int8_t), &rb) ) return false; +#endif if (ra < audio->threshold && rb < audio->threshold) return false; @@ -197,4 +276,3 @@ bool CAT(pump_decoder,DECODE_BITS)( return decode(c, &s->dec, audio->offset, ro, out); } - From 3f3dcf1f5184821c11b14031a8a73548dcf589f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:25:45 +0000 Subject: [PATCH 2/3] Harden Goertzel fixed-point implementation Co-authored-by: kulp <74288+kulp@users.noreply.github.com> --- src/decode-impl.h | 7 ++--- src/decode.c | 69 ++++++++++++++++++++++++++++++++++------------- src/types.h | 6 +++-- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/decode-impl.h b/src/decode-impl.h index 0cfbcd4..81b7edc 100644 --- a/src/decode-impl.h +++ b/src/decode-impl.h @@ -28,6 +28,7 @@ #define THRESHOLD 0 #define MAX_RMS_SAMPLES 8 +#define GOERTZEL_WINDOW_SIZE SAMPLES_PER_BIT #define EXPAND(X,Y) (assert(sizeof(Y) >= sizeof(X)), (X) << (CHAR_BIT * (sizeof(Y) - sizeof(X)))) #define SHRINK(X,Y) (assert(sizeof(X) >= sizeof(Y)), (X) >> (CHAR_BIT * (sizeof(X) - sizeof(Y)))) @@ -67,9 +68,9 @@ struct filter_state { struct decode_state { #if defined(USE_GOERTZEL) - int8_t signal_window[MAX_RMS_SAMPLES]; - uint8_t signal_ptr; - bool signal_primed; + int8_t signal_window[GOERTZEL_WINDOW_SIZE]; + uint16_t signal_ptr; + uint16_t signal_count; #else struct power_state power[2]; struct filter_state filt[2]; diff --git a/src/decode.c b/src/decode.c index 8099c7b..303417c 100644 --- a/src/decode.c +++ b/src/decode.c @@ -33,6 +33,13 @@ #define PROGMEM #endif +#if defined(USE_GOERTZEL) +static int64_t abs64(int64_t x) +{ + return x < 0 ? -x : x; +} +#endif + static bool decode(const SERIAL_CONFIG *c, struct bits_state *s, int8_t offset, DECODE_IN_DATA datum, char *out) { const uint8_t before_parity = (uint8_t)(NUM_START_BITS + c->data_bits); @@ -91,6 +98,7 @@ static bool decode(const SERIAL_CONFIG *c, struct bits_state *s, int8_t offset, return false; } +#if ! defined(USE_GOERTZEL) static bool power(const uint8_t window_size, struct power_state *s, RMS_IN_DATA datum, RMS_OUT_DATA *out) { s->sum -= s->window[s->ptr]; @@ -109,6 +117,7 @@ static bool power(const uint8_t window_size, struct power_state *s, RMS_IN_DATA return s->primed; } +#endif static bool runs(int8_t hysteresis, struct runs_state *s, RUNS_IN_DATA da, RUNS_IN_DATA db, RUNS_OUT_DATA *out) { @@ -164,21 +173,21 @@ static bool filter(const struct filter_config * PROGMEM c, struct filter_state * return true; } #else -static bool goertzel_window(uint8_t window_size, DECODE_STATE *s, int8_t datum) +static bool goertzel_window(DECODE_STATE *s, int8_t datum) { s->signal_window[s->signal_ptr] = datum; - if (s->signal_ptr == window_size - 1) - s->signal_primed = true; + if (s->signal_count < GOERTZEL_WINDOW_SIZE) + s->signal_count++; // Avoid expensive modulo - if (++s->signal_ptr >= window_size) + if (++s->signal_ptr >= GOERTZEL_WINDOW_SIZE) s->signal_ptr = 0; - return s->signal_primed; + return s->signal_count >= GOERTZEL_WINDOW_SIZE; } -static bool goertzel_power(const struct filter_config *c, uint8_t window_size, DECODE_STATE *s, RMS_OUT_DATA *out) +static bool goertzel_power(const struct filter_config *c, DECODE_STATE *s, RMS_OUT_DATA *out) { if (c->coeff_b0 == 0) return false; @@ -189,11 +198,11 @@ static bool goertzel_power(const struct filter_config *c, uint8_t window_size, D float q2 = 0; #else const int32_t coeff = -((int32_t)c->coeff_b1 << COEFF_FRACTIONAL_BITS) / c->coeff_b0; - int32_t s1 = 0; - int32_t s2 = 0; + int64_t s1 = 0; + int64_t s2 = 0; #endif - uint8_t ptr = s->signal_ptr; - for (uint8_t i = 0; i < window_size; i++) { + uint16_t ptr = s->signal_ptr; + for (uint16_t i = 0; i < GOERTZEL_WINDOW_SIZE; i++) { const int8_t datum = s->signal_window[ptr]; #if defined(USE_FLOATING_POINT) @@ -201,12 +210,12 @@ static bool goertzel_power(const struct filter_config *c, uint8_t window_size, D q2 = q1; q1 = q0; #else - const int32_t s0 = (int32_t)datum + (int32_t)(((int64_t)coeff * s1) >> COEFF_FRACTIONAL_BITS) - s2; + const int64_t s0 = (int64_t)datum + (((int64_t)coeff * s1) >> COEFF_FRACTIONAL_BITS) - s2; s2 = s1; s1 = s0; #endif - if (++ptr >= window_size) + if (++ptr >= GOERTZEL_WINDOW_SIZE) ptr = 0; } @@ -218,9 +227,33 @@ static bool goertzel_power(const struct filter_config *c, uint8_t window_size, D power = UINT16_MAX; *out = (RMS_OUT_DATA)power; #else - int64_t power = ((int64_t)s1 * s1) + ((int64_t)s2 * s2) - (((int64_t)coeff * s1 * s2) >> COEFF_FRACTIONAL_BITS); - if (power < 0) - power = 0; + int64_t a = s1; + int64_t b = s2; + uint8_t downshift = 0; + const uint8_t limit_bits = COEFF_FRACTIONAL_BITS < 60 ? (uint8_t)((60 - COEFF_FRACTIONAL_BITS) / 2) : 0; + const int64_t max_mag = 1LL << limit_bits; + while ((abs64(a) > max_mag || abs64(b) > max_mag) && downshift < 24) { + a >>= 1; + b >>= 1; + downshift++; + } + + int64_t power_q = (((a * a) + (b * b)) << COEFF_FRACTIONAL_BITS) - ((int64_t)coeff * a * b); + if (power_q < 0) + power_q = 0; + + int64_t power = power_q >> COEFF_FRACTIONAL_BITS; + if (downshift) { + const uint8_t restore = (uint8_t)(downshift * 2); + for (uint8_t i = 0; i < restore; i++) { + if (power > (UINT16_MAX >> 1)) { + power = UINT16_MAX; + break; + } + power <<= 1; + } + } + if (power > UINT16_MAX) power = UINT16_MAX; *out = (RMS_OUT_DATA)power; @@ -242,13 +275,13 @@ bool CAT(pump_decoder,DECODE_BITS)( DECODE_DATA_TYPE *in = (DECODE_DATA_TYPE*)p; #if defined(USE_GOERTZEL) - if (! goertzel_window(audio->window_size, s, (int8_t)SHRINK(*in, int8_t))) + if (! goertzel_window(s, (int8_t)SHRINK(*in, int8_t))) return false; RMS_OUT_DATA ra = 0, rb = 0; if ( - ! goertzel_power(&coeffs[BIT_ZERO], audio->window_size, s, &ra) - || ! goertzel_power(&coeffs[BIT_ONE ], audio->window_size, s, &rb) + ! goertzel_power(&coeffs[BIT_ZERO], s, &ra) + || ! goertzel_power(&coeffs[BIT_ONE ], s, &rb) ) return false; #else diff --git a/src/types.h b/src/types.h index fae6c7b..6a11d90 100644 --- a/src/types.h +++ b/src/types.h @@ -54,8 +54,10 @@ enum { NUM_START_BITS = 1 }; #error "#define SAMPLE_RATE in Hz" #endif -static const unsigned int BAUD_RATE = 300; -static const unsigned int SAMPLES_PER_BIT = (SAMPLE_RATE + BAUD_RATE / 2) / BAUD_RATE; // round to nearest +enum { + BAUD_RATE = 300, + SAMPLES_PER_BIT = (SAMPLE_RATE + BAUD_RATE / 2) / BAUD_RATE, // round to nearest +}; #define FREQUENCY_LIST(_) \ _(CHAN_ZERO, BIT_ZERO, 1070) \ From 0911b3e41f5d35277ec7bddc4effb1d7ee13a9e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:38:29 +0000 Subject: [PATCH 3/3] Revert non-functional types.h style changes Co-authored-by: kulp <74288+kulp@users.noreply.github.com> --- src/decode-impl.h | 8 +++++--- src/decode.c | 19 +++++++++---------- src/types.h | 7 +++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/decode-impl.h b/src/decode-impl.h index 81b7edc..47fc50e 100644 --- a/src/decode-impl.h +++ b/src/decode-impl.h @@ -26,9 +26,11 @@ #include "coeff.h" #include "decode.h" +#include + #define THRESHOLD 0 #define MAX_RMS_SAMPLES 8 -#define GOERTZEL_WINDOW_SIZE SAMPLES_PER_BIT +#define GOERTZEL_WINDOW_SIZE ((SAMPLE_RATE + BAUD_RATE_VALUE / 2) / BAUD_RATE_VALUE) #define EXPAND(X,Y) (assert(sizeof(Y) >= sizeof(X)), (X) << (CHAR_BIT * (sizeof(Y) - sizeof(X)))) #define SHRINK(X,Y) (assert(sizeof(X) >= sizeof(Y)), (X) >> (CHAR_BIT * (sizeof(X) - sizeof(Y)))) @@ -69,8 +71,8 @@ struct filter_state { struct decode_state { #if defined(USE_GOERTZEL) int8_t signal_window[GOERTZEL_WINDOW_SIZE]; - uint16_t signal_ptr; - uint16_t signal_count; + size_t signal_ptr; + size_t signal_count; #else struct power_state power[2]; struct filter_state filt[2]; diff --git a/src/decode.c b/src/decode.c index 303417c..2dcc58f 100644 --- a/src/decode.c +++ b/src/decode.c @@ -25,6 +25,7 @@ #include #include +#include #if defined(__AVR__) #include @@ -34,9 +35,9 @@ #endif #if defined(USE_GOERTZEL) -static int64_t abs64(int64_t x) +static uint64_t mag64(int64_t x) { - return x < 0 ? -x : x; + return x < 0 ? (uint64_t)(-(x + 1)) + 1u : (uint64_t)x; } #endif @@ -201,8 +202,8 @@ static bool goertzel_power(const struct filter_config *c, DECODE_STATE *s, RMS_O int64_t s1 = 0; int64_t s2 = 0; #endif - uint16_t ptr = s->signal_ptr; - for (uint16_t i = 0; i < GOERTZEL_WINDOW_SIZE; i++) { + size_t ptr = s->signal_ptr; + for (size_t i = 0; i < GOERTZEL_WINDOW_SIZE; i++) { const int8_t datum = s->signal_window[ptr]; #if defined(USE_FLOATING_POINT) @@ -232,17 +233,15 @@ static bool goertzel_power(const struct filter_config *c, DECODE_STATE *s, RMS_O uint8_t downshift = 0; const uint8_t limit_bits = COEFF_FRACTIONAL_BITS < 60 ? (uint8_t)((60 - COEFF_FRACTIONAL_BITS) / 2) : 0; const int64_t max_mag = 1LL << limit_bits; - while ((abs64(a) > max_mag || abs64(b) > max_mag) && downshift < 24) { + while (mag64(a) > (uint64_t)max_mag || mag64(b) > (uint64_t)max_mag) { a >>= 1; b >>= 1; downshift++; } - int64_t power_q = (((a * a) + (b * b)) << COEFF_FRACTIONAL_BITS) - ((int64_t)coeff * a * b); - if (power_q < 0) - power_q = 0; - - int64_t power = power_q >> COEFF_FRACTIONAL_BITS; + int64_t power = (a * a) + (b * b) - (((int64_t)coeff * a * b) >> COEFF_FRACTIONAL_BITS); + if (power < 0) + power = 0; if (downshift) { const uint8_t restore = (uint8_t)(downshift * 2); for (uint8_t i = 0; i < restore; i++) { diff --git a/src/types.h b/src/types.h index 6a11d90..be10d7b 100644 --- a/src/types.h +++ b/src/types.h @@ -54,10 +54,9 @@ enum { NUM_START_BITS = 1 }; #error "#define SAMPLE_RATE in Hz" #endif -enum { - BAUD_RATE = 300, - SAMPLES_PER_BIT = (SAMPLE_RATE + BAUD_RATE / 2) / BAUD_RATE, // round to nearest -}; +#define BAUD_RATE_VALUE 300 +static const unsigned int BAUD_RATE = BAUD_RATE_VALUE; +static const unsigned int SAMPLES_PER_BIT = (SAMPLE_RATE + BAUD_RATE_VALUE / 2) / BAUD_RATE_VALUE; // round to nearest #define FREQUENCY_LIST(_) \ _(CHAN_ZERO, BIT_ZERO, 1070) \