Everything adds up
What arrives at the microphone is the sum of all sound sources. Following one of them means separating it out of this sum first.
A charger, a smoke alarm with a flat battery, a headphone case, an uninterruptible power supply in the basement. It can be heard but not found. BeepHunt listens in, remembers exactly this one sound and, while the user walks around, says whether it is getting warmer or colder.
A beep every two seconds is loud enough to drive anyone mad and too short to follow. Before anyone has turned their head, it is over. If it comes from a drawer, from behind a cupboard or from the ceiling, it sounds much the same everywhere, because the room reflects it off every wall.
The obvious tools do not help. A sound level meter measures everything at once: fridge, street, one's own breathing and the beep add up to a single number. A spectrum analyser does show frequencies, but leaves all the work to the viewer.
What arrives at the microphone is the sum of all sound sources. Following one of them means separating it out of this sum first.
Standing waves form in a furnished room. With high tones, two centimetres can decide between loud and quiet.
Hearing direction needs at least two separate recording channels. Many devices report two and in reality deliver one.
The app asks one question and offers one button. Everything else happens in the background; what it can do beyond that depends on the device, but the path to the find works with a single microphone.
The app listens in and keeps the last 15 seconds in memory. This lead time is necessary: by the time the button is pressed, the beep is long gone.
What was heard becomes a short list, sorted by how precisely a device keeps its rhythm – not by what is loudest. Each entry is described so that it can be recognised: “88 ms, every 3.00 s, 3.10 kHz, heard seven times”.
From now on only this one sound counts. The rest of the room is ignored. While the user walks around, the app reports warmer or colder and remembers the best spot.
Where the hardware allows it: a turn on the spot for a rough sector, two real microphones for a bearing, a second phone for locating within the room.
Eight questions that had to be answered while building it – what can and cannot be determined from a variation in air pressure. All numerical values are the ones the app actually calculates with.
To know which frequency is sounding at the moment, a piece of signal has to be examined. The longer this piece, the more precise the frequency and the less precise the point in time. This is not a question of effort but a property of the Fourier transform: for a window of length N the frequency resolution is fixed.
23.4 Hz is more than the difference between two units of the same device. So one window is not enough. BeepHunt therefore calculates three resolutions at once, each for the task it is suited to.
| Stream | Window | Frequency grid | Time resolution | Task |
|---|---|---|---|---|
| Fast | 1024 | 46.9 Hz | 5.3 ms | exactly when a tone starts |
| Main | 2048 | 23.4 Hz | 10.7 ms | detection and recognition |
| Fine | 4096 | 11.7 Hz | 21.3 ms | determining the frequency precisely |
Even the finest grid would still be too coarse. The actual frequency almost never lies exactly on a grid point, but on a logarithmic scale the main lobe of a sine tone is almost a parabola. Fitting a parabola through the largest point and its two neighbours, its vertex gives the frequency to about one per cent of a grid step.
The same window length also distorts the measured duration: a window already sees the tone as soon as it reaches in, and still sees it when it is almost out. Every event therefore measures too long, and by a constant amount, regardless of how long it really was. For the window used, that is 19.1% of its length. Subtracting it turns a measured 108 ms back into the 100 ms that the device actually beeps.
A fixed threshold in decibels does not work: in a quiet flat it is too high, next to a street too low. BeepHunt therefore never measures in absolute terms, but always relative to the background at the same point. The method comes from radar technology, where it is called CFAR, after the constant false alarm rate it guarantees.
The guard gap prevents the tone being measured from raising its own background.
Three details carry the whole method:
| Decision | Why |
|---|---|
| Average power, not decibels | Averaging decibels actually forms the geometric mean, and that lies below the true background. Every signal-to-noise ratio therefore comes out too good. This one detail was the difference between zero and 1014 false alarms over twelve seconds of noise. |
| Take the larger side | Room background is pink: it falls off towards high frequencies. A symmetric window over a sloping curve averages the two sides and sits too low on the loud side. In the lower two octaves, that permanently invents 12 to 13 dB of signal-to-noise ratio that does not exist. |
| Across frequency, not across time | Estimating the background from the past of the same frequency loses every continuous tone: after one minute, the past is that tone, and it is measured against itself. Only estimating from the neighbouring frequencies lets a coil whine stand out permanently. |
The power of a noise cell is exponentially distributed. This makes it possible to calculate how often pure chance breaks the threshold:
The sound pressure of a point source decreases with distance. Moving in from five metres to half a metre changes the level by around 20 dB. A recognition feature that shifts with volume would therefore stop matching precisely when one is closest.
The solution lies in a rule of arithmetic: a gain is a multiplication in amplitude terms and an addition in decibel terms. Subtracting its own mean from the spectrum in decibels removes exactly this constant term, and with it the volume, without losing the shape.
Every other feature is made independent in the same way: the envelope by its own maximum, the frequency curve by its median, the partials relative to the strongest. Duration and harmonicity are level-independent anyway.
When comparing two recordings, frequency carries the most weight, because the oscillator that sets the pitch of a beeper changes neither with distance nor with viewing direction. What is compared is the relative deviation: 30 Hz is a rounding error at 12 kHz and a different tone at 400 Hz. Envelope and duration weigh less, because the room's reverberation smears the end of every event. On top of that, cheap beepers are no clocks: the same alarm clock delivers 92 ms and, minutes later, 104 ms once its oscillator has warmed up.
A device beeps every two seconds. When a lorry drives past, one beep is drowned out, and the intervals become 2.0 · 2.0 · 4.0 · 2.0. The median is still 2.0, but the 4.0 pushes the spread up so far that the beat counts as irregular and the countdown switches off – precisely when one is standing in a noisy environment and needs it most.
That is why intervals are folded: if an interval fits an integer multiple of the fundamental period, it is divided by that multiple and admitted again. 4.0 becomes two beats of 2.0. The fundamental period itself is estimated from the shorter half of the intervals, because the average of all intervals would be pulled upwards by every gap, and a fundamental period that is too long no longer folds anything correctly.
In a free field it would be simple: the level falls with distance, so the distance could be calculated from the level. In a room, that does not hold. The direct sound overlaps with the reflections from the walls, and with a pure tone this creates a fixed pattern of maxima and minima in the room. Their spacing is half a wavelength.
The app therefore never calculates a distance from volume. It only says warmer or colder, uses a dead zone of 1.5 dB for this, and asks for several small steps instead of one big one. In addition, it remembers the best value reached so far, so walking past the spot shows up.
Two microphones a distance d apart hear the same sound wave one after the other. Because the source is far enough away for the wavefront to be treated as plane, the time difference depends only on the angle.
At a 48 kHz sampling rate, a single sample corresponds to 7.1 mm of path difference. The peak of the cross-correlation is therefore interpolated between samples; the result is better than 0.4 samples.
Two limits are hard here. First: |c·τ| ≤ d must hold. A larger difference cannot be a direct path, only a reflection. It is discarded rather than clipped to ±90°, which would otherwise produce a confident bearing straight ahead or straight behind whenever in doubt.
Second: a pure phase measurement is only unambiguous as long as the microphone spacing is at most half a wavelength. Beyond that, the pattern repeats.
A narrow-band tone therefore produces a correlation that oscillates at its own frequency and every 1/f has peaks of almost equal height. Taking the highest means taking an arbitrary one. That produces bearings that are off by dozens of degrees while looking very convincing. BeepHunt therefore also forms the envelope of the correlation. It does not oscillate along, its width depends on the bandwidth rather than on the centre frequency, and it shows which peak is the right one. Only within this peak does the actual correlation provide the fine detail.
What even that does not resolve: two omnidirectional microphones measure exactly one number, and one number cannot fix a direction in space. Every bearing is really a cone around the connecting line; in the plane, this leaves an ambiguity between front and back. The app names both possibilities and suggests two steps to the side instead of picking one and being wrong half the time.
And finally, everything depends on a quantity that no sensor measures:
Two phones hear the same beep at different moments, and the difference could be used to determine the location. Before that, however, there is a problem bigger than the effect being measured: the audio clocks of two devices have nothing to do with each other. As long as their relationship has not been measured, a time difference between them is not imprecise but meaningless.
Two nominally identical 48 kHz crystals typically differ by a few tens of millionths. 25 ppm amounts to 3 ms after two minutes, which converts to a good metre of path difference – in a measurement whose entire useful signal is a few hundred microseconds.
The measurement uses sound, not the network: Wi-Fi latencies fluctuate in the millisecond range and differ between the two directions. They would shift the result, not merely scatter it. The room, on the other hand, is the only clock both devices truly share. Each phone plays a short sweep, and both hear both. The two directions give two equations:
The calibration tone is a logarithmic sweep from 2 to 10 kHz in 60 ms. A click would have the sharpest autocorrelation, but would need a high peak amplitude for it. A tone burst would be quiet and unobtrusive, but correlates with itself when shifted by whole periods – every 0.25 ms at 4 kHz, i.e. every 8 cm of apparent distance. A sweep spreads its energy over time and therefore stays quiet, but has the bandwidth that a sharp response needs.
Only then does the actual localisation begin. The time difference between two receivers places the source on a hyperbola: all points whose difference in distance stays the same. With two devices, that is the honest result, and the app draws the curve instead of claiming a point. Only a third microphone provides a second hyperbola and thus an intersection.
The uncertainty does not come from a formula but from randomly perturbing each measurement 160 times within its own scatter and solving again. If the devices stand almost in a line, this produces a long, thin crescent, and that shape is at the same time the instruction to move a phone.
The most valuable part of a measurement is stating where it ends. This list also appears in the project documentation.
| Not claimed | Because |
|---|---|
| Distance from volume | Standing waves mean the level in the room does not change monotonically with distance. |
| Front or back from two microphones | A time difference is one number and cannot fix a direction in space. |
| A point from two devices | A time difference gives a hyperbola, not a location. |
| A location from devices in a row | The arrangement is poorly conditioned; the error slides away along the line. |
| A bearing from half a turn | Sectors not visited stay empty instead of being filled in between. |
| Which device is beeping | A tone is a tone. Deriving a make from it would be guesswork. The app describes the waveform, for example “short high electronic tone”. |
| That a signal is harmless | Never. If a sound resembles a warning device, the app points this out neutrally and says nothing else. False reassurance about a smoke or CO alarm would hurt the user, not me. |
Android audio hardware varies widely, and no data sheet describes it reliably. Instead of demanding minimum equipment, the app is built as a ladder: the lowest rung needs one microphone and works on its own. Each further rung is added when its prerequisite is really met.
The rule above all of this: No stage may make a lower one worse. If the stereo signal turns out to be a mix of a single microphone, warmer and colder stays exactly as good as before. If the connection to the second phone drops in the middle of a search, the single-device search carries on without any window popping up.
This check is not a formality. Manufacturers ship stereo recordings in which both channels come from the same processing. Such a signal correlates perfectly at zero delay on every event, and a naive evaluation turns it into a rock-solid bearing of 0°, always straight ahead, no matter where the source is. What matters is therefore not the height of the correlation, but the spread of the measured delays: two real microphones in a real room see values that move. Two channels from one source do not.
Kotlin, Compose for the interface, otherwise nothing third-party in the computing core. The signal processing deliberately sits in a module of its own without any Android dependency.
| Module | Content |
|---|---|
| Computing core | Pure Java module: Fourier transform, background estimation, fingerprints, correlation, geometry, least-squares fitting. No Android, no Compose. That makes it testable in milliseconds, and the layering rule is enforced by the build tool rather than by discipline. |
| Recording | Microphone access, ring buffer, check of the device's capabilities. |
| Platform | Sensors, distance measurement, connection to the second phone via network service discovery and a simple connection on the local network. No third-party service, so that the function does not depend on a service that not every device has. |
| Flow | The search lives in the application, not in a screen: a hunt for a beep that comes once a minute has to survive the device being rotated. |
The recording thread only writes values into the ring buffer and never blocks. The analysis thread reads at its own pace, and if it cannot keep up, it skips ahead and reports this, instead of holding up the writer or reading stitched-together audio. A gap costs one event; corrupted audio produces confident nonsense.
All on the Java side, run time around 14 seconds. They test against artificially generated signals: beeps at 1 and 8 kHz, 25 dB level difference, two sources at once, speech and music as interference. And silence when there is only noise.
The signal generator is not in the test folder but in the program itself. The diagnostics view uses it to send the same reference tone that the tests run against.
Only from this version onwards does the system report where a device's microphones are located. Without this information, no angle can be derived from a time difference.
An app that listens has to be able to say something about it. The audio is analysed exclusively on the device.
Started on 16 August 2026. The app is finished and is under review at Google Play. As soon as the review is complete, it will appear in the store.
Event detection, fingerprints, rhythm, target tracking, radar sweep and the calculation for a second phone.
Candidate list, finder, radar, introduction, help and questions, feedback by e-mail. German and English.
No crashes and no bugs reported, only suggestions for improvement. All of them have been implemented. 126 automated tests pass.
Version 1.0.1 has been submitted for release.
As soon as the review is complete, the app will appear in the store automatically.
Above the list there are three buttons: All, high peep, low hum. The boundary is 1 kHz. So the fridge, router and heating drop out when a detector is beeping. Will be released once 1.0.1 has been approved.
Questions about the construction, the decisions behind it or how it transfers to another case.