| 162 | } |
| 163 | |
| 164 | void Reactive::processSample(const Sample& sample) { |
| 165 | if (!sample.isValid()) { |
| 166 | return; // Invalid sample, ignore |
| 167 | } |
| 168 | |
| 169 | // Extract timestamp from the Sample |
| 170 | fl::u32 currentTimeMs = sample.timestamp(); |
| 171 | |
| 172 | // Phase 1: Signal conditioning pipeline |
| 173 | Sample processedSample = sample; |
| 174 | |
| 175 | // Step 1: Signal conditioning (DC removal, spike filtering, noise gate) |
| 176 | if (mConfig.enableSignalConditioning) { |
| 177 | processedSample = mSignalConditioner.processSample(processedSample); |
| 178 | if (!processedSample.isValid()) { |
| 179 | return; // Signal was completely filtered out |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | // Step 2: Noise floor tracking (update tracker, but don't modify signal) |
| 184 | if (mConfig.enableNoiseFloorTracking) { |
| 185 | float rms = processedSample.rms(); |
| 186 | mNoiseFloorTracker.update(rms); |
| 187 | } |
| 188 | |
| 189 | // Set conditioned sample on shared Context (clears per-frame FFT cache) |
| 190 | mContext->setSample(processedSample); |
| 191 | |
| 192 | // Populate silence flag after setSample() resets it — detectors run via |
| 193 | // updateFromContext() below and will read context->isSilent(). |
| 194 | // |
| 195 | // Uses an absolute RMS threshold (not the adaptive noise floor). The |
| 196 | // adaptive floor's first-sample init matches the current level, which |
| 197 | // causes isAboveFloor() to be stuck at false for any steady signal — |
| 198 | // wrongly flagging loud constant tones as silent. Absolute RMS is the |
| 199 | // right primitive for "no signal present": a steady loud tone has |
| 200 | // large RMS and is not silent regardless of noise-floor adaptation. |
| 201 | if (mConfig.enableNoiseFloorTracking) { |
| 202 | constexpr float kSilenceRmsThreshold = 10.0f; |
| 203 | mContext->setSilent(processedSample.rms() < kSilenceRmsThreshold); |
| 204 | } |
| 205 | |
| 206 | // Process the conditioned Sample - timing is gated by sample availability |
| 207 | processFFT(processedSample); |
| 208 | |
| 209 | // Update internal Processor BEFORE populating Data fields, |
| 210 | // so updateVolumeAndPeak() can source normalized volume. |
| 211 | ensureAudioProcessor(); |
| 212 | mAudioProcessor->updateFromContext(mContext); |
| 213 | |
| 214 | updateVolumeAndPeak(processedSample); |
| 215 | |
| 216 | // Enhanced processing pipeline |
| 217 | applySpectralEqualization(); |
| 218 | calculateBandEnergies(); |
| 219 | |
| 220 | // Apply pink noise compensation AFTER band energy calculation |
| 221 | // so that bassEnergy/midEnergy/trebleEnergy reflect actual spectral content |
no test coverage detected