the arithmetic of why no keyboard is in tune

No String Divides Evenly

A real string resists being bent, so its tenth partial is not ten times its first. Pluck one string near your microphone and this page reports B, the stiffness coefficient of that particular string, after the same estimator has recovered five known answers including a synthesised string whose stiffness is exactly zero. Then it turns your B into the cents of octave stretch it forces, which is the reason a piano tuner cannot make every octave beatless and 2:1 at once.

Your string

Pluck or strike one string, once, firmly but not violently, and let it ring. Anything with a string will do: a guitar, a ukulele, a piano, a rubber band stretched over a box. Nothing leaves your device; the recording is held in memory for the few seconds the measurement takes and is never uploaded, stored, or sent anywhere.

not measured

B is dimensionless and small, and for a plain wire it follows from three things you can measure and two you have to look up: diameter, speaking length and pitch, then the wire's density and its stiffness as a material. No tension chart and no gauge table, because tension is whatever puts that wire at that pitch. The looked-up half is where the honest uncertainty would sit: B is directly proportional to Young's modulus, and nothing here measures the modulus of anything. So this page never predicts a B from a string packet. The five specimens are synthesised at a modulus we declare, which makes their B exact by construction whatever real steel does, and your own string's B is measured from the sound it makes and not computed from its dimensions. Four of the five strings on the slate below have stiffness, and they span 3.98e-5, which is what 0.33 mm plain steel gives at 648 mm and 246.94 Hz, to 2.48e-3, which is what 1.00 mm gives at 390 mm and 261.63 Hz. The fifth has none at all, B exactly 0, and it is there to catch an estimator that finds stiffness in everything. Small numbers, and then the arithmetic runs away with them: push B to 0.01 and partial 10 no longer sits at ten times the fundamental, it sits at 14.1 times it, a full 600 cents sharp of where a harmonic ladder would put it. No tuning of any keyboard absorbs that.

What is actually being fitted

A perfectly flexible string, the one in the textbook, has partials at exactly n F. A real string is a wire, and a wire resists being bent. That bending stiffness adds a fourth-derivative term to the wave equation, and for a string hinged at both ends the answer comes out closed form:

fn = n F √(1 + B n2)

Square it and divide by n: (f_n/n)^2 = F^2 + F^2 B n^2. So plotting the squared partial-over-index against n^2 gives a straight line whose intercept is F^2 and whose slope is F^2 B. One ordinary least squares fit, in closed form, with no starting guess and no iteration. Every number this page reports about a string falls out of that one line, and the interval on B is the delta method applied to slope over intercept, the standard first-order treatment of a ratio of two correlated fitted parameters. It is not the only one: Fieller's theorem gives the exact interval and is wider when the intercept is poorly determined. Here the intercept is F^2, the best determined number in the whole fit, and the verifier recomputes Fieller's interval from the same five moments: the two agree to better than one part in 100,000 on all five shipped strings.

Before it measures you, it measures five things it already knows

Five recordings run on the slate below. Every one is synthesised by us from a closed form, so its true B is exact by construction; the four with stiffness span --, nearly two decades, and the fifth has none at all. The same estimate() runs on all five and on whatever your microphone hears, with no branch between them: it is handed samples and a sample rate and nothing else, so it has no way to know which is which. Until it recovers all five, it will not report yours.

running the slate…

The four specimens with stiffness are not generated from the equation being fitted. That would test the FFT and nothing else. They are generated by solving the clamped-end eigenvalue problem numerically, a different boundary condition whose frequencies are not of the form above, and the estimator fits the pinned-end closed form to them. (The fifth has no stiffness to solve for: at B exactly 0 both boundary conditions give the same perfectly harmonic ladder, and that is what it is built from.) The gap between the two models is real physics, and it is what the tolerances are made of. Measured against the exact clamped roots of the four stiff ones, the clamped end turns out to act almost entirely as a common factor on every partial (well fitted by 1 + (2/π)√B + (4/π2)B), which the fit absorbs into F and not into B: what is left over in B is +0.12 per cent on the most flexible of them, +0.14 per cent at its largest anywhere on the slate, and −2.5 per cent on the stiffest.

What a fake would do

The same five specimens, run through an estimator that ignores its input entirely and always returns the anchor's value with a small error bar. It sails through the anchor. It cannot pass four controls at four different values at once. This is executed on your device, right now, not promised.

The objection this page exists to answer

Here it is, at full strength, and it is not a strawman:

You did not measure stiffness. You fitted a curve with a free parameter to a decaying pluck. Any non-stationary or noisy spectrum will hand you a nonzero B. Your number is an artefact of how hard you hit the string.

That is true of the obvious implementation, and it is true for a specific mechanical reason. A hard pluck stretches the string enough to raise its own pitch, and the pitch falls back over the first half second as the amplitude decays. High partials die fastest, so inside any long analysis window they are weighted toward the early, sharp part of the recording. That weighting rises with partial number the way stiffness does, and it is close enough to fool a single fit. It is not close enough to fool the exponent test below, which is the point of having one. A perfectly flexible string, plucked hard, will report stiffness it does not have.

