|
| void | prepare (double sample_rate, double lowest_hz=20.0) |
| | Sizes the delay line for the lowest note the string will play.
|
| void | set_frequency (double hz) |
| void | set_modulated_frequency (double hz) |
| | Uses the delay line's continuously interpolated reader for audio-rate pitch modulation.
|
| void | set_decay_seconds (double seconds) |
| | Time for the note to fall by 60 dB, in seconds.
|
| void | set_damping (double amount) |
| | How much faster high partials die than low ones, 0 (bright) to 1 (dark).
|
| void | set_stiffness (double amount) |
| | Inharmonicity, 0 (a true harmonic series) to 1 (bar-like stretch).
|
| void | set_pluck_position (double position) |
| | Where along the string it is excited, 0 (at the end) to 0.5 (the middle).
|
| void | set_dynamic_bandwidth_hz (double hz) |
| | Bandwidth of the excitation, in Hz — how hard the string is played.
|
| void | set_pick_direction (double amount) |
| | Direction of the pick, 0 to 1.
|
| void | reset () |
| void | pluck (bool restart=false) |
| | Excites the string.
|
| bool | is_active () const |
| double | level () const |
| SampleType | process (SampleType excitation) |
| | Advances one sample.
|
template<typename SampleType = float>
class pulp::signal::KarplusStrongT< SampleType >
A plucked string: a delay line fed back through a loss filter.
The idea is that a string is a delay. A disturbance travels to the end, reflects, and comes back, and the round trip time is the period — so a buffer of noise circulating through a delay of that length is a plucked string, with no oscillator and no harmonic series written down anywhere. The harmonics appear because only frequencies whose period divides the loop survive going round it.
Three filters in the loop turn that from a curiosity into an instrument:
- Loss. A gain below one, set from the decay time, is what makes the note stop. It is derived from the frequency rather than fixed, because the loop runs f0 times a second: the same per-trip loss gives a much shorter note at a high pitch, which is exactly what a real string does.
- Damping. A lowpass in the loop makes high partials die faster than low ones, which every real string does and an ideal delay does not.
- Stiffness. An allpass makes the loop's delay frequency-dependent, so the partials stretch slightly sharp of a true harmonic series. That inharmonicity is what separates a piano or a struck bar from a guitar, and it is audible long before it is measurable as a tuning error.
The excitation matters as much as the loop. pluck_position subtracts a delayed copy of the burst from itself, which cancels every partial with a node at that point — the reason a string plucked near the bridge is thin and bright and one plucked at the middle is round.
The fractional part of the loop length is taken by a first-order allpass rather than by interpolating between samples. Both approaches are standard, but they differ in a way that matters here: linear interpolation is itself a lowpass, and it sits inside the loop next to the damping filter, so its attenuation compounds on every round trip and shortens the note by an amount that depends on pitch. An allpass has unity magnitude at every frequency, so the decay stays the decay the caller asked for. The allpass delay is kept away from zero, where its pole and zero cancel on the unit circle and the delay becomes discontinuous.
Karplus & Strong, CMJ 7(2), 1983; the loss and stiffness extensions follow Jaffe & Smith, CMJ 7(2), 1983.
RT contract: prepare() allocates the delay line. Everything else, including pluck() and process(), allocates nothing and takes no locks.