What was checked and what was not measured
On November 4, 2026, we inspected pitch_detector.dart and audio_dsp_specs.dart in Narae’s local project. Unmodified copies were placed in a separate verification folder and exercised with synthetic signals using plain Dart. Neither the app nor the original files was changed.
The sine inputs used a 44,100 Hz sample rate and 2,048 samples. This did not test a microphone, speaker, live instrument, or phone processing latency. The figures below are returned frequencies under those conditions, not a guarantee of real-world tuner accuracy.
| Synthetic input | Returned value | Interpretation |
|---|---|---|
| 440 Hz sine | 440.101 Hz | Frequency candidate returned |
| 261.63 Hz sine | 262.007 Hz | Frequency candidate returned |
| 82.41 Hz sine | 83.012 Hz | Frequency candidate returned |
| Silence / 256-sample input / seeded noise | -1 in each case | No detected pitch |
The noise generator used seed 42 for reproducibility. Rejecting this one noise input does not establish rejection of every kind of environmental noise.
Download the standalone Dart verification ZIP for the required detector, constants, verification script, and bilingual README. It contains no microphone access, Flutter, network or advertising SDK, or app assets. Unused solfege display and unrelated constants are omitted; detection and cents-conversion calculations remain the same.
- Prepare the Dart SDK and extract the ZIP into a new folder.
- Open a terminal beside
verify.dartand README, then run the command below. - Success prints one JSON line. Round sine detections to three decimals when comparing with the table. Failed checks throw
StateErrorand exit unsuccessfully.
dart run verify.dart
Each 2,048-sample sine uses 0.5 * sin(2 * pi * frequency * n / 44100). The check fails if a returned frequency differs from its input by more than 1.5 Hz. Silence is 2,048 zeros; the short buffer is 256 values of 0.5. Noise uses 2,048 values of (nextDouble() * 2 - 1) * 0.1 from Dart Random(42). Each of these three non-pitch inputs must return -1.
The cents checks allow 0.1 cent around +19.56 and -7.85. These tolerances are regression checks for the example, not real-instrument accuracy specifications. We extracted the ZIP into a new folder and reproduced the results with Dart SDK 3.12.2 on Windows x64. Record dart --version when comparing runs because SDK random implementations may differ.
The path from samples to a period
The detector first rejects buffers shorter than 1,024 samples, then uses RMS to reject a quiet input. It subtracts the mean to reduce DC offset, shifts the waveform against itself, and calculates autocorrelation values.
It finds local peaks in repeated intervals, compares the chosen peak with signal energy, and estimates a fractional lag using parabolic interpolation around neighboring values. The configured search range and thresholds are implementation choices, not physical guarantees for every instrument.
Instrument sounds contain harmonics. Selecting the largest FFT peak and comparing autocorrelation candidates are different approaches. Autocorrelation can still choose the wrong candidate with short attacks, strong harmonics, or mixed instruments, so real audio needs further evaluation.
Frequency, note name, and cents are separate steps
After pitch detection, convertHzToNote() takes a logarithmic ratio against the reference pitch and chooses the nearest note. It then expresses the deviation from that note’s target frequency in cents. The same 440 Hz input reads slightly flat if the reference is 442 Hz.
This independent example only calculates cents. Running it in Node.js or a browser console prints 19.56 and -7.85. It is not a complete pitch detector and does not address microphone permissions or harmonics.
function centsFrom(frequency, target) {
if (!Number.isFinite(frequency) || frequency <= 0 ||
!Number.isFinite(target) || target <= 0) {
throw new RangeError('Frequencies must be finite and positive.');
}
return 1200 * Math.log2(frequency / target);
}
console.log(centsFrom(445, 440).toFixed(2));
console.log(centsFrom(440, 442).toFixed(2));
// 19.56
// -7.85
The actual detector conversion also returned A4 at approximately +19.56 cents for 445 Hz, and A4 at approximately -7.85 cents for 440 Hz under a 442 Hz reference. Check the reference setting before treating the difference as an instrument problem.
Separate input failures from rejected candidates
- Check microphone permission and whether the app is receiving input. Permission failure and no detected pitch have different causes.
- Sustain one note and compare with fewer competing sounds. Simultaneous notes make a single-pitch detector’s task harder.
- Record an octave-high reading separately from a completely missing reading.
- Check reference pitch and the intended instrument. Identical frequencies can produce different cents readings under different references.
The inspected code returns the same no-pitch value for silence, short buffers, and low-confidence candidates. A blank note display therefore does not by itself establish a broken microphone. Reproduce the device state and distinguish missing input from candidate rejection.
What to verify next
A synthetic sine is useful for checking the calculation path, but does not reproduce instrument attacks, reverberation, harmonics, or automatic microphone processing. Real-audio evaluation needs recorded device, output route, microphone distance, reference pitch, and buffer conditions, compared with a known reference signal. Measure processing time separately from frequency error.
Reviewing the microphone-analysis code is also different from verifying the entire app’s data processing. See the Narae privacy notice for the distinction between audio and advertising SDKs, and the product guide for usage steps.