Chord recognition looks simple from the outside: listen, identify pitches, name the chord. The difficult part starts earlier. A detector only sees a finite window of samples, so two nearby frequencies can land in the same FFT bin before any chord logic runs.

I reviewed the audio path in ChordCatcher, a private Flutter prototype. Its configuration makes a useful, concrete question possible: at the bottom of a guitar’s range, how much frequency detail does a 4,096-point FFT provide? The calculation below comes from the checked-in settings. It is a resolution analysis, not a test of a live microphone or a claim that the app mislabels a particular recording.

CHORDCATCHER / FFT RESOLUTION

Two low notes, one nearest bin
SOURCE-DERIVED
INPUT RATE44,100 Hz
FFT SIZE4,096 samples
BIN SPACING10.77 Hz
WINDOW92.9 ms

Nearest-bin view around the low E string

fundamental frequency →
E2 and F2 fall in the same nearest FFT-bin regionThe 4096-point FFT at 44100 samples per second has bins 10.77 hertz apart. E2 at 82.41 hertz and F2 at 87.31 hertz both round to bin 8, whose center is 86.13 hertz. This illustrates a frequency-resolution limit, not a measured misclassification.E2 · 82.41 HzF2 · 87.31 Hzk = 886.13 Hz center80.7586.1391.5280 Hz105 Hzsame nearest integer bin for both fundamentals

The blue region is the nearest-bin interval for k = 8. Harmonics and other peaks can still affect the final chord label.

  1. 01 / CAPTUREMicrophone blocks44,100 Hz · 4,096 samples

    Each block spans about 92.9 ms before processing.

  2. 02 / GATERMS thresholddefault 0.01

    Quiet blocks are skipped before FFT analysis.

  3. 03 / ANALYZEHann window → FFT80–1,200 Hz · top 12 peaks

    Local maxima above 10% of the strongest peak are kept.

  4. 04 / MATCHPitch classes → chordheuristic overlap score

    The displayed confidence is a score, not a calibrated probability.

A source-based explanation of the configured FFT and pitch-matching path. The chart is calculated from the sample rate and FFT size; it is not a microphone recording or a device accuracy benchmark.

Start with the window, not the chord name

The capture code requests 44,100 samples per second and processes blocks of 4,096 samples. One block therefore spans:

4,096 / 44,100 ≈ 0.0929 seconds

For an FFT of that size at that sample rate, adjacent bin centers are separated by:

44,100 / 4,096 ≈ 10.77 Hz

The processor applies a Hann window, searches from 80 to 1,200 Hz, keeps local peaks above 10% of the strongest magnitude, and retains at most twelve peaks. The window reduces spectral leakage compared with abruptly cutting a signal, but it does not make the bin centers closer together. Peak interpolation or another pitch estimator could add precision; this code path converts the selected integer bin directly to frequency.

Why the low strings are a useful stress case

The standard guitar’s low E is about 82.41 Hz; the next semitone, F, is about 87.31 Hz. Those fundamentals are only about 4.90 Hz apart—less than the configured 10.77 Hz bin spacing.

With nearest-integer bin selection, both frequencies round to bin 8:

E2: 82.41 × 4,096 / 44,100 ≈ 7.65 → bin 8
F2: 87.31 × 4,096 / 44,100 ≈ 8.11 → bin 8
bin 8 center: 8 × 44,100 / 4,096 ≈ 86.13 Hz

That does not prove the whole chord detector must confuse E and F. A plucked string produces harmonics, and the other notes in a chord contribute additional peaks. It does show that the fundamental alone cannot be separated by this nearest-bin mapping in this example. The distinction matters because converting a coarse frequency to the nearest semitone may look precise in the interface even when the input evidence is ambiguous.

Follow the signal through the implementation

The capture layer computes RMS and skips a block below its default threshold of 0.01. Blocks above the threshold are sent to a separate Dart isolate for processing. There the samples are windowed, transformed, filtered to the configured frequency range, and reduced to candidate peaks. Each integer FFT bin becomes a frequency estimate, which is mapped to the nearest semitone and then to a pitch class.

Chord matching is another heuristic step. The score rewards matching pitch classes and subtracts a penalty for extra detected notes. It is clamped to a 0–1 range and exposed under a confidence name, but the repository does not calibrate that value against labelled recordings. I would call it a match score until such a calibration exists; 0.8 should not be read as an 80% chance that the chord is correct.

The capture callback also notifies UI listeners. That makes responsiveness and audio-thread scheduling worth measuring on actual devices, especially when blocks arrive continuously. The source alone does not establish that notifications cause dropouts or that the isolate guarantees a particular latency, so I treat those as test questions rather than findings.

What I would measure next

The repository includes an audio processor and an ML pitch detector, but I found no automated processor tests, labelled recording set, or reproducible device benchmark for this path. The CI workflow does not run Flutter analysis or tests; its checks inspect repository structure. That is a useful boundary to make visible: a plausible signal-processing implementation is not yet evidence of pitch accuracy or real-time performance.

I would add deterministic synthetic tones for E2 and F2 first, then test mixtures with harmonics, noise, and realistic attack/decay envelopes. Each case should have an expected note or chord label. The comparison could include the current nearest-bin approach, interpolated FFT peaks, and a time-domain estimator such as YIN, using the same recordings and thresholds.

For a device run, I would publish the device model, operating-system version, sample rate actually delivered by the microphone, warm-up procedure, number of trials, note/chord accuracy, and median plus p95 time from block capture to UI result. Those measurements would answer two separate questions: whether the detector identifies the right notes, and whether it returns them quickly enough to feel live.

Until that evidence exists, the defensible result is narrower: ChordCatcher has a source-visible audio pipeline, and its FFT settings imply roughly 10.77 Hz between bin centers. That is a useful engineering constraint to investigate, not a performance score to advertise.

The source repository is private, so this note describes the implementation at a high level and does not publish its code or assets.

← All notes