Skip to content

Commit 69e9d05

Browse files
committed
audio: tflm: soft mel-log AGC for level-robust wake-word detection
Add a soft AGC in the Q9.23 mel-log domain that automatically clamps peak energy to prevent clipping on loud utterances, while allowing recovery toward a 0 dB target at 0.5 dB/sec during active speech. Requantization is mapped symmetrically from [-1.0, +1.0] in Q9.23 to [-128, 127] in int8 to match model calibration. In addition, track a 49-hop VAD history bitmask from the MFCC header. When the entire sliding context window contains only silence (VAD=0), int8 requantization and neural network inference execution are gated off, substantially reducing DSP MCPS load during idle periods. Signed-off-by: Seppo Ingalsuo <seppo.ingalsuo@linux.intel.com>
1 parent afcbc89 commit 69e9d05

3 files changed

Lines changed: 95 additions & 136 deletions

File tree

src/audio/tensorflow/speech.cc

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
// The following values are derived from values used during model training.
2020
// If you change the way you preprocess the input, update all these constants.
21-
//constexpr int kAudioSampleFrequency = TFLM_SAMPLE_RATE;
2221
static constexpr int kFeatureSize = TFLM_FEATURE_SIZE;
2322
static constexpr int kFeatureCount = TFLM_FEATURE_COUNT;
2423
static constexpr int kFeatureElementCount = TFLM_FEATURE_ELEM_COUNT;
@@ -56,32 +55,6 @@ int RegisterOps(MicroSpeechOpResolver *op_resolver) {
5655

5756
static int Init_Interpreter(struct tf_classify *tfc);
5857

59-
// Decompose the Q9.23 -> int8 requantize factor
60-
// M = 1 / (input_scale * 2^23)
61-
// into a normalized int32 multiplier in [2^30, 2^31) and a right-shift, so the
62-
// runtime hot path can do (mel_q23 * mult) >> shift in pure integer math.
63-
// Unpacks IEEE 754 bits directly rather than calling frexpf so that this file
64-
// pulls in no libm symbols on the minimal-libc SOF build.
65-
static void ComputeInputRequantizeMultiplier(float input_scale,
66-
int32_t *out_mult, int *out_shift)
67-
{
68-
float m = 1.0f / (input_scale * static_cast<float>(1 << 23));
69-
if (!(m > 0.0f)) {
70-
*out_mult = 0;
71-
*out_shift = 0;
72-
return;
73-
}
74-
uint32_t bits;
75-
std::memcpy(&bits, &m, sizeof(bits));
76-
// IEEE 754 binary32: bias-127 exponent; frexp uses [0.5, 1.0) => bias-126.
77-
int exp = static_cast<int>((bits >> 23) & 0xffu) - 126;
78-
// Reinsert the implicit leading 1, then shift the 24-bit mantissa into bit
79-
// 30 so the result is Q0.31 with MSB set, i.e. in [2^30, 2^31).
80-
int32_t mult = static_cast<int32_t>(((bits & 0x7fffffu) | 0x800000u) << 7);
81-
*out_mult = mult;
82-
*out_shift = 31 - exp;
83-
}
84-
8558
int TF_InitOps(struct tf_classify *tfc)
8659
{
8760
op_resolver = new MicroSpeechOpResolver();
@@ -160,14 +133,6 @@ static int Init_Interpreter(struct tf_classify *tfc)
160133
return -EINVAL;
161134
}
162135

163-
// expose the model's real input quantization params so callers can
164-
// requantize their features correctly instead of assuming a fixed
165-
// scale/zero_point.
166-
tfc->input_scale = input->params.scale;
167-
tfc->input_zero_point = input->params.zero_point;
168-
ComputeInputRequantizeMultiplier(tfc->input_scale,
169-
&tfc->input_mult, &tfc->input_shift);
170-
171136
return 0;
172137
}
173138

src/audio/tensorflow/speech.h

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,6 @@ struct tf_classify {
3131
int op_count;
3232
uint32_t node_cycles[10];
3333
int node_codes[10];
34-
float input_scale;
35-
int input_zero_point;
36-
/* Precomputed integer requantizer for the Q9.23 mel-log -> int8
37-
* feature path: pre_zp = round((int64)mel_q23 * input_mult >> input_shift).
38-
* The factor of 2^-23 from Q9.23 is folded into input_mult so runtime
39-
* does no float math. See Init_Interpreter() in speech.cc.
40-
*/
41-
int32_t input_mult;
42-
int input_shift;
4334
};
4435

