@@ -325,6 +325,7 @@ StreamAnalyzer::StreamAnalyzer(const StreamConfig& config) : config_(config) {
325325 if (config_.compute_chroma ) {
326326 chroma_buffer_.resize (12 );
327327 chroma_sum_.fill (0 .0f );
328+ bar_chroma_sum_.fill (0 .0f );
328329 // / Initialize chord templates for chord detection
329330 chord_templates_ = generate_triad_templates ();
330331 }
@@ -423,6 +424,15 @@ StreamFrame StreamAnalyzer::process_single_frame(const float* frame_start, size_
423424 chroma_sum_[i] += chroma_buffer_[i];
424425 }
425426 ++chroma_frame_count_;
427+
428+ // / Detect chord for this frame
429+ if (!chord_templates_.empty () && chroma_buffer_.size () == 12 ) {
430+ auto [best_chord, chord_corr] =
431+ find_best_chord (chroma_buffer_.data (), chord_templates_);
432+ frame.chord_root = static_cast <int >(best_chord.root );
433+ frame.chord_quality = static_cast <int >(best_chord.quality );
434+ frame.chord_confidence = std::max (0 .0f , chord_corr);
435+ }
426436 }
427437
428438 // / Compute onset strength
@@ -678,6 +688,84 @@ void StreamAnalyzer::update_progressive_estimate(float current_time) {
678688 }
679689 }
680690 }
691+
692+ // / Update bar-synchronized chord tracking
693+ if (config_.compute_chroma ) {
694+ update_bar_chord_tracking (current_time);
695+ }
696+ }
697+
698+ void StreamAnalyzer::update_bar_chord_tracking (float current_time) {
699+ // / Check if BPM is stable enough to start bar tracking
700+ if (!bar_tracking_active_) {
701+ if (current_estimate_.bpm_confidence >= kBpmConfidenceThreshold && current_estimate_.bpm > 0 .0f ) {
702+ // / Start bar tracking
703+ bar_tracking_active_ = true ;
704+ bar_duration_ = static_cast <float >(kBeatsPerBar ) * 60 .0f / current_estimate_.bpm ;
705+ current_bar_index_ = 0 ;
706+ bar_start_time_ = current_time;
707+ bar_chroma_sum_.fill (0 .0f );
708+ bar_chroma_count_ = 0 ;
709+
710+ // / Update estimate with bar info
711+ current_estimate_.bar_duration = bar_duration_;
712+ current_estimate_.current_bar = 0 ;
713+ }
714+ return ;
715+ }
716+
717+ // / Update bar duration if BPM changed significantly
718+ float new_bar_duration = static_cast <float >(kBeatsPerBar ) * 60 .0f / current_estimate_.bpm ;
719+ if (std::abs (new_bar_duration - bar_duration_) > 0 .1f ) {
720+ bar_duration_ = new_bar_duration;
721+ current_estimate_.bar_duration = bar_duration_;
722+ }
723+
724+ // / Accumulate chroma for current bar
725+ for (int c = 0 ; c < 12 ; ++c) {
726+ bar_chroma_sum_[c] += chroma_buffer_[c];
727+ }
728+ ++bar_chroma_count_;
729+
730+ // / Check if we've crossed a bar boundary
731+ if (current_time >= bar_start_time_ + bar_duration_) {
732+ // / Detect chord for completed bar using accumulated chroma
733+ if (bar_chroma_count_ > 0 && !chord_templates_.empty ()) {
734+ // / Normalize accumulated chroma
735+ std::array<float , 12 > bar_chroma;
736+ float sum = 0 .0f ;
737+ for (int c = 0 ; c < 12 ; ++c) {
738+ bar_chroma[c] = bar_chroma_sum_[c] / static_cast <float >(bar_chroma_count_);
739+ sum += bar_chroma[c];
740+ }
741+ if (sum > kEpsilon ) {
742+ for (int c = 0 ; c < 12 ; ++c) {
743+ bar_chroma[c] /= sum;
744+ }
745+ }
746+
747+ // / Find best chord for this bar
748+ auto [best_chord, chord_corr] = find_best_chord (bar_chroma.data (), chord_templates_);
749+
750+ // / Add to bar chord progression
751+ BarChord bar_chord;
752+ bar_chord.bar_index = current_bar_index_;
753+ bar_chord.root = static_cast <int >(best_chord.root );
754+ bar_chord.quality = static_cast <int >(best_chord.quality );
755+ bar_chord.start_time = bar_start_time_;
756+ bar_chord.confidence = std::max (0 .0f , chord_corr);
757+ current_estimate_.bar_chord_progression .push_back (bar_chord);
758+ }
759+
760+ // / Move to next bar
761+ ++current_bar_index_;
762+ bar_start_time_ = current_time;
763+ bar_chroma_sum_.fill (0 .0f );
764+ bar_chroma_count_ = 0 ;
765+
766+ // / Update estimate
767+ current_estimate_.current_bar = current_bar_index_;
768+ }
681769}
682770
683771size_t StreamAnalyzer::available_frames () const { return output_buffer_.size (); }
@@ -715,6 +803,9 @@ void StreamAnalyzer::read_frames_soa(size_t max_frames, FrameBuffer& buffer) {
715803 buffer.rms_energy .push_back (frame.rms_energy );
716804 buffer.spectral_centroid .push_back (frame.spectral_centroid );
717805 buffer.spectral_flatness .push_back (frame.spectral_flatness );
806+ buffer.chord_root .push_back (frame.chord_root );
807+ buffer.chord_quality .push_back (frame.chord_quality );
808+ buffer.chord_confidence .push_back (frame.chord_confidence );
718809
719810 // Append mel (row-major)
720811 buffer.mel .insert (buffer.mel .end (), frame.mel .begin (), frame.mel .end ());
@@ -832,6 +923,14 @@ void StreamAnalyzer::reset(size_t base_sample_offset) {
832923 prev_chord_root_ = -1 ;
833924 prev_chord_quality_ = -1 ;
834925 chord_stable_time_ = 0 .0f ;
926+
927+ // / Reset bar tracking state
928+ bar_tracking_active_ = false ;
929+ bar_duration_ = 0 .0f ;
930+ current_bar_index_ = -1 ;
931+ bar_start_time_ = 0 .0f ;
932+ bar_chroma_sum_.fill (0 .0f );
933+ bar_chroma_count_ = 0 ;
835934}
836935
837936AnalyzerStats StreamAnalyzer::stats () const {
0 commit comments