A tape machine as an INSERT — record chain in, playback chain out, no delay and no feedback anywhere in it. More...
#include <pulp/signal/tape_machine_components.hpp>#include <pulp/signal/character_delay/tape_physical.hpp>#include <pulp/signal/ballistics_filter.hpp>#include <pulp/signal/character_delay/primitives.hpp>#include <pulp/signal/character_delay/tables.hpp>#include <pulp/signal/character_delay/tape.hpp>#include <pulp/signal/character_delay/tape_loss.hpp>#include <pulp/signal/denormal.hpp>#include <pulp/signal/units.hpp>#include <algorithm>#include <array>#include <cmath>#include <complex>#include <cstddef>#include <cstdint>#include <vector>Go to the source code of this file.
Classes | |
| class | pulp::signal::TapeMachineT< SampleType > |
| The tape machine insert. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::signal |
Typedefs | |
| using | pulp::signal::TapeMachine = TapeMachineT<float> |
| using | pulp::signal::TapeMachine64 = TapeMachineT<double> |
| using | pulp::signal::TapeCompander = TapeCompanderT<float> |
A tape machine as an INSERT — record chain in, playback chain out, no delay and no feedback anywhere in it.
The catalog already models tape as a thing that happens inside a loop: the character delay's physical tier (character_delay/tape_physical.hpp) runs Jiles-Atherton magnetization and Wallace playback loss once per recirculation so colouration compounds per repeat. This block models the other half of what a deck does — the part that is a system rather than a saturator — and calls into that tier rather than restating any of its physics.
What is genuinely new here, and why a saturator cannot fake it:
The EQ is standard-defined, not a tone knob. Every recording standard closes the raw flux-transfer gap with a PAIR of time constants — a bass turnover and a treble turnover — realized as complementary record and playback networks. NAB defines a bass shelf at every reel speed it covers; the documented CCIR/IEC reel data has only the treble turnover. That single structural difference is the whole "thin European tape" folklore: a tape recorded IEC and replayed NAB diverges in the low end because one curve has a bass network and the other does not. set_eq_curve is deliberately independent of set_archetype so a Studer preset can be told to run NAB and reproduce that mismatch on purpose.
Bias is one control that moves three things at once. AC bias linearizes the hysteresis loop at the operating point, and pushing past the manufacturer's optimum buys lower distortion at the cost of high-frequency output while also erasing more of whatever has already printed through. "Hotter bias, darker and cleaner, less pre-echo" is the entire practical mental model, and it is three coupled consequences of one number — which is exactly what a plain drive knob cannot express.
Gap loss is an extinction, not a roll-off. As the recorded wavelength approaches the reproduce head's physical gap width, opposing flux polarities inside the gap cancel: a sinc with a genuine null at λ = g, not a corner frequency. It is modelled separately from the reused Wallace filter because it is a different mechanism with its own published closed form.
The compander is a real 2:1/1:2 pair. dbx-family noise reduction halves the programme's dynamic range in dB before it meets tape noise and doubles it back afterwards, so noise that entered in between comes back attenuated in proportion to how quiet the programme was. That is a level-dependent process; no fixed EQ reproduces it.
The prototype network is normalized to unity at 1 kHz. §4.1's reproduce prototype H(s) = (1 + s·t₁)/(1 + s·t₂) is stated unnormalized, and unnormalized it is a +25.6 dB gain at 1 kHz and +36.1 dB asymptotically for NAB (t₁/t₂ = 3180/50). Inserting that raw would mean the saturation stage sees a 1 kHz tone 25.6 dB off its intended operating point — a bias test measuring distortion at 1 kHz would be measuring the EQ instead. Every published NAB/IEC/cassette curve is quoted in dB relative to 1 kHz, which is also the tape-alignment reference frequency, so normalizing there is the convention rather than a liberty. kEqReferenceHz fixes it once.
H is the RECORD network; playback is its exact inverse. The source document says both things — its diagram and its rationale label the record side "pre-emphasis" (highs pushed harder into the nonlinearity), while one sentence calls the record side the reciprocal. They cannot both hold, because (1 + s·t₁)/(1 + s·t₂) with t₁ ≫ t₂ RISES with frequency. This header resolves it toward pre-emphasis-on-record, for three reasons: it is the only assignment under which the stated rationale is true; it matches the reused tape tier's own internal emphasis pair; and it is what the physics says — the resulting playback network is −10.04 dB at 10 kHz relative to 1 kHz, against −9.94 dB for the fully-derived NAB reproduce characteristic (head differentiation compensated). The other assignment gets that sign backwards.
The playback network's degenerate pole is damped, not ignored. When a curve has no bass shelf (t₁ = 0: IEC/CCIR reel at every speed, and NAB at 30 ips) the record network is a plain one-pole whose bilinear form carries a zero at Nyquist. Its exact inverse therefore carries a POLE at Nyquist — marginally stable, unbounded gain on whatever hiss and saturation product lives up there. kEqInversePoleDamping pulls that pole just inside the unit circle. The cost is measured across every degenerate case at three sample rates rather than assumed: 0.0396 dB of worst-case round-trip ripple over 20 Hz–15 kHz — seven times inside the ±0.3 dB flatness criterion — in exchange for 14 dB less gain at Nyquist than the obvious lighter setting. Damping is applied ONLY to the degenerate case; NAB's and cassette's inverses are proper first-order sections and are left exact.
The insert is aligned, the way a real machine is. The reused Wallace loss model is tuned as delay-loop character, where per-repeat darkening IS the effect and absolute level is set by the feedback control. Dropped into an insert unchanged it loses 19.7 dB at 1 kHz on the cassette preset at mid age, and a twenty-decibel hole at 1 kHz is not tape character, it is a broken insert. So the loss the playback chain will apply at the alignment frequency is computed in closed form from the filters actually in the path and undone — which is precisely what setting reproduce gain against a calibration tape does on real hardware. reproduce_alignment_db() reports it, and kAlignmentCeilingDb bounds it.
This block does NOT report zero latency, and cannot. Two of its inherited stages are constant delays:
Minimum-phase and IIR group delay (the gap-loss FIR, the EQ sections, the Wallace tilt) is colouration and is NOT reported, the same convention the rest of the catalog uses for a biquad. latency_samples() reports the sum of the two constant delays, plus the print-through offset when pre-echo is enabled, and the acceptance suite asserts it against a MEASURED impulse position rather than trusting the arithmetic.
This is a pure insert: record chain, then playback chain, both feed-forward. Forge's worst_case_gain field is therefore not applicable, and the catalog header says so explicitly rather than omitting it. worst_case_insertion_gain() exists anyway, as an asserted bound on the largest gain any single stage can present, because "not applicable" is a worse answer than a number when a reviewer is sizing headroom.
RT contract: prepare(sample_rate) may allocate — it sizes the print-through line for the worst configured offset, the wow/flutter line for the worst configured modulation depth, and designs one gap-loss FIR per legal speed. set_bias, set_drive, set_age, set_eq_curve, set_crosstalk_db, set_companding_enabled, set_print_through, set_mix, process() and reset() never allocate, never lock, never perform I/O, and are safe on the audio thread. set_speed_ips and set_archetype are the two exceptions and are CONTROL-THREAD ONLY: the reused physical tier redesigns its own speed-dependent minimum-phase loss FIR when the tape speed changes, which allocates. That is inherited behaviour, it is asserted by the suite rather than assumed, and this block pre-designs its OWN speed-dependent filters at prepare() so nothing in this file contributes to it. All state is POD; zero-init is a valid fresh instance, and reset() restores it.
References. Reproduce gap-loss sinc with its extinction null: W. K. Westmijze, Studies on Magnetic Recording, Philips Research Reports 8, 1953. Speed ladder, NAB/IEC-CCIR time constants at 30 ips, IEC/CCIR at 7.5/15 ips, and the ≥40 dB 100 Hz–12 kHz crosstalk figure: Studer A800 multichannel recorder technical data. Cassette equalization and the fixed 4.75 cm/s speed: IEC 60094 cassette parts; Richard Hess, "Cassette Equalization: The 4 dB Ambiguity at 16 kHz". NAB 3180/50 µs reel constants: documented NAB reel-to-reel reproduce practice, corroborated in secondary literature — honest gap, the exact NAB document title/year could not be reconfirmed against a live source, so the constants are carried on corroboration rather than on a citation this header would be over-claiming. dbx-family 2:1/1:2 linear-dB companding: published concept only, no trademarked curve and no proprietary time constant. Print-through mechanism and the tails-out pre/post echo asymmetry: general documented phenomenon. Bilinear transform: textbook digital filter design. Jiles-Atherton hysteresis, Wallace loss, head bump, wow/flutter and the age table are REUSED from character_delay/, which carries their citations.