4536
/* Export of C++ APIs into C namespace for linkage */

src/audio/tensorflow/tflm-classify.c

Lines changed: 95 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,29 @@ static const char * const prediction[] = TFLM_CATEGORY_DATA;
109109
#define TFLM_MFCC_HOP_MS 20
110110
#define TFLM_INFERENCE_STRIDE_HOPS 25
111111

112+
/* Soft mel-log AGC (units: Q9.23, matches MFCC output). One decade = +10 dB.
113+
* Target 0 dB, floor -20 dB. Attack: instant clamp so peak+gain never exceeds
114+
* MEL_CLIP_MAX_Q23 (+1.0, +10 dB). Release: additive step per hop during active
115+
* speech (vad_flag == 1), chosen so recovery is ~0.5 dB/sec (hop=20 ms, 50 hops/sec).
116+
*/
117+
#define TFLM_AGC_GAIN_TARGET_Q23 0 /* 0 dB */
118+
#define TFLM_AGC_GAIN_FLOOR_Q23 -16777216 /* -20 dB, int32(-20 * 0.1 * 2^23) */
119+
#define TFLM_AGC_RELEASE_STEP_Q23 8389 /* 0.5 dB/s, int32((0.5 * 0.1 / 50) * 2^23) */
120+
121+
/* This needs to match with sof_tflm_train.py and sof_tflm_verify.py.
122+
*
123+
* The range -1.0 to +1.0 of Q9.23 Mel values is scaled to +/-1.0 Q1.7.
124+
*
125+
* in = [-1.0 1.0]; out = [-1 1];
126+
* offset = -(in(1) + in(2)) / 2 = 0
127+
* scale = (out(2) - out(1)) / (in(2) - in(1)) = 1.0
128+
*/
129+
#define MEL_OFFSET_Q23 0 /* 0 */
130+
#define MEL_SCALE_Q30 (1 << 30) /* 1.0 in Q30 */
131+
#define MEL_CLIP_MAX_Q23 (1 << 23) /* +1.0 in Q23 */
132+
#define MEL_CLIP_MAX_Q7 127
133+
#define MEL_CLIP_MIN_Q7 -128
134+
112135
struct tflm_comp_data {
113136
struct comp_data_blob_handler *model_handler;
114137
struct tf_classify tfc;
@@ -122,6 +145,10 @@ struct tflm_comp_data {
122145
/* Per-instance sliding window and inference cadence. */
123146
int8_t feature_buf[TFLM_FEATURE_ELEM_COUNT];
124147
int frame_counter;
148+
/* Bitmask of VAD flags for the TFLM_FEATURE_COUNT frames in feature_buf. */
149+
uint64_t vad_history;
150+
/* Persistent AGC gain applied to every Q9.23 mel value (see AGC defines). */
151+
int32_t agc_gain_q23;
125152
/* Per-instance shutdown-summary counters. */
126153
uint32_t category_totals[TFLM_CATEGORY_COUNT];
127154
uint32_t total_inferences;
@@ -190,15 +217,11 @@ static __maybe_unused void tflm_send_keyword_notification(struct processing_modu
190217

191218
/* Shared TFLM backend state. speech.cc has one static arena/model/interpreter,
192219
* so TF_SetModel/TF_InitOps must run only once no matter how many tflmcly
193-
* instances the topology creates; secondary instances copy the resolved input
194-
* quant params from the first-init cache below.
220+
* instances the topology creates; secondary instances attach to the first-init
221+
* instance.
195222
*/
196223
static bool g_tflm_initialized;
197224
static int g_tflm_instance_count;
198-
static float g_tflm_shared_input_scale;
199-
static int g_tflm_shared_input_zero_point;
200-
static int32_t g_tflm_shared_input_mult;
201-
static int32_t g_tflm_shared_input_shift;
202225
static int g_tflm_shared_categories;
203226

204227
__cold static void tflm_log_summary_at_shutdown(struct processing_module *mod)
@@ -251,6 +274,7 @@ __cold static int tflm_init(struct processing_module *mod)
251274
md->private = cd;
252275
cd->tfc.categories = TFLM_CATEGORY_COUNT;
253276
cd->drain_req_ms = TFLM_KPB_DRAIN_REQ_MS;
277+
cd->agc_gain_q23 = TFLM_AGC_GAIN_TARGET_Q23;
254278
#if CONFIG_AMS
255279
cd->kpd_uuid_id = AMS_INVALID_MSG_TYPE;
256280
#endif
@@ -283,39 +307,6 @@ __cold static int tflm_set_config(struct processing_module *mod, uint32_t param_
283307
return 0;
284308
}
285309

286-
/*
287-
* MFCC's mel-log output is Q9.23 fixed point, normalized by the mel40.conf
288-
* profile's mel_offset/mel_scale/top_db tuning to approximately the 0..1
289-
* range (see the dynamic_mmax clamp + offset + scale in mfcc_common.c).
290-
* We requantize that into int8 features using the model's own input tensor
291-
* scale/zero_point (populated by TF_InitOps()/Init_Interpreter() in speech.cc).
292-
*
293-
* The float-domain operation is
294-
* int8_pre = round((mel_q23 / 2^23) / input_scale) + input_zero_point
295-
* which we execute in integer math using a pre-decomposed multiplier and
296-
* right-shift so this per-sample hot path (40 features x ~50 hops/sec)
297-
* needs no float divides or FPU support:
298-
* int8_pre = round((int64)mel_q23 * input_mult >> input_shift) + input_zero_point
299-
* with input_mult/input_shift set once at model prepare time.
300-
*/
301-
static inline int8_t mfcc_mel_q23_to_int8(int32_t mel_q23, int32_t input_mult,
302-
int input_shift, int input_zero_point)
303-
{
304-
int64_t prod = (int64_t)mel_q23 * (int64_t)input_mult;
305-
int64_t abs_prod = prod >= 0 ? prod : -prod;
306-
int64_t rounding = (int64_t)1 << (input_shift - 1);
307-
int32_t abs_scaled = (int32_t)((abs_prod + rounding) >> input_shift);
308-
int32_t scaled = prod >= 0 ? abs_scaled : -abs_scaled;
309-
310-
scaled += input_zero_point;
311-
if (scaled > 127)
312-
scaled = 127;
313-
else if (scaled < -128)
314-
scaled = -128;
315-
316-
return (int8_t)scaled;
317-
}
318-
319310
/*
320311
* This expects features from 16kHz mono 16 bit input stream.
321312
*
@@ -332,9 +323,6 @@ static inline int8_t mfcc_mel_q23_to_int8(int32_t mel_q23, int32_t input_mult,
332323
* features in the input buffer.
333324
*/
334325

335-
336-
337-
338326
static int tflm_process(struct processing_module *mod,
339327
struct sof_source **sources, int num_of_sources,
340328
struct sof_sink **sinks, int num_of_sinks)
@@ -396,45 +384,61 @@ static int tflm_process(struct processing_module *mod,
396384
* TFLM_FEATURE_SIZE int32 Q9.23 mel-log values into
397385
* int8 features for this hop.
398386
*/
387+
const struct mfcc_data_header *hdr =
388+
(const struct mfcc_data_header *)hop_scratch;
399389
const int32_t *mel = (const int32_t *)
400390
(hop_scratch + sizeof(struct mfcc_data_header));
401391
int8_t hop_features[TFLM_FEATURE_SIZE];
402392

403-
/* Match training preprocessing exactly: clip Q9.23 mel to
404-
* [-1.0, +4.0] and then rescale to [-1.0, +1.0] centered
405-
* on 0 (np.clip + (X - 1.5) / 2.5 in train_wov_tflm.py).
406-
* TFLite calibration on that range picks
407-
* input_scale=1/128, input_zero_point=0, so the model
408-
* consumes the full int8 dynamic range.
409-
*/
410-
enum {
411-
MEL_CLIP_MIN_Q23 = -(1 << 23), /* -1.0 */
412-
MEL_CLIP_MAX_Q23 = (4 << 23), /* +4.0 */
413-
MEL_CENTER_Q23 = (3 << 22), /* +1.5 */
414-
TWO_FIFTHS_Q31 = 858993459, /* round(0.4 * (1<<31)) */
415-
};
416-
417-
for (int i = 0; i < TFLM_FEATURE_SIZE; i++) {
418-
int32_t mel_c = mel[i];
419-
420-
if (mel_c < MEL_CLIP_MIN_Q23)
421-
mel_c = MEL_CLIP_MIN_Q23;
422-
else if (mel_c > MEL_CLIP_MAX_Q23)
423-
mel_c = MEL_CLIP_MAX_Q23;
424-
425-
/* Fold (mel - 1.5) * (2/5) into one Q1.31 multiply. */
426-
mel_c = Q_MULTSR_32X32((int64_t)(mel_c - MEL_CENTER_Q23),
427-
TWO_FIFTHS_Q31, 23, 31, 23);
428-
429-
hop_features[i] = mfcc_mel_q23_to_int8(mel_c,
430-
cd->tfc.input_mult, cd->tfc.input_shift,
431-
cd->tfc.input_zero_point);
393+
/* Update VAD history bitmask (tracks last TFLM_FEATURE_COUNT hops). */
394+
cd->vad_history = ((cd->vad_history << 1) | (hdr->vad_flag ? 1ULL : 0ULL)) &
395+
((1ULL << TFLM_FEATURE_COUNT) - 1);
396+
397+
/* AGC: attack on this hop's peak (always track energy). */
398+
int32_t hop_peak_q23 = mel[0];
399+
for (int i = 1; i < TFLM_FEATURE_SIZE; i++)
400+
if (mel[i] > hop_peak_q23)
401+
hop_peak_q23 = mel[i];
402+
403+
int32_t clip_headroom_q23 = MEL_CLIP_MAX_Q23 - hop_peak_q23;
404+
if (cd->agc_gain_q23 > clip_headroom_q23)
405+
cd->agc_gain_q23 = clip_headroom_q23;
406+
if (cd->agc_gain_q23 < TFLM_AGC_GAIN_FLOOR_Q23)
407+
cd->agc_gain_q23 = TFLM_AGC_GAIN_FLOOR_Q23;
408+
409+
int32_t agc_gain_q23 = cd->agc_gain_q23;
410+
411+
comp_info(mod->dev, "tflm agc: peak_q23=%d gain_q23=%d",
412+
hop_peak_q23, agc_gain_q23);
413+
414+
/* Requantize to int8: skip if no speech in the 49-hop window (fill with silence) */
415+
if (cd->vad_history) {
416+
for (int i = 0; i < TFLM_FEATURE_SIZE; i++) {
417+
int32_t mel_c = mel[i] + agc_gain_q23;
418+
419+
/* Rescale asymmetrically with offset and gain. */
420+
mel_c = Q_MULTSR_32X32((int64_t)(mel_c + MEL_OFFSET_Q23),
421+
MEL_SCALE_Q30, 23, 30, 7);
422+
if (mel_c > MEL_CLIP_MAX_Q7)
423+
mel_c = MEL_CLIP_MAX_Q7;
424+
else if (mel_c < MEL_CLIP_MIN_Q7)
425+
mel_c = MEL_CLIP_MIN_Q7;
426+
427+
hop_features[i] = (int8_t)mel_c;
428+
}
429+
} else {
430+
memset(hop_features, MEL_CLIP_MIN_Q7, sizeof(hop_features));
431+
}
432+
433+
/* Release: creep back toward 0 dB target. */
434+
if (cd->agc_gain_q23 < TFLM_AGC_GAIN_TARGET_Q23) {
435+
cd->agc_gain_q23 += TFLM_AGC_RELEASE_STEP_Q23;
436+
if (cd->agc_gain_q23 > TFLM_AGC_GAIN_TARGET_Q23)
437+
cd->agc_gain_q23 = TFLM_AGC_GAIN_TARGET_Q23;
432438
}
433439

434440
#if CONFIG_COMP_TENSORFLOW_DEBUG_TRACE
435441
{
436-
const struct mfcc_data_header *hdr =
437-
(const struct mfcc_data_header *)hop_scratch;
438442
static int dbg_hop_count;
439443
int32_t mel_min = mel[0], mel_max = mel[0];
440444
int8_t f_min = hop_features[0], f_max = hop_features[0];
@@ -448,11 +452,12 @@ static int tflm_process(struct processing_module *mod,
448452
if (hop_features[i] > f_max) f_max = hop_features[i];
449453
}
450454
snprintk(dbg_buf, sizeof(dbg_buf),
451-
"[DBG hop %d] vad=%d E=%d Ne=%d mel_min=%d mel_max=%d f_min=%d f_max=%d",
455+
"[DBG hop %d] vad=%d E=%d Ne=%d mel_min=%d mel_max=%d f_min=%d f_max=%d agc_q23=%d",
452456
dbg_hop_count, (int)hdr->vad_flag,
453457
(int)hdr->energy, (int)hdr->noise_energy,
454458
mel_min, mel_max,
455-
f_min, f_max);
459+
f_min, f_max,
460+
(int)cd->agc_gain_q23);
456461
sof_ut_log(dbg_buf);
457462
}
458463
#endif
@@ -506,6 +511,14 @@ static int tflm_process(struct processing_module *mod,
506511
if (cd->frame_counter >= TFLM_INFERENCE_STRIDE_HOPS) {
507512
cd->frame_counter = 0;
508513

514+
/* VAD gate: skip inference computation if no active speech in the 49-hop window */
515+
if (!cd->vad_history) {
516+
#if CONFIG_COMP_TENSORFLOW_DEBUG_TRACE
517+
sof_ut_log("[DBG inference] skipped: VAD=0 in entire 49-hop window");
518+
#endif
519+
continue;
520+
}
521+
509522
cd->tfc.audio_features = feature_buf;
510523
cd->tfc.audio_data_size = TFLM_FEATURE_ELEM_COUNT;
511524

@@ -605,8 +618,9 @@ static int tflm_process(struct processing_module *mod,
605618
}
606619
#endif
607620

608-
// Only announce a keyword hit for real keyword classes
609-
// (indices >= 2, i.e. not silence/unknown).
621+
/* Only announce a keyword hit for real keyword classes
622+
* (indices >= 2, i.e. not silence/unknown).
623+
*/
610624
if (max_idx >= 2 && max_score >= 0.50f) {
611625
char kw_buf[96];
612626
int max_pct = (int)(max_score * 100.0f);
@@ -651,14 +665,7 @@ static int tflm_prepare(struct processing_module *mod,
651665
printk("[TFLM PREPARE] tflm_prepare called, loading model...\n");
652666

653667
if (g_tflm_initialized) {
654-
/* Shared TFLM engine already up. Copy the resolved input quant
655-
* params so this instance's requantize path uses the same
656-
* scale/zero_point/mult/shift as the first-initialized instance.
657-
*/
658-
cd->tfc.input_scale = g_tflm_shared_input_scale;
659-
cd->tfc.input_zero_point = g_tflm_shared_input_zero_point;
660-
cd->tfc.input_mult = g_tflm_shared_input_mult;
661-
cd->tfc.input_shift = g_tflm_shared_input_shift;
668+
/* Shared TFLM engine already up. */
662669
cd->tfc.categories = g_tflm_shared_categories;
663670
printk("[TFLM PREPARE] shared engine already initialized; attach instance\n");
664671
goto post_init;
@@ -677,17 +684,11 @@ static int tflm_prepare(struct processing_module *mod,
677684
return ret;
678685
}
679686

680-
g_tflm_shared_input_scale = cd->tfc.input_scale;
681-
g_tflm_shared_input_zero_point = cd->tfc.input_zero_point;
682-
g_tflm_shared_input_mult = cd->tfc.input_mult;
683-
g_tflm_shared_input_shift = cd->tfc.input_shift;
684687
g_tflm_shared_categories = cd->tfc.categories;
685688
g_tflm_initialized = true;
686689
printk("[TFLM PREPARE] TFLM model & ops initialized successfully!\n");
687690
printk("[TFLM PREPARE] arena_used=%zu / capacity=%zu bytes\n",
688691
TF_ArenaUsedBytes(), TF_ArenaCapacity());
689-
printk("[DBG quant] input_scale_x1e6=%d input_zero_point=%d\n",
690-
(int)(cd->tfc.input_scale * 1000000.0f), cd->tfc.input_zero_point);
691692

692693
post_init:
693694
#if CONFIG_AMS
@@ -711,6 +712,9 @@ static int tflm_reset(struct processing_module *mod)
711712
struct tflm_comp_data *cd = module_get_private_data(mod);
712713

713714
tflm_log_summary_at_shutdown(mod);
715+
cd->vad_history = 0;
716+
cd->frame_counter = 0;
717+
memset(cd->feature_buf, 0, sizeof(cd->feature_buf));
714718
#if CONFIG_AMS
715719
if (cd->kpd_uuid_id != AMS_INVALID_MSG_TYPE) {
716720
int ret = ams_helper_unregister_producer(mod->dev,
@@ -730,7 +734,6 @@ static const struct module_interface tflmcly_interface = {
730734
.prepare = tflm_prepare,
731735
.process = tflm_process,
732736
.set_configuration = tflm_set_config,
733-
// .get_configuration = tflm_get_config,
734737
.reset = tflm_reset,
735738
.free = tflm_free
736739
};

0 commit comments

Comments
 (0)