| 204 | } |
| 205 | |
| 206 | float ChordDetector::matchChordPattern(const float* chroma, int root, ChordType type) { |
| 207 | // Find the matching template using pre-computed lookup (O(1) vs O(n) linear search) |
| 208 | auto it = mTemplateMap.find(static_cast<int>(type)); |
| 209 | if (it == mTemplateMap.end()) return 0.0f; |
| 210 | |
| 211 | const ChordTemplate* tmpl = it->second; |
| 212 | if (!tmpl) return 0.0f; |
| 213 | |
| 214 | // Calculate match score |
| 215 | float matchScore = 0.0f; |
| 216 | float totalChroma = 0.0f; |
| 217 | |
| 218 | // Sum chroma energy for chord notes |
| 219 | for (int i = 0; i < tmpl->numNotes; i++) { |
| 220 | int interval = tmpl->intervals[i]; |
| 221 | if (interval < 0) break; |
| 222 | int pitchClass = (root + interval) % 12; |
| 223 | matchScore += chroma[pitchClass]; |
| 224 | } |
| 225 | |
| 226 | // Sum total chroma energy |
| 227 | for (int i = 0; i < 12; i++) { |
| 228 | totalChroma += chroma[i]; |
| 229 | } |
| 230 | |
| 231 | // Penalize energy in non-chord notes |
| 232 | float nonChordEnergy = totalChroma - matchScore; |
| 233 | |
| 234 | // Score is ratio of chord notes to total energy, minus non-chord penalty |
| 235 | float score = 0.0f; |
| 236 | if (totalChroma > 1e-6f) { |
| 237 | score = matchScore / totalChroma; |
| 238 | score -= 0.3f * (nonChordEnergy / totalChroma); |
| 239 | score = fl::max(0.0f, score); |
| 240 | } |
| 241 | |
| 242 | return score; |
| 243 | } |
| 244 | |
| 245 | bool ChordDetector::isSimilarChord(const Chord& a, const Chord& b) { |
| 246 | if (!a.isValid() || !b.isValid()) return false; |