| 24 | Vocal::~Vocal() FL_NOEXCEPT = default; |
| 25 | |
| 26 | void Vocal::update(shared_ptr<Context> context) { |
| 27 | mSampleRate = context->getSampleRate(); |
| 28 | // Dual FFT: high-resolution formant analysis + broad spectral features. |
| 29 | // Formant FFT: 64 CQ bins in 200-3500 Hz — concentrates resolution on |
| 30 | // F1/F2/F3 formants (94% bin utilization vs 73% with full-range). |
| 31 | // Broad FFT: 16 LOG_REBIN bins in 174.6-4698.3 Hz — full spectral |
| 32 | // coverage for flatness, density, flux, and vocal presence ratio. |
| 33 | mRetainedFormantFFT = context->getFFT(64, 200.0f, 3500.0f); |
| 34 | mRetainedBroadFFT = context->getFFT(16, 174.6f, 4698.3f); |
| 35 | const fft::Bins& formantFft = *mRetainedFormantFFT; |
| 36 | const fft::Bins& broadFft = *mRetainedBroadFFT; |
| 37 | mFormantNumBins = static_cast<int>(formantFft.raw().size()); |
| 38 | mBroadNumBins = static_cast<int>(broadFft.raw().size()); |
| 39 | |
| 40 | // Formant ratio from high-res narrow FFT |
| 41 | computeFormantRatio(formantFft); |
| 42 | // Broad spectral features (flatness, density, flux) from wide FFT |
| 43 | computeBroadSpectralFeatures(broadFft); |
| 44 | |
| 45 | // Vocal presence ratio from broad FFT linear bins (needs 200-4000 Hz coverage) |
| 46 | mVocalPresenceRatio = calculateVocalPresenceRatio(broadFft); |
| 47 | mSpectralVariance = mSpectralVarianceFilter.update(broadFft.raw()); |
| 48 | |
| 49 | // Calculate time-domain features from raw PCM |
| 50 | span<const i16> pcm = context->getPCM(); |
| 51 | const float dt = computeAudioDt(pcm.size(), context->getSampleRate()); |
| 52 | // Fused pass: envelope jitter + zero-crossing CV in one PCM traversal |
| 53 | computePCMTimeDomainFeatures(pcm); |
| 54 | mEnvelopeJitter = mEnvelopeJitterSmoother.update(mEnvelopeJitter, dt); |
| 55 | mAutocorrelationIrregularity = mAcfIrregularitySmoother.update( |
| 56 | calculateAutocorrelationIrregularity(pcm), dt); |
| 57 | mZeroCrossingCV = mZcCVSmoother.update(mZeroCrossingCV, dt); |
| 58 | |
| 59 | // Calculate raw confidence and apply time-aware smoothing |
| 60 | float rawConfidence = calculateRawConfidence( |
| 61 | mFormantRatio, mSpectralFlatness, mHarmonicDensity, |
| 62 | mVocalPresenceRatio, mSpectralFlux, mSpectralVariance); |
| 63 | float smoothedConfidence = mConfidenceSmoother.update(rawConfidence, dt); |
| 64 | |
| 65 | // Hysteresis: use separate on/off thresholds to prevent chattering |
| 66 | bool wantActive; |
| 67 | if (mVocalActive) { |
| 68 | wantActive = (smoothedConfidence >= mOffThreshold); |
| 69 | } else { |
| 70 | wantActive = (smoothedConfidence >= mOnThreshold); |
| 71 | } |
| 72 | |
| 73 | // Debounce: require state to persist for MIN_FRAMES_TO_TRANSITION frames |
| 74 | if (wantActive != mVocalActive) { |
| 75 | mFramesInState++; |
| 76 | if (mFramesInState >= MIN_FRAMES_TO_TRANSITION) { |
| 77 | mVocalActive = wantActive; |
| 78 | mFramesInState = 0; |
| 79 | } |
| 80 | } else { |
| 81 | mFramesInState = 0; |
| 82 | } |
| 83 |
nothing calls this directly
no test coverage detected