Coin Pinger
Acoustic bullion authentication — free, private, on-device
Under the hood
How Coin Pinger's algorithm works
A coin's ring carries more information than its pitch. Coin Pinger reads the whole ring — the resonance peaks, how steady they are, and how long the ring lasts — and matches it to a reference. Here's exactly how, with the real code. It's all open source under the MIT License.
1 · High-resolution peak detection
An FFT (Fast Fourier Transform) is the standard way to break a sound into the individual frequencies it contains — the same public, decades-old, textbook math behind every spectrum analyzer, guitar tuner, and voice app. There's nothing proprietary about it; we just run a big one. We use the largest FFT the browser allows (32768 points) on the ring — roughly 1.4 Hz frequency bins — then refine each peak with parabolic interpolation for sub-bin, sub-Hz precision. A coin's lowest three free-disc vibration modes (c0d2 / c0d3 / c0d4) become three sharp peaks.
// refine each peak to sub-Hz precision
// (parabolic interpolation on the FFT bins)
const denom = left - 2 * peak + right;
const offset = denom ? 0.5 * (left - right) / denom : 0;
const hz = (bin + offset) * binHz; // sub-bin frequency
2 · Is it even a coin ring?
Before any verdict, the captured sound has to look like a struck free disc. We reject anything that isn't: no sharp peak, too short to be a ring, an unsteady (drifting) pitch, or peaks whose spacing doesn't fit the 1 : 2.3 : 3.9 modal ratios of a vibrating disc. A dropped key or a clinking glass can't sneak through as a "match".
function classifyRing(peaks, sustainMs, ringFreqs) {
if (weakPeak(peaks)) return "no sharp peak";
if (sustainMs < 250) return "too short";
if (pitchDrifts(ringFreqs)) return "pitch not steady";
if (!fitsModalRatios(peaks)) return "not a coin ring";
return null; // genuine ring — go to the verdict
}
// a struck disc's modes cluster near 1 : 2.3 : 3.9
function fitsModalRatios(peaks) {
const [f1, f2, f3] = peaks.map(p => p.hz).sort();
const near = (a, b) => Math.abs(a - b) / b <= 0.25;
return near(f2 / f1, 2.3) && near(f3 / f1, 3.9);
}
3 · Ring-decay analysis
This is the part pitch alone can't tell you. Solid precious metal has very low internal damping, so it rings for a long time; a plated or base-metal fake at the right pitch dies out fast. We fit a straight line to the ring's loudness (in dB) over time — a T30-style measurement — to get a decay rate in dB/s, then compare it to the reference. A fake that fooled the pitch check gets caught here.
// least-squares slope of loudness (dB) over time
// -> ring-decay rate in dB per second
function computeDecay(ringLevels) {
const pts = ringBody(ringLevels); // drop attack + tail
const slope = linearFit(pts).slope;
return Math.round(-slope); // higher = dies faster
}
// flag a coin that rings out far faster than genuine
if (measured.decay / ref.decay > 2.5) {
warn("Dies out fast — possible plating / base metal");
}
4 · Matching & the verdict
Each of the coin's modes must land inside its expected range (a per-mode band, wider only where that mode genuinely varies between specimens). Every mode in range → MATCH. Most but not all → "tap again" for a cleaner read. Clearly outside → an honest NO MATCH.
// each mode must fall inside its expected band
const matched = ref.peaks.filter((center, i) =>
peaks.some(p =>
Math.abs(p.hz - center) / center <= band(ref, i)));
if (matched.length === ref.peaks.length) verdict = "MATCH";
else if (matched.length >= 2) verdict = "tap again"; // partial
else verdict = "NO MATCH";
Open and honest
The snippets above are simplified from the real code, which is open source under the MIT License. The ping test is one strong signal, not proof — pair it with weight and dimension checks, and for high-value items get a professional opinion. Reference ranges are our own estimates from official mint specs and public acoustic-physics modeling; see Terms & Legal for full sourcing.