diff --git a/include/app_config.h b/include/app_config.h index 2648d13..23e73f2 100644 --- a/include/app_config.h +++ b/include/app_config.h @@ -22,7 +22,9 @@ extern "C" { #define AGC_DECAY_RATE 0.9997f #define AGC_MAX_GAIN 30.0f #define AGC_NOISE_FLOOR 200.0f -#define AGC_INITIAL_PEAK 1000.0f +#define AGC_INITIAL_PEAK 28000.0f // Soft-start: initial gain = 1.0x (0dB) to avoid 28x burst +#define AUDIO_LEAD_MUTE_SAMPLES 2400 // 150ms silence zone (2400 samples @ 16kHz) to eliminate button click & pops +#define AUDIO_FADE_IN_SAMPLES 160 // 10ms micro fade-in (160 samples @ 16kHz) - only for transient smoothing at boundary #define DECLIP_THRESHOLD 1000 // ========================================== diff --git a/src/audio/audio_agc.c b/src/audio/audio_agc.c index 64c4ecb..879f096 100644 --- a/src/audio/audio_agc.c +++ b/src/audio/audio_agc.c @@ -3,16 +3,16 @@ void audio_agc_init(audio_agc_t *agc) { if (!agc) return; - agc->peak = 1000.0f; agc->target_level = 28000.0f; agc->decay_rate = 0.9997f; agc->max_gain = 30.0f; agc->noise_floor = 200.0f; + agc->peak = agc->target_level; // Soft-start: start at 1.0x gain (0dB) to prevent burst noise } void audio_agc_reset(audio_agc_t *agc) { if (!agc) return; - agc->peak = 1000.0f; + agc->peak = agc->target_level; // Soft-start: reset to 1.0x gain (0dB) } void audio_agc_process(audio_agc_t *agc, int16_t *samples, size_t count) { diff --git a/src/audio/audio_pipeline.c b/src/audio/audio_pipeline.c index 5c71a1b..47a616a 100644 --- a/src/audio/audio_pipeline.c +++ b/src/audio/audio_pipeline.c @@ -27,7 +27,9 @@ void audio_pipeline_start_session(audio_pipeline_t *pipeline, uint8_t session_id audio_ring_buffer_clear(&pipeline->ring_buf); pipeline->session_id = session_id; pipeline->active = true; - pipeline->buffering = true; // Wait for initial 30ms prefill cushion to prevent jitter underruns + pipeline->buffering = true; // Wait for initial prefill cushion to prevent jitter underruns + pipeline->lead_mute_remaining = AUDIO_LEAD_MUTE_SAMPLES; + pipeline->fade_in_remaining = AUDIO_FADE_IN_SAMPLES; pipeline->total_frames_decoded = 0; pipeline->total_samples_pushed = 0; pipeline->underrun_count = 0; @@ -53,10 +55,33 @@ size_t audio_pipeline_feed_adpcm(audio_pipeline_t *pipeline, const uint8_t *adpc // 3. DC-Blocker (strips ~0-80Hz sub-bass and AC hum) audio_filter_dc_block(&pipeline->filter, pipeline->temp_pcm, samples_decoded); - // 4. Dynamic AGC + Soft Clip (target 28000, decay 0.9997, max_gain 30) + // 4. Dynamic AGC + Soft Clip + // KEY: During the lead mute period, zero the samples BEFORE AGC so the peak envelope + // sees silence and does NOT decay. This keeps gain locked at 1.0x (soft-start). + // When mute ends, the AGC is still at 1.0x and can only rise from real audio -> no burst. + for (size_t i = 0; i < samples_decoded; i++) { + if (pipeline->lead_mute_remaining > 0) { + // Feed zeros to AGC so it doesn't decay the peak during silence + pipeline->temp_pcm[i] = 0; + pipeline->lead_mute_remaining--; + } + // fade_in samples pass through AGC normally (they're real audio, just new) + } audio_agc_process(&pipeline->agc, pipeline->temp_pcm, samples_decoded); - // 5. Enqueue into ring buffer + // 5. Post-Processing: Micro Fade-In (10ms) applied AFTER AGC + // The mute already zeroed the first 150ms. This 10ms fade-in smooths the + // hard boundary transition. It is too short (160 samples) to create audible + // noise artifacts, but long enough to eliminate any hard step discontinuity. + for (size_t i = 0; i < samples_decoded; i++) { + if (pipeline->fade_in_remaining > 0) { + uint32_t step = AUDIO_FADE_IN_SAMPLES - pipeline->fade_in_remaining; + pipeline->temp_pcm[i] = (int16_t)(((int32_t)pipeline->temp_pcm[i] * (int32_t)step) / AUDIO_FADE_IN_SAMPLES); + pipeline->fade_in_remaining--; + } + } + + // 6. Enqueue into ring buffer size_t written = audio_ring_buffer_write(&pipeline->ring_buf, pipeline->temp_pcm, samples_decoded); pipeline->total_frames_decoded++; @@ -109,5 +134,7 @@ void audio_pipeline_stop_session(audio_pipeline_t *pipeline) { if (!pipeline) return; pipeline->active = false; pipeline->buffering = false; + pipeline->lead_mute_remaining = 0; + pipeline->fade_in_remaining = 0; audio_ring_buffer_clear(&pipeline->ring_buf); } diff --git a/src/audio/audio_pipeline.h b/src/audio/audio_pipeline.h index 4c91bd7..7342cb1 100644 --- a/src/audio/audio_pipeline.h +++ b/src/audio/audio_pipeline.h @@ -28,6 +28,8 @@ typedef struct { uint32_t total_frames_decoded; uint32_t total_samples_pushed; uint32_t underrun_count; + uint32_t lead_mute_remaining; + uint32_t fade_in_remaining; } audio_pipeline_t; /**