LCOV - code coverage report
Current view: top level - src/audio/effects - tuner.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 93.5 % 138 129
Test Date: 2026-06-01 11:15:25 Functions: 100.0 % 8 8

            Line data    Source code
       1              : #include "audio/effects/tuner.h"
       2              : #include "audio/effects/effect_factory.h"
       3              : 
       4              : #include <cmath>
       5              : #include <cstring>
       6              : 
       7              : namespace Amplitron {
       8              : 
       9            2 : static EffectRegistrar<TunerPedal> reg("Tuner");
      10              : 
      11              : static const char* NOTE_NAMES[] = {
      12              :     "C", "C#", "D", "D#", "E", "F",
      13              :     "F#", "G", "G#", "A", "A#", "B"
      14              : };
      15              : 
      16           24 : const char* TunerPedal::note_name(int note_index) {
      17           24 :     if (note_index < 0 || note_index > 11) return "?";
      18           21 :     return NOTE_NAMES[note_index];
      19            8 : }
      20              : 
      21          116 : TunerPedal::TunerPedal() {
      22          203 :     params_ = {
      23           87 :         {"Mute",    1.0f, 0.0f, 1.0f, 1.0f, "", "When fully engaged (1.0), the tuner silences the audio output while you tune."},
      24           29 :         {"A4 Ref", 440.0f, 430.0f, 450.0f, 440.0f, "Hz", "Calibration frequency for the note A4. Default is standard 440Hz."},
      25          203 :     };
      26           58 :     yin_buffer_.resize(YIN_BUFFER_SIZE, 0.0f);
      27           87 :     yin_buf_.resize(YIN_BUFFER_SIZE, 0.0f);
      28           87 :     yin_d_.resize(YIN_BUFFER_SIZE / 2, 0.0f);
      29          116 :     recalc_update_interval();
      30          203 : }
      31              : 
      32           48 : void TunerPedal::set_sample_rate(int sample_rate) {
      33           48 :     sample_rate_ = sample_rate;
      34           64 :     recalc_update_interval();
      35           48 : }
      36              : 
      37          135 : void TunerPedal::recalc_update_interval() {
      38              :     // ~15 updates per second for responsive display
      39          135 :     update_interval_ = sample_rate_ / 15;
      40          135 :     if (update_interval_ < YIN_BUFFER_SIZE)
      41          132 :         update_interval_ = YIN_BUFFER_SIZE;
      42           90 : }
      43              : 
      44           48 : void TunerPedal::reset() {
      45           48 :     std::fill(yin_buffer_.begin(), yin_buffer_.end(), 0.0f);
      46           48 :     yin_write_pos_ = 0;
      47           48 :     yin_buffer_full_ = false;
      48           48 :     samples_since_update_ = 0;
      49           48 :     detected_freq.store(0.0f, std::memory_order_relaxed);
      50           48 :     detected_cents.store(0.0f, std::memory_order_relaxed);
      51           48 :     detected_note.store(-1, std::memory_order_relaxed);
      52           48 :     detected_octave.store(-1, std::memory_order_relaxed);
      53           48 :     signal_detected.store(false, std::memory_order_relaxed);
      54           48 : }
      55              : 
      56         4575 : void TunerPedal::process(float* buffer, int num_samples) {
      57         4575 :     if (!enabled_) return;
      58              : 
      59         4575 :     float a4_ref = params_[1].value;
      60         4575 :     bool mute = params_[0].value >= 0.5f;
      61              : 
      62              :     // Accumulate samples into YIN buffer
      63      1175007 :     for (int i = 0; i < num_samples; ++i) {
      64      1170432 :         yin_buffer_[yin_write_pos_] = buffer[i];
      65      1170432 :         yin_write_pos_++;
      66      1170432 :         if (yin_write_pos_ >= YIN_BUFFER_SIZE) {
      67          267 :             yin_write_pos_ = 0;
      68          267 :             yin_buffer_full_ = true;
      69           89 :         }
      70       390144 :     }
      71              : 
      72         4575 :     samples_since_update_ += num_samples;
      73              : 
      74              :     // Run pitch detection at the update interval when buffer is full
      75         4575 :     if (yin_buffer_full_ && samples_since_update_ >= update_interval_) {
      76          267 :         samples_since_update_ = 0;
      77              : 
      78          267 :         float freq = yin_detect_pitch(a4_ref);
      79          267 :         if (freq > 0.0f) {
      80          231 :             detected_freq.store(freq, std::memory_order_relaxed);
      81          231 :             signal_detected.store(true, std::memory_order_relaxed);
      82          231 :             freq_to_note(freq, a4_ref);
      83           77 :         } else {
      84           36 :             signal_detected.store(false, std::memory_order_relaxed);
      85              :         }
      86           89 :     }
      87              : 
      88              :     // Mute output when tuner is active (standard hardware tuner behavior)
      89         4575 :     if (mute) {
      90         1188 :         std::memset(buffer, 0, static_cast<size_t>(num_samples) * sizeof(float));
      91          396 :     }
      92         1525 : }
      93              : 
      94              : // ============================================================
      95              : // YIN pitch detection algorithm
      96              : // Reference: de Cheveigné & Kawahara, 2002
      97              : // ============================================================
      98              : 
      99          267 : float TunerPedal::yin_detect_pitch(float /*a4_ref*/) {
     100              :     // W = integration window length. Use half the buffer.
     101          267 :     const int W = YIN_BUFFER_SIZE / 2;
     102              : 
     103              :     // Linearize the circular buffer into preallocated member (no heap alloc)
     104      1093899 :     for (int i = 0; i < YIN_BUFFER_SIZE; ++i) {
     105      1093632 :         yin_buf_[i] = yin_buffer_[(yin_write_pos_ + i) % YIN_BUFFER_SIZE];
     106       364544 :     }
     107              : 
     108              :     // Check if there's enough signal energy (RMS gate)
     109          178 :     float energy = 0.0f;
     110      1093899 :     for (int i = 0; i < YIN_BUFFER_SIZE; ++i)
     111      1093632 :         energy += yin_buf_[i] * yin_buf_[i];
     112          267 :     float rms_val = std::sqrt(energy / YIN_BUFFER_SIZE);
     113          267 :     if (rms_val < 0.01f) return -1.0f; // Too quiet — no pitch
     114              : 
     115              :     // Step 1 & 2: Difference function d(tau) and cumulative mean normalized
     116              :     // difference function d'(tau)
     117              :     // Reuse preallocated member (no heap alloc)
     118          231 :     std::fill(yin_d_.begin(), yin_d_.begin() + W, 0.0f);
     119              : 
     120              :     // d'(0) is defined as 1
     121          231 :     yin_d_[0] = 1.0f;
     122              : 
     123          231 :     float running_sum = 0.0f;
     124              : 
     125       473088 :     for (int tau = 1; tau < W; ++tau) {
     126       315238 :         float diff = 0.0f;
     127    968883993 :         for (int j = 0; j < W; ++j) {
     128    968411136 :             float delta = yin_buf_[j] - yin_buf_[j + tau];
     129    968411136 :             diff += delta * delta;
     130    322803712 :         }
     131              : 
     132              :         // Cumulative mean normalized difference
     133       472857 :         running_sum += diff;
     134       472857 :         yin_d_[tau] = (running_sum > 0.0f) ? (diff * tau / running_sum) : 1.0f;
     135       157619 :     }
     136              : 
     137              :     // Step 3: Absolute threshold — find the first dip below threshold
     138              :     // then pick the deepest local minimum within that dip.
     139          231 :     constexpr float YIN_THRESHOLD = 0.20f;
     140              : 
     141              :     // Minimum lag: highest detectable frequency ~2000Hz
     142          231 :     int min_tau = sample_rate_ / 2000;
     143          231 :     if (min_tau < 2) min_tau = 2;
     144              : 
     145              :     // Maximum lag: lowest detectable frequency ~60Hz (below low E)
     146          231 :     int max_tau = sample_rate_ / 60;
     147          231 :     if (max_tau >= W) max_tau = W - 1;
     148              : 
     149              :     // Search strategy: find the first tau below threshold, then walk to
     150              :     // the local minimum. This avoids sub-harmonic false positives by
     151              :     // preferring the earliest qualifying dip (highest frequency candidate).
     152          231 :     int best_tau = -1;
     153        54828 :     for (int tau = min_tau; tau < max_tau; ++tau) {
     154        54828 :         if (yin_d_[tau] < YIN_THRESHOLD) {
     155              :             // Walk to the local minimum of this valley
     156          154 :             int valley_min = tau;
     157         7377 :             while (tau + 1 < max_tau && yin_d_[tau + 1] < yin_d_[tau]) {
     158         4764 :                 ++tau;
     159              :             }
     160              :             // tau now points at the local minimum (or end of descent)
     161          154 :             valley_min = tau;
     162          154 :             best_tau = valley_min;
     163          154 :             break;
     164              :         }
     165        18199 :     }
     166              : 
     167              :     // Fallback: if no dip was below threshold, take the global minimum
     168          231 :     if (best_tau < 1) {
     169            0 :         float global_min = 2.0f;
     170            0 :         for (int tau = min_tau; tau < max_tau; ++tau) {
     171            0 :             if (yin_d_[tau] < global_min) {
     172            0 :                 global_min = yin_d_[tau];
     173            0 :                 best_tau = tau;
     174            0 :             }
     175            0 :         }
     176              :         // Only accept if reasonably periodic
     177            0 :         if (global_min > 0.5f) return -1.0f;
     178            0 :     }
     179              : 
     180          154 :     if (best_tau < 1) return -1.0f;
     181              : 
     182              :     // Step 4: Parabolic interpolation for sub-sample accuracy
     183          231 :     float refined_tau = static_cast<float>(best_tau);
     184          231 :     if (best_tau > min_tau && best_tau < W - 1) {
     185          231 :         float s0 = yin_d_[best_tau - 1];
     186          231 :         float s1 = yin_d_[best_tau];
     187          231 :         float s2 = yin_d_[best_tau + 1];
     188              :         // Only interpolate if it's a true local minimum
     189          231 :         if (s0 > s1 && s2 > s1) {
     190          231 :             float denom = 2.0f * (s0 - 2.0f * s1 + s2);
     191          231 :             if (std::fabs(denom) > 1e-12f) {
     192          231 :                 refined_tau += (s0 - s2) / denom;
     193           77 :             }
     194           77 :         }
     195           77 :     }
     196              : 
     197          231 :     if (refined_tau <= 0.0f) return -1.0f;
     198              : 
     199          231 :     float freq = static_cast<float>(sample_rate_) / refined_tau;
     200              : 
     201              :     // Sanity check: guitar range ~60Hz to ~1400Hz (high frets on high E)
     202          231 :     if (freq < 60.0f || freq > 1400.0f) return -1.0f;
     203              : 
     204          154 :     return freq;
     205           89 : }
     206              : 
     207          231 : void TunerPedal::freq_to_note(float freq, float a4_ref) {
     208              :     // Semitones from A4
     209          231 :     float semitones_from_a4 = 12.0f * std::log2(freq / a4_ref);
     210              : 
     211              :     // Nearest semitone
     212          231 :     int nearest = static_cast<int>(std::round(semitones_from_a4));
     213              : 
     214              :     // Cents deviation from nearest note
     215          231 :     float cents = (semitones_from_a4 - static_cast<float>(nearest)) * 100.0f;
     216              : 
     217              :     // A4 = MIDI note 69 (note index 9 = A, octave 4)
     218          231 :     int midi_note = 69 + nearest;
     219          231 :     int note_index = ((midi_note % 12) + 12) % 12; // 0=C .. 11=B
     220          231 :     int octave = (midi_note / 12) - 1;
     221              : 
     222          231 :     detected_note.store(note_index, std::memory_order_relaxed);
     223          231 :     detected_octave.store(octave, std::memory_order_relaxed);
     224          231 :     detected_cents.store(cents, std::memory_order_relaxed);
     225          231 : }
     226              : 
     227              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1