| 291 | } |
| 292 | |
| 293 | void Reactive::mapFFTBinsToFrequencyChannels() { |
| 294 | // Sample::fft() returns CQ-kernel bins that are already |
| 295 | // frequency-mapped (linearly spaced from fmin to fmax). Copy them |
| 296 | // directly instead of re-mapping through FrequencyBinMapper, which |
| 297 | // incorrectly treats CQ bins as raw DFT bins. |
| 298 | fl::span<const float> rawBins = mFFTBins.raw(); |
| 299 | if (rawBins.empty()) { |
| 300 | for (int i = 0; i < 16; ++i) { |
| 301 | mCurrentData.frequencyBins[i] = 0.0f; |
| 302 | } |
| 303 | return; |
| 304 | } |
| 305 | |
| 306 | // Copy CQ bins directly to frequency bins (already frequency-mapped) |
| 307 | for (int i = 0; i < 16; ++i) { |
| 308 | if (i < static_cast<int>(rawBins.size())) { |
| 309 | mCurrentData.frequencyBins[i] = rawBins[i]; |
| 310 | } else { |
| 311 | mCurrentData.frequencyBins[i] = 0.0f; |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | // Note: Pink noise compensation is applied later in processSample(), |
| 316 | // AFTER band energies are calculated from the raw CQ bins. |
| 317 | // This ensures bassEnergy/midEnergy/trebleEnergy reflect actual |
| 318 | // spectral content, not display-oriented compensation. |
| 319 | |
| 320 | // Find dominant frequency bin |
| 321 | float maxMagnitude = 0.0f; |
| 322 | int maxBin = 0; |
| 323 | for (int i = 0; i < 16; ++i) { |
| 324 | if (mCurrentData.frequencyBins[i] > maxMagnitude) { |
| 325 | maxMagnitude = mCurrentData.frequencyBins[i]; |
| 326 | maxBin = i; |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | // CQ bins are log-spaced — use the same formula as Bins::binToFreq(). |
| 331 | mCurrentData.dominantFrequency = mFFTBins.binToFreq(maxBin); |
| 332 | mCurrentData.magnitude = maxMagnitude; |
| 333 | } |
| 334 | |
| 335 | void Reactive::updateVolumeAndPeak(const Sample& sample) { |
| 336 | // Get PCM data from Sample |