Improve soft-clipper with validation, error handling, and overflow protection

- Add input validation: NULL checks, bounds checking (max 7680 samples)
- Change return type to int for error propagation
- Use saturating NEON arithmetic (vqaddq_s16, vqsubq_s16) to prevent overflow
- Fix type consistency: use int16_t instead of short throughout
- Update documentation: precise threshold (0.9375 or 15/16), describe 4:1 compression
- Remove redundant clamping operations (mathematically proven unnecessary)
- Add stdbool.h include for bool type support
- Handle soft-clip errors at call site to prevent encoding corrupted audio
This commit is contained in:
Alex P 2025-11-21 21:17:40 +02:00
parent 9e6ffb34e3
commit 2ef6cb2d4d
1 changed files with 26 additions and 23 deletions

View File

@ -21,6 +21,7 @@
#include <speex/speex_resampler.h> #include <speex/speex_resampler.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdbool.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
@ -208,45 +209,44 @@ static inline void simd_clear_samples_s16(short * __restrict__ buffer, uint32_t
/** /**
* Soft-clip audio samples to prevent digital clipping distortion * Soft-clip audio samples to prevent digital clipping distortion
* Uses smooth saturation curve for transients that exceed ±30720 (~0.94 of max) * Samples within ±30720 (0.9375 or 15/16 of max) pass through unchanged
* Samples exceeding threshold are compressed 4:1 (excess reduced by 75%)
* Processes 8 samples per iteration using ARM NEON * Processes 8 samples per iteration using ARM NEON
*/ */
static inline void simd_soft_clip_s16(short * __restrict__ buffer, uint32_t samples) { static inline int simd_soft_clip_s16(int16_t * __restrict__ buffer, uint32_t samples) {
const int16_t threshold = 30720; // 0.9375 * 32768 if (__builtin_expect(buffer == NULL || samples == 0, 0)) {
return 0;
}
if (__builtin_expect(samples > 7680, 0)) {
fprintf(stderr, "ERROR: simd_soft_clip_s16: sample count %u exceeds maximum\n", samples);
fflush(stderr);
return -1;
}
const int16_t threshold = 30720;
const int16x8_t thresh_pos = vdupq_n_s16(threshold); const int16x8_t thresh_pos = vdupq_n_s16(threshold);
const int16x8_t thresh_neg = vdupq_n_s16(-threshold); const int16x8_t thresh_neg = vdupq_n_s16(-threshold);
const int16x8_t max_val = vdupq_n_s16(32767);
const int16x8_t min_val = vdupq_n_s16(-32768);
uint32_t i = 0; uint32_t i = 0;
uint32_t simd_samples = samples & ~7U; uint32_t simd_samples = samples & ~7U;
for (; i < simd_samples; i += 8) { for (; i < simd_samples; i += 8) {
int16x8_t samples_vec = vld1q_s16(&buffer[i]); int16x8_t samples_vec = vld1q_s16(&buffer[i]);
// Detect samples exceeding positive threshold
uint16x8_t exceeds_pos = vcgtq_s16(samples_vec, thresh_pos); uint16x8_t exceeds_pos = vcgtq_s16(samples_vec, thresh_pos);
// Detect samples below negative threshold
uint16x8_t exceeds_neg = vcltq_s16(samples_vec, thresh_neg); uint16x8_t exceeds_neg = vcltq_s16(samples_vec, thresh_neg);
// Apply soft saturation to samples exceeding thresholds
// For positive: scale down to range [threshold, max_val]
// For negative: scale up to range [min_val, -threshold]
int16x8_t clipped = samples_vec; int16x8_t clipped = samples_vec;
clipped = vbslq_s16(exceeds_pos, clipped = vbslq_s16(exceeds_pos,
vaddq_s16(thresh_pos, vshrq_n_s16(vsubq_s16(samples_vec, thresh_pos), 2)), vqaddq_s16(thresh_pos, vshrq_n_s16(vqsubq_s16(samples_vec, thresh_pos), 2)),
clipped); clipped);
clipped = vbslq_s16(exceeds_neg, clipped = vbslq_s16(exceeds_neg,
vaddq_s16(thresh_neg, vshrq_n_s16(vsubq_s16(samples_vec, thresh_neg), 2)), vqaddq_s16(thresh_neg, vshrq_n_s16(vqsubq_s16(samples_vec, thresh_neg), 2)),
clipped); clipped);
// Clamp to int16 range
clipped = vminq_s16(vmaxq_s16(clipped, min_val), max_val);
vst1q_s16(&buffer[i], clipped); vst1q_s16(&buffer[i], clipped);
} }
// Scalar: remaining samples
for (; i < samples; i++) { for (; i < samples; i++) {
int32_t sample = buffer[i]; int32_t sample = buffer[i];
if (sample > threshold) { if (sample > threshold) {
@ -254,11 +254,10 @@ static inline void simd_soft_clip_s16(short * __restrict__ buffer, uint32_t samp
} else if (sample < -threshold) { } else if (sample < -threshold) {
sample = -threshold + ((sample + threshold) >> 2); sample = -threshold + ((sample + threshold) >> 2);
} }
// Clamp to int16 range buffer[i] = (int16_t)sample;
if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
buffer[i] = (short)sample;
} }
return 0;
} }
// INITIALIZATION STATE TRACKING // INITIALIZATION STATE TRACKING
@ -810,8 +809,12 @@ retry_read:
return -1; return -1;
} }
// Apply soft-clipping to prevent digital clipping distortion on sharp transients if (simd_soft_clip_s16(pcm_to_encode, opus_frame_size * capture_channels) < 0) {
simd_soft_clip_s16(pcm_to_encode, opus_frame_size * capture_channels); fprintf(stderr, "ERROR: capture: Soft-clipping failed\n");
fflush(stderr);
pthread_mutex_unlock(&capture_mutex);
return -1;
}
nb_bytes = opus_encode(enc, pcm_to_encode, opus_frame_size, out, max_packet_size); nb_bytes = opus_encode(enc, pcm_to_encode, opus_frame_size, out, max_packet_size);