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

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

Generated by: LCOV version 2.0-1