So here is that failure, shipped, and made operable. This specimen is the harmonic control from the slate above, same synthesis, same seed, same phases, same hiss, same decay envelope, with one thing added: 60 cents of downward pitch glide decaying with a 0.5 second time constant. Its true B is exactly 0. Press the button and watch the same estimator invent stiffness out of it.

The trap

not run

The answer is not a better fit. The answer is that stiffness is a constant of the string, and a constant does not care which half second you look at. So the analysis window is not a detail, it is the load-bearing free choice on this page, and the honest thing to do with a load-bearing free choice is to sweep it and show the reader the shape. Run the anchor through the same sweep and compare.

B against window length

not run

Each line is divided by its own value at the 0.5 second window, because the point is the shape and not the magnitude: a constant of the string is a flat line at 1, whatever its size. The raw numbers are underneath, unnormalised, so nothing hides in the normalisation.

There is a second discriminator, free, from the same fit. Stiffness says the stretch grows as n^2 and as nothing else. Refit with the exponent free, f_n = n F (1 + B n^p)^(1/2), and see where p lands. For any fixed p the model is still a straight line in n^p, so the two linear parameters profile out exactly and only p has to be searched. On the four shipped strings that have stiffness it comes back between 1.975 and 2.000, the worst of them being the stiffest string, where the clamped-versus-pinned gap is largest. On the trap it does not come back at 2 at all, and the number is printed in the trap panel above rather than described here.

And there is a third one, which costs nothing and is the only test here that needs no threshold at all. B = pi^2 E I / (T L^2): Young's modulus, the second moment of area, the tension and the length are every one of them positive, so B is positive for every string that exists. A fit whose whole interval sits below zero has therefore not measured a small stiffness, it has measured something that is not stiffness, and no tolerance had to be chosen to say so. It is reachable: a pitch that rises as it rings puts it there, which is what a bent note let back, or a peg still settling, or a new string still stretching does. This page refuses that recording instead of printing a negative stiffness coefficient. The one thing the rule must not do is catch a genuinely flexible string, whose B is small and whose interval covers zero from both sides, and the check below runs the B = 0 specimen through it at every window to make sure it does not.

A prediction you can break in ten seconds

B is proportional to 1 / (T L2): tension and speaking length, and nothing about how hard you played. Stop a string at the twelfth fret and you have halved L without changing the wire or its tension, because the twelfth fret is the halfway point by the definition of a fretboard. So B has to go up by a factor of four. Not roughly four. Four.

This is a prediction of the model this page fits, not a published measured result, and it is the reader's independent way to falsify the whole apparatus. Here it is on the shipped pair, which is the same wire at 648 mm and at 324 mm:

Now yours. Measure your open string with the button at the top, then stop it at the twelfth fret, pluck again, and press this.

Your fret test

open string: not measured yet
stopped at the twelfth fret: not measured yet

Honest edges, stated before you run it rather than after it disagrees. A real bridge is compensated by a few millimetres, so the stopped length is not exactly half. Pressing the string down behind a fret raises its tension, which pushes the ratio below four. A wound string is not a uniform wire at all and its bending stiffness is not what this geometry predicts. Any of those can move the number, and a ratio that comes back at 3.2 is a result about your instrument, not a bug on this page.

What your number does to an octave

Here is where B stops being a curiosity. An octave can be tuned by measuring a frequency ratio, or it can be tuned until it stops beating, and on a stiff string those are not the same octave. Beating is between partials, so which pair you silence is a choice. Take the obvious one first: the lower note's second partial against the upper note's first. Set those equal:

Fup √(1 + B) = 2 Flow √(1 + 4B)

so the beatless octave is not 2:1 but 2 sqrt((1+4B)/(1+B)), which is 600 log2((1+4B)/(1+B)) cents wide of it. At B = 0 that is exactly zero, which is the whole reason a perfectly flexible string was ever a good idea. On any real string it is not zero, and there is no way to have both.

And that is one choice of pair, not the only one. Silence the lower note's fourth partial against the upper note's second instead, and the same algebra with both partial numbers doubled gives 600 log2((1+16B)/(1+4B)); the sixth against the third gives 600 log2((1+36B)/(1+9B)). These are not small differences and they are not a refinement of the first answer, they are different answers to a differently posed question. For B small they come out at four times and nine times the number above, because the stretch is riding on B n^2 and n has been doubled or tripled. All three are printed below from your own B, because picking one and printing it alone would be presenting a choice as a fact. Nothing on this page tells you which pair a tuner in front of a real piano silences, because we have not read a source that would let us say.

The octave your string forces

source: --, B = --

That second line is a conditional and it is written as one on purpose. B is not a constant of an instrument, it is a constant of a string, and it changes from string to string. Take the formula at the top of this page and scale a keyboard the ideal way, halving the speaking length for every octave up and keeping the wire: L goes as 1/f0, and B = π2E d2 / (64 ρ L4 f02) collapses to B proportional to f02. Stiffness quadruples every octave you go up. The fret test above is exactly that factor of four, on your own instrument, in ten seconds. Which is why a stretch curve worked out string by string bends upward instead of running straight.

What we could not put beside it

