PianoProcessor
A parametric physical model of a piano.
| Declared in | src/libs/libbahn/bahn/tracker/processor/PianoProcessor.hpp |
| Inherits | StringInstrumentProcessor |
| Group | bahn |
Detailed description
Physics only. The voice pool, the body, the control smoothing, the output chain, the rate retarget and the quality budget all live in StringInstrumentProcessor, because a guitar and a violin need every one of them and the piano is simply the first instrument to have needed them.
What the model carries
- Stiff strings. Partials at
n*f0*sqrt(1 + B*n^2), not atn*f0. This is the single most audible thing about a piano. - Hammer. Strike position combs the spectrum; felt hardness and velocity set how far up the series the blow reaches. Loud is bright because the felt compresses and the contact shortens, not because a filter was opened.
- Unison groups. One string in the bass, two in the middle, three in the treble, slightly detuned. The detuning is not a chorus effect: it is what produces the piano's two-stage decay, a prompt sound that falls quickly and an aftersound that lingers as the strings trade energy.
- Soundboard. An inharmonic modal body, because a real board is an irregular chamfered plate and its modes are nothing like a harmonic series.
- Dampers. Note-off drops the felt. The sustain pedal lifts every damper at once, which is also what lets struck notes ring into the strings that were never played.
The scale parameter
Scale resizes the instrument while it is sounding, from roughly a spinet to a concert grand, and it does so coherently rather than by shifting pitch:
- Inharmonicity falls as
1/scale^2— a longer string is less stiffness-dominated, which is why a concert grand sounds pure where a spinet sounds hard and bright. - Decay lengthens with
scale— more string mass, more stored energy. - The soundboard's modes fall as
1/scale— a bigger plate resonates lower.
Pitch is deliberately NOT scaled. A bigger piano is not a lower piano; it is a piano whose strings are longer at the same tuning. That is the whole point of the parameter, and getting it wrong would make it a pitch knob.
Every one of those targets is smoothed through a Lag and pushed into rotation-form resonators on each block, so the sweep is continuous and no step reaches a coefficient. See StiffString for why that form was chosen.
Member function documentation
baseDecayFor
[override virtual protected] ProcessorSample PianoProcessor::baseDecayFor(int note) const
Decay shortens as pitch rises, steeply.
Returns the T60 in seconds for the MIDI note note, before Scale moves it.
A bottom A can ring for the better part of a minute; a top C is gone in well under a second. The curve here is exponential in semitones, which tracks measured instruments closely enough for the ear.
baseInharmonicity
[override virtual protected] ProcessorSample PianoProcessor::baseInharmonicity(int note) const
Inharmonicity rises steeply towards the treble.
Returns the inharmonicity coefficient for the MIDI note note, before Scale divides it. A note outside the compass takes the value of the nearest end.
A bass string is long, heavy and wound, so its stiffness barely matters: B is around 1e-4. A top treble string is a couple of inches of thick plain wire, where stiffness dominates and B approaches 1e-2 — two orders of magnitude. This spread is why the treble of a piano sounds hard and "stretched" while the bass sounds nearly harmonic, and why tuners stretch the octaves at both ends.
bodyTable
[override virtual protected] std::vector<ModalBody<double>::ModeSpec> PianoProcessor::bodyTable() const
The soundboard, with its gains stated honestly.
Deliberately inharmonic: a piano's board is an irregular, chamfered, ribbed plate, and its modes have no simple relationship. The ratios avoid small integer coincidences, because any such coincidence reads immediately as a drum or a tube rather than a board.
These gains look extreme and they are the instrument as it was voiced. The table this replaces read 0.50 down to 0.03 and was scaled by (1-r), which was believed to give each mode about unity peak gain. It does not. (1-r) accounts for ONE of a mode's two pole factors; the other, |1 - r*e^{-2jw}|, is small at low frequency and near 1 at high, so the old form left a residual gain of 60.5x at 58 Hz and 1.5x at 2411 Hz — 56 dB of bass tilt that nothing in the code admitted to.
Every downstream constant — the body tilt filter, the string and board mix, the output gain — was then tuned by ear and by measurement AGAINST that tilt. So the tilt is not a bug in the sound. It is the sound, and only the explanation was wrong.
The gains are therefore the old gain times the residual it was getting anyway, computed at 44100 with the default BodyDecay. At that reference the instrument is unchanged; away from it the board is now correct rather than reproducing an error that happened to vary with rate and decay. Re-voicing the board is a deliberate act for the owner, not a side effect of fixing the arithmetic.
Ordered loudest first, so truncating the table is the quality budget.
See specs/physical-instruments §PIR-14.
damperT60
[override virtual protected] ProcessorSample PianoProcessor::damperT60(const StringInstrumentProcessor<StiffString<double>>::Voice &v, ProcessorSample freeT60) const
The damper.
Returns the T60 in seconds that the voice v has now. freeT60 is the T60 of the note with the damper lifted.
A keyboard releases a key and the felt drops. A tracker has no key-release event unless the user writes NOTE_OFF into the row, which a sequence rarely contains — so held stayed true for the life of every note. That made held || pedal always true, which meant the pedal control could not change anything AND every note rang for its full multi-second decay with no damper on it. Reported from real playing as "the pedal did nothing" and "it rings no matter what": one cause, two symptoms.
So the pedal drives the damper directly rather than only overriding a key-release that never arrives. Up, the felt rests on the strings and the ring is roughly halved; down, the string is free and keeps its full T60. An explicit NOTE_OFF still stops a note hard.
exciteVoice
[override virtual protected] void PianoProcessor::exciteVoice(StringInstrumentProcessor<StiffString<double>>::Voice &v, ProcessorSample velocity)
The hammer.
Strikes every string of the voice v. velocity sets the loudness and, through the felt, the brightness.
Strike position, felt hardness and velocity, applied to every string of the unison group. Called by the base only after the voice has been tuned, which is a correctness requirement rather than an ordering preference — see StringInstrumentProcessor::tuneVoice.
unisonCountFor
[override virtual protected] int PianoProcessor::unisonCountFor(int note) const
The bass is singly or doubly strung, the treble is in threes.
Returns the number of strings that sound the MIDI note note.
The boundaries are where real instruments put them, and they matter to the sound: a single wound bass string has no unison partner to trade energy with, so it has no aftersound to speak of, while a treble tricord does.