| 207 | //------------------------------------------------------------------------------ |
| 208 | |
| 209 | void KeyDetector::extractChroma(const fft::Bins& fft, float* chroma) { |
| 210 | // Initialize chroma to zero |
| 211 | for (int i = 0; i < 12; i++) { |
| 212 | chroma[i] = 0.0f; |
| 213 | } |
| 214 | |
| 215 | // Map fft::FFT bins to pitch classes using linearly-rebinned magnitudes. |
| 216 | // Linear bins give evenly-spaced frequency mapping: freq = fmin + bin * binWidth. |
| 217 | fl::span<const float> linearBins = fft.linear(); |
| 218 | const int numBins = static_cast<int>(linearBins.size()); |
| 219 | const float fmin = fft.linearFmin(); |
| 220 | const float fmax = fft.linearFmax(); |
| 221 | const float linearBinWidth = (numBins > 0) ? (fmax - fmin) / static_cast<float>(numBins) : 1.0f; |
| 222 | |
| 223 | // Process each linear bin |
| 224 | for (int bin = 0; bin < numBins; bin++) { |
| 225 | float magnitude = linearBins[bin]; |
| 226 | if (magnitude < 1e-6f) continue; |
| 227 | |
| 228 | // Calculate frequency for this linearly-spaced bin |
| 229 | float freq = fmin + (static_cast<float>(bin) + 0.5f) * linearBinWidth; |
| 230 | |
| 231 | // Skip frequencies below 60 Hz (below C2) |
| 232 | if (freq < 60.0f) continue; |
| 233 | |
| 234 | // Convert frequency to MIDI note number |
| 235 | // MIDI note = 69 + 12 * log2(freq / 440) |
| 236 | float midiNote = 69.0f + 12.0f * (fl::logf(freq / 440.0f) / fl::logf(2.0f)); |
| 237 | |
| 238 | // Get pitch class (0-11) |
| 239 | int pitchClass = static_cast<int>(midiNote + 0.5f) % 12; |
| 240 | if (pitchClass < 0) pitchClass += 12; |
| 241 | |
| 242 | // Accumulate magnitude into pitch class |
| 243 | chroma[pitchClass] += magnitude; |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | void KeyDetector::normalizeChroma(float* chroma) { |
| 248 | // Find maximum value |
nothing calls this directly
no test coverage detected