The intention was to lay your number against the published Railsback curve, the measured stretch of real tuned pianos. We are not going to, because the papers we went to for it return 403 to us, and quoting a magnitude we have not read would be the one thing this project does not do. Here is the state of each source, and which of them we did read.

So what is on this page instead is arithmetic that anyone can check, five specimens whose answers we generated ourselves, and your own string.

Every free choice this page made

Each of these moves the answer. They are read out of the estimator's own constants object at run time, so this table cannot state a setting the code does not use.

choiceset towhy, and what it costs
analysis window The load-bearing one. Short enough that the pluck's own pitch glide has mostly decayed, long enough for the partials to separate. Longer windows fabricate stiffness, which is what the sweep above is for.
attack skip Skipped after the loudest frame, to clear the hammer or plectrum noise, which is broadband and would raise the local median that the prominence gate measures against.
pitch band Where the fundamental is looked for. A string below or above it is refused rather than measured wrongly, and until 2026-08-17 that sentence sat here without being true. The pitch finder searches a range of periods and returns the best it saw; on a string too low for the range there is no real peak anywhere in it and the search hands back the edge, which is not a detection. Fed a bass guitar's low E at 41.2 Hz, this page used to answer B = −3.7e-3 with a fundamental of about 480 Hz and a window sweep that read flat, and it did the same for a low B and for a piano's bottom octave. It now recognises a search that ran to its own edge and says so. A number that is refused is the cheapest kind of honesty on this page; the expensive kind was finding out it was not being refused.
prominence gate A candidate partial under this is dropped, not fitted. Dropping partials widens the interval; keeping noise would move the answer.
minimum partials Below this the page refuses. B lives in the high partials. Three points would already leave one residual degree of freedom, and an interval built on one degree of freedom is mostly luck; six is the fewest that gives the slope anything to stand on.
partial ceiling Above 0.45 of the sample rate a partial is unreliable. This one costs more than it looks: until 2026-08-17 this cell said fewer partials buy a wider interval, which is true and is not the whole cost. On a string whose ends are clamped rather than hinged, where this fit is an approximation, the ceiling moves the answer itself. Fitting the pinned form to the exact clamped partials of the shipped anchor returns B +0.46 per cent high over six partials and −0.37 per cent low over thirty, crossing zero in between; on the stiffest string here it runs +0.51 to −2.53. So the count is printed because it is part of the answer, not only part of the error bar. The ceiling of 30 binds below 720 Hz at every sample rate a browser offers, so it is the same choice for every reader whose string is lower than that.
zero padding Interpolation only. It adds no information and cannot beat 1/T; it removes bin snapping before the parabolic peak fit, and the final frequency is refined off the grid by Goertzel anyway.
sweep ladder The window lengths the flatness test uses. The top of it is capped by how much recording follows your pluck; where it does not fit, the page says so and drops that point rather than shortening it silently.
flatness thresholds Spread inside three sigma is called flat. Spread beyond half of B itself is called not stiffness and the number is refused. Between the two the interval printed in the readout is the widened one, stretched to cover every window the sweep ran, and the number is marked provisional. Fewer than three windows fitting at all is a fourth verdict, inconclusive, and it says so rather than borrowing one of the other three. Whose sigma is a real choice and it is the largest in the sweep, which errs permissive: a bad window raises the threshold as well as the spread, so a worse recording is judged more leniently. The stricter reading, the smallest sigma, was tried and it refuses the specimen that has to pass: on the string with B exactly 0 the spread is 2.2e-8 against a smallest sigma of 2.3e-9, so a rule built on it calls a flat line at zero contaminated. Neither reading is free, and this is the one whose failures are visible in the other tests.

What this cannot do

The check, in full

Offline, node research/inharmonicity-railsback/verify-inharmonicity-railsback.mjs regenerates all eight WAV files from their closed forms and compares sha256 against the committed bytes, loads this page's own inharm.js in a sandbox rather than reimplementing it, recomputes the five declared truths from the geometry, checks that the clamped-versus-pinned model gap is where this page says it is, requires the trap to trip the flatness test and a hard-coded stub to fail the slate, drives the one sweep verdict nothing on this page reaches, recomputes Fieller's interval against the one printed here, and asserts each refusal path fires on a signal built to trigger it. Since 2026-08-17 it also builds the seven synthesised unisons quoted above and measures what the split really costs, drives all three arms of the fret test including the one that refuses to call a band too wide to separate 4 from 3 a confirmation, and requires the negative-stiffness refusal to fire on a ladder that goes flat as it rings while leaving the string with B exactly 0 measured rather than refused. Where this page prints a number to two or three decimals, that check formats the recomputed value the same way and compares the text, rather than accepting any value inside a band. It reports 262/262 checks passed.

Separately, node scripts/check-live-sensor.mjs --only=inharmonicity-railsback launches a real Chromium, replaces the microphone with a WAV of known B, and drives this page's own capture path. It asserts that the slate arms, that a hard-coded estimator does not arm it, that the live path recovers the injected value, that a different injected value moves the reported number with it, and that refusing permission produces a structured refusal rather than a crash or a plausible number.

Sources