The tuner subtracts its own drone
A tuner that plays a drone through the speaker can hear itself louder than the player. Beatwheel cancels its own drone with a recursive least squares filter and shows beats near unison.
Beatwheel is a strobe tuner for Android that keeps a drone sounding while it measures the note played against it. On the phone's speaker the microphone can hear the drone louder than the player, so a tuner that measures the mixture measures its own drone. The app tracks the room's gain and phase at the drone's fundamental and first eight harmonics with a recursive least squares filter that forgets over about two seconds, and subtracts the estimate; each partial becomes a notch well under 1 Hz wide, so a player a few hertz away passes through. Within 1.5 Hz of a partial the two tones cannot honestly be separated, so there the app shows signed beats per second instead of cents. The lesson is to work out what the physics allows and pin it in a test: a player 23.4 cents sharp of the drone reads within half a cent with the canceller on, and with it off the test accepts only a reading more than three cents adrift, or none.
The number the test pins
The test is DroneCancellerTest in app/src/test/kotlin/com/mohdshayan/beatwheel/core/DspTest.kt, and it runs on the JVM with synthetic signals. It renders four seconds of the reed drone at 440 Hz at unit gain from a table that peaks at 0.9, passes it through a simulated room, a direct path delayed 37 samples at a gain of 0.9 plus one reflection at 211 samples and 0.3, and mixes in a player at 446 Hz at an amplitude of 0.5 with harmonics in the ratio 1.0, 0.5 and 0.25. 446 against 440 is 1200 log2(446/440), which is 23.45 cents. With the canceller told the drone is at 440 Hz, the estimator must report MIDI 69, lock the strobe, and read 23.4 cents within 0.5. The same signal then runs with the canceller cleared, and that assertion is loose on purpose: either no pitch at all, or a reading more than three cents from 23.4. The comment above it says why: without cancellation the drone drags the reading toward its own pitch.
A neighbouring test sets the scale for that half cent: the phase demodulator alone, fed a 440.127 Hz tone with a little noise, must read 0.5 cents within 0.1.
From the microphone to the reading
The microphone delivers 48 kHz mono float blocks of 480 samples to an analysis thread in audio/AudioEngine.kt, which runs the canceller, then the estimator, then the beat meter. The canceller is told about the drone only when the drone is on the speaker: DroneSynth.isOutputPrivate() looks for wired, USB or Bluetooth headphones, and with any of those present the canceller is given 0 Hz and passes the signal through.
The estimator in core/pitch/PitchEstimator.kt is two detectors stacked. The coarse one is YIN, run every 40 ms on a copy decimated by four to 12 kHz, as de Cheveigné and Kawahara published it in 2002. The paper reports error rates about three times lower than the best competing methods on speech recorded with a laryngograph, and its name, from yin and yang, refers to the interplay of autocorrelation and cancellation. YIN chooses the note, two agreeing frames to pick one from silence and three to change it.
The fine detector is PhaseDemodulator.kt. The signal is mixed against a quadrature oscillator at the target frequency and averaged over blocks a whole number of periods long, so every harmonic of the target averages out, leaving a phasor that turns at the difference frequency; its angle drives the disc, and the least-squares slope of its unwrapped angle over 0.5 s (1.5 s in held-tone mode) gives the cents. That is a mechanical strobe in software: the Peterson strobe tuner patent, granted in 1999, has the pattern stationary when the note is perfectly in tune, moving clockwise when sharp and counterclockwise when flat, at a speed proportional to the error in frequency.
A notch per partial
core/drone/DroneCanceller.kt is short. For each partial, up to nine, it keeps a reference phase advancing at that partial's frequency, two weights on the cosine and sine of the phase, and a symmetric two by two matrix. Every sample it predicts the drone from the weighted cosines and sines, subtracts the prediction, and updates each partial's weights by the recursive least squares rule with the error left over. The forgetting factor is 1 minus 1 over two seconds of samples, 0.99999 at 48 kHz to five places. Partials above 0.45 times the sample rate are dropped, 21,600 Hz at 48 kHz, and nine partials of A 440 top out at 3,960. Nine is the length of the reed drone's harmonic list in Wavetables.kt.
In the RLS filter, as Wikipedia describes it (read on 12 October 2026), the forgetting factor gives exponentially less weight to older error samples and is usually chosen between 0.98 and 1. Beatwheel's is close enough to 1 that the weights average over about two seconds, and that is what makes each partial a narrow notch. A component at the partial's own frequency holds a fixed phase against the reference and the weights settle on it; one a few hertz away turns through several cycles inside two seconds, leaves nothing lasting in the weights, and passes. The header comment states the consequence and the assumption together: each partial becomes a notch well under 1 Hz wide, and the room path is assumed steady, which is what makes the notches narrow.
Where the physics stops
A notch under 1 Hz wide does not help when the player is inside it. Play A 440 against an A 440 drone and the canceller removes the drone and the player together. BeatMeter.kt carries the constant for this, UNISON_WINDOW_HZ = 1.5, and AudioEngine.kt engages the meter when the estimate lies within 1.5 Hz of the nearest drone partial, about six cents at A 440. The meter demodulates the cancelled signal at the partial over a two-second window, subtracts the mean phasor, the steady drone residue, and fits a least-squares line to the unwrapped phase of the remainder. The slope is the beat rate, signed, positive when the player is sharp.
That is the honest output because it is what the player hears. Beats occur at the difference of the two frequencies, musicians use them to check tuning at the unison, and piano and organ tuners count them for particular intervals. A readout claiming 0.3 cents there would be inventing a number; the test instead asks the meter for 1.0 beats per second within 0.05 from a 441 Hz player over a 440 Hz drone, and minus 1.0 from 439.
What the test does not say
The simulated room is kinder than a real one: one reflection, no movement, four seconds. It checks the design, not a hall, and the raw assertion proves only that the canceller does the work. BLUEPRINT.md in the repository names measuring with the drone on the speaker as the hardest subsystem, makes the simulated-room JVM test the gate when no phone check is available, and keeps headphones-only measuring as the fallback if cancellation failed.
Two facts about the app, as the public repository on GitHub stood on 12 October 2026, last changed on 8 October. The manifest declares RECORD_AUDIO and removes INTERNET and ACCESS_NETWORK_STATE from the merged manifest. And the 8 October commit, versioned 1.1.0, moves the code to a free tier with one optional full unlock: core/unlock/FreeTier.kt lets the meter publish only when no drone sounds or the unlock is on, so that build plays the drone but shows no reading while it sounds. That is a commit, not a release: the repository has no release or tag, the Play listing, read on 12 October 2026, still sells version 1.0.0 as a one-time purchase, and the README, last changed on 13 September, still says paid once.
Measure what the physics allows
The two numbers this post turns on are in BLUEPRINT.md as well as in the code: a two-second memory fixes the notch at under a hertz, and the notch fixes the point, 1.5 Hz, past which no honest cents reading exists. The blueprint's list of JVM tests includes the canceller test at 23.4 cents within 0.5. A canceller that passed on a phone with no such test would be a feeling about one room; one with the test is a claim anyone can run with ./gradlew testDebugUnitTest.
Get new posts by email
Occasional essays on engineering, AI, and building for the people technology leaves behind.
Subscribe with RSS