Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::signal::TapeMachineT< SampleType > Class Template Reference

The tape machine insert. More...

#include <tape_machine.hpp>

Public Types

using Curve = TapeCurve
using Archetype = TapeArchetype

Public Member Functions

 TapeMachineT ()
void prepare (double sample_rate)
 Sizes both delay lines for their worst configured case and designs one gap-loss FIR per legal speed, so nothing in this file allocates later.
void reset ()
 Never allocates.
void set_archetype (Archetype archetype)
 Applies the full baked preset.
Archetype archetype () const noexcept
void set_speed_ips (double ips)
 Snapped to the nearest legal speed for the active archetype.
double speed_ips () const noexcept
void set_eq_curve (Curve curve)
 Independent of archetype on purpose: a Studer really could be told to run NAB, and the resulting low-end mismatch is the point.
Curve eq_curve () const noexcept
void set_bias (float bias_c)
double effective_bias () const noexcept
 The bias actually in force: what was requested plus the age table's drift, clamped.
void set_drive (float drive01)
double drive () const noexcept
void set_age (float age01)
double age () const noexcept
void set_crosstalk_db (float db)
double crosstalk_db () const noexcept
void set_companding_enabled (bool enabled) noexcept
bool companding_enabled () const noexcept
void set_print_through (float level_db, float offset_ms, bool pre_echo_enabled)
 level_db is the post-echo level before bias erasure; offset_ms is the wrap offset; pre_echo_enabled adds the lookahead and the latency that goes with it.
double print_through_db () const noexcept
double print_offset_ms () const noexcept
bool pre_echo_enabled () const noexcept
void set_mix (float wet01) noexcept
void process (const SampleType *in_l, const SampleType *in_r, SampleType *out_l, SampleType *out_r, int n_frames)
int latency_samples () const noexcept
 Reported exactly, never estimated (series law 5).
double worst_case_insertion_gain () const noexcept
 The largest gain any single stage in this insert can present, as a bound a reviewer can size headroom against.
const TapeEqSection & record_eq (int channel=0) const noexcept
const TapeEqSection & playback_eq (int channel=0) const noexcept
const std::vector< double > & gap_fir () const noexcept
 Coefficients of this instance's reproduce gap-loss FIR at the active speed, so the suite can measure its response without rendering.
double reproduce_gap_m () const noexcept
 The reproduce gap width in force, in metres.
double reproduce_alignment_db () const noexcept
 How much reproduce gain the machine is currently aligned with, in dB.
double sample_rate () const noexcept

Static Public Member Functions

static int oversampler_latency_samples () noexcept
 Group delay of the reused hysteresis stage's 4× half-band oversampling wrap, in host samples.

Static Public Attributes

static constexpr double kBiasMin = -1.0
 Bias −1 (under) .. 0 (manufacturer optimum) .. +1 (over).
static constexpr double kBiasMax = 1.0
static constexpr double kBiasDefault = 0.0
static constexpr double kOverBiasHfCutDb = 6.0
 Over-bias darkens; under-bias brightens.
static constexpr double kUnderBiasHfBoostDb = 4.0
 [design parameter] default 4 dB per unit, range 2–8.
static constexpr double kBiasShelfHz = 8000.0
 [design parameter] default 8000 Hz, range 5000–10000.
static constexpr double kBiasDistK = 0.6
 A parabola in bias, centred on optimum: minimum distortion at 0, rising either side.
static constexpr double kBiasErasureDb = 3.0
 Over-bias erases more of whatever has already printed through — the documented direction.
static constexpr double kDriveDbMax = 24.0
 Drive maps [0, 1] onto this dB span, which is then multiplied by the bias parabola.
static constexpr double kDriveDefault = 0.3
static constexpr double kCrosstalkDbMin = -45.0
 Crosstalk bounds are the union of the per-archetype ranges.
static constexpr double kCrosstalkDbMax = -22.0
static constexpr double kCrosstalkTiltHz = 5000.0
 Inter-channel coupling worsens with frequency (documented general behaviour of inductive/capacitive coupling), so the leak is tilted up above a corner and shelf-limited so it can never exceed crosstalk_db + kCrosstalkTiltMaxDb at any frequency.
static constexpr double kCrosstalkTiltMaxDb = 6.0
 [design parameter] default +6 dB total, range 4–9 dB.
static constexpr double kPrintThroughDbMin = -80.0
 Print-through level bounds.
static constexpr double kPrintThroughDbMax = -38.0
static constexpr double kPrintThroughOffsetMsDefault = 700.0
 Wrap offset between the printing layer and the printed one.
static constexpr double kPrintThroughOffsetMsMin = 200.0
static constexpr double kPrintThroughOffsetMsMax = 2000.0
static constexpr double kPreEchoAsymmetryDb = 4.0
 Pre-echo is documented as the weaker of the two in the tails-out storage convention professionals use.
static constexpr double kAlignmentCeilingDb = 24.0
 Ceiling on the reproduce alignment gain, in dB.
static constexpr double kInstabilityNominalMs = 2.0
 Nominal read offset of the wow/flutter delay line, in ms.

Detailed Description

template<typename SampleType = float>
class pulp::signal::TapeMachineT< SampleType >

The tape machine insert.

Member Typedef Documentation

◆ Archetype

template<typename SampleType = float>
using pulp::signal::TapeMachineT< SampleType >::Archetype = TapeArchetype

◆ Curve

template<typename SampleType = float>
using pulp::signal::TapeMachineT< SampleType >::Curve = TapeCurve

Constructor & Destructor Documentation

◆ TapeMachineT()

template<typename SampleType = float>
pulp::signal::TapeMachineT< SampleType >::TapeMachineT ( )
inline

Member Function Documentation

◆ age()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::age ( ) const
inlinenoexcept

◆ archetype()

template<typename SampleType = float>
Archetype pulp::signal::TapeMachineT< SampleType >::archetype ( ) const
inlinenoexcept

◆ companding_enabled()

template<typename SampleType = float>
bool pulp::signal::TapeMachineT< SampleType >::companding_enabled ( ) const
inlinenoexcept

◆ crosstalk_db()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::crosstalk_db ( ) const
inlinenoexcept

◆ drive()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::drive ( ) const
inlinenoexcept

◆ effective_bias()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::effective_bias ( ) const
inlinenoexcept

The bias actually in force: what was requested plus the age table's drift, clamped.

Exposed so a caller can show the effective value rather than the requested one.

◆ eq_curve()

template<typename SampleType = float>
Curve pulp::signal::TapeMachineT< SampleType >::eq_curve ( ) const
inlinenoexcept

◆ gap_fir()

template<typename SampleType = float>
const std::vector< double > & pulp::signal::TapeMachineT< SampleType >::gap_fir ( ) const
inlinenoexcept

Coefficients of this instance's reproduce gap-loss FIR at the active speed, so the suite can measure its response without rendering.

◆ latency_samples()

template<typename SampleType = float>
int pulp::signal::TapeMachineT< SampleType >::latency_samples ( ) const
inlinenoexcept

Reported exactly, never estimated (series law 5).

The sum of the two constant delays this block cannot avoid — the reused oversampling wrap and the wow/flutter line's nominal read offset — plus the print-through offset when pre-echo is enabled. Constant across archetype, curve, speed and age by construction.

◆ oversampler_latency_samples()

template<typename SampleType = float>
int pulp::signal::TapeMachineT< SampleType >::oversampler_latency_samples ( )
inlinestaticnoexcept

Group delay of the reused hysteresis stage's 4× half-band oversampling wrap, in host samples.

Rate-independent, inherited, and reported.

◆ playback_eq()

template<typename SampleType = float>
const TapeEqSection & pulp::signal::TapeMachineT< SampleType >::playback_eq ( int channel = 0) const
inlinenoexcept

◆ pre_echo_enabled()

template<typename SampleType = float>
bool pulp::signal::TapeMachineT< SampleType >::pre_echo_enabled ( ) const
inlinenoexcept

◆ prepare()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::prepare ( double sample_rate)
inline

Sizes both delay lines for their worst configured case and designs one gap-loss FIR per legal speed, so nothing in this file allocates later.

May allocate.

◆ print_offset_ms()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::print_offset_ms ( ) const
inlinenoexcept

◆ print_through_db()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::print_through_db ( ) const
inlinenoexcept

◆ process()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::process ( const SampleType * in_l,
const SampleType * in_r,
SampleType * out_l,
SampleType * out_r,
int n_frames )
inline

◆ record_eq()

template<typename SampleType = float>
const TapeEqSection & pulp::signal::TapeMachineT< SampleType >::record_eq ( int channel = 0) const
inlinenoexcept

◆ reproduce_alignment_db()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::reproduce_alignment_db ( ) const
inlinenoexcept

How much reproduce gain the machine is currently aligned with, in dB.

A real deck is aligned by playing a calibration tape and setting the reproduce amplifier so the alignment tone reads 0 VU. Without doing the same thing here this insert would be unusable, and the number says why: the reused Wallace model at cassette speed and mid age loses 19.7 dB at 1 kHz (spacing 12 µm at 4.7625 cm/s gives −13.7 dB, coating thickness another −6.0 dB). That model was tuned as delay-loop character, where per-repeat darkening IS the effect and absolute level is set by the feedback control. An insert has no such control, 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 kEqReferenceHz is computed from the SHIPPED filters actually in the path — the reused tier's fitted IIR tilt, its gap FIR, and this module's own reproduce-gap FIR — and undone. Not measured by rendering, and not a fudge factor: it is the closed-form magnitude of the three stages at one frequency.

The gain sits AFTER the loss stage, so it lifts the reused hiss along with the programme. That is correct rather than unfortunate: a real reproduce amplifier amplifies head noise too, which is exactly why companding exists.

◆ reproduce_gap_m()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::reproduce_gap_m ( ) const
inlinenoexcept

The reproduce gap width in force, in metres.

◆ reset()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::reset ( )
inline

Never allocates.

A zero-initialised instance is already in this state apart from the constructor's archetype preset.

◆ sample_rate()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::sample_rate ( ) const
inlinenoexcept

◆ set_age()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_age ( float age01)
inline

◆ set_archetype()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_archetype ( Archetype archetype)
inline

Applies the full baked preset.

CONTROL THREAD ONLY — see the header's RT contract; a speed change reaches the reused tier's FIR redesign.

◆ set_bias()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_bias ( float bias_c)
inline

◆ set_companding_enabled()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_companding_enabled ( bool enabled)
inlinenoexcept

◆ set_crosstalk_db()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_crosstalk_db ( float db)
inline

◆ set_drive()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_drive ( float drive01)
inline

◆ set_eq_curve()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_eq_curve ( Curve curve)
inline

Independent of archetype on purpose: a Studer really could be told to run NAB, and the resulting low-end mismatch is the point.

◆ set_mix()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_mix ( float wet01)
inlinenoexcept

◆ set_print_through()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_print_through ( float level_db,
float offset_ms,
bool pre_echo_enabled )
inline

level_db is the post-echo level before bias erasure; offset_ms is the wrap offset; pre_echo_enabled adds the lookahead and the latency that goes with it.

◆ set_speed_ips()

template<typename SampleType = float>
void pulp::signal::TapeMachineT< SampleType >::set_speed_ips ( double ips)
inline

Snapped to the nearest legal speed for the active archetype.

CONTROL THREAD ONLY — see the header's RT contract.

◆ speed_ips()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::speed_ips ( ) const
inlinenoexcept

◆ worst_case_insertion_gain()

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::worst_case_insertion_gain ( ) const
inlinenoexcept

The largest gain any single stage in this insert can present, as a bound a reviewer can size headroom against.

There is no feedback path here, so Forge's worst_case_gain field does not apply and the catalog row says so. This is the useful number instead, and it is measured on the REALIZED sections over the whole (curve × frequency) grid to Nyquist — not on the analog prototype, which is where an earlier version of this got it wrong by 8 dB: the prototype has no equivalent of the damped inverse's behaviour at Nyquist, so a bound derived from it is not a bound.

It is a large number, and the reason is structural rather than a defect to tune away. A reproduce network for a curve with no bass shelf is (1 + s·t₂), a differentiator, so its gain climbs 6 dB per octave to the top of the band and its ceiling therefore rises with sample rate. The programme never sees it: the record network is that section's exact reciprocal, so anything arriving at the reproduce network has already been attenuated by the same amount, and the pair is unity. What DOES see it is whatever was injected in between — the reused hiss generator, which is lowpassed at 4 kHz and so has almost nothing up there. The suite asserts the consequence directly by bounding the module's output on silence and on a full-scale input.

Member Data Documentation

◆ kAlignmentCeilingDb

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kAlignmentCeilingDb = 24.0
staticconstexpr

Ceiling on the reproduce alignment gain, in dB.

See reproduce_alignment_db() for what alignment is and why it exists; this is the guard that keeps a pathological configuration from turning the reused hiss generator into the loudest thing in the mix. [design parameter] default +24 dB, range 12–36 dB.

◆ kBiasDefault

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kBiasDefault = 0.0
staticconstexpr

◆ kBiasDistK

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kBiasDistK = 0.6
staticconstexpr

A parabola in bias, centred on optimum: minimum distortion at 0, rising either side.

Multiplies the drive fed to the reused hysteresis stage. [design parameter] default 0.6, range 0.3–1.0.

◆ kBiasErasureDb

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kBiasErasureDb = 3.0
staticconstexpr

Over-bias erases more of whatever has already printed through — the documented direction.

Subtracted from the print-through level. [design parameter] default 3 dB per unit, range 1–6.

◆ kBiasMax

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kBiasMax = 1.0
staticconstexpr

◆ kBiasMin

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kBiasMin = -1.0
staticconstexpr

Bias −1 (under) .. 0 (manufacturer optimum) .. +1 (over).

◆ kBiasShelfHz

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kBiasShelfHz = 8000.0
staticconstexpr

[design parameter] default 8000 Hz, range 5000–10000.

◆ kCrosstalkDbMax

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kCrosstalkDbMax = -22.0
staticconstexpr

◆ kCrosstalkDbMin

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kCrosstalkDbMin = -45.0
staticconstexpr

Crosstalk bounds are the union of the per-archetype ranges.

◆ kCrosstalkTiltHz

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kCrosstalkTiltHz = 5000.0
staticconstexpr

Inter-channel coupling worsens with frequency (documented general behaviour of inductive/capacitive coupling), so the leak is tilted up above a corner and shelf-limited so it can never exceed crosstalk_db + kCrosstalkTiltMaxDb at any frequency.

[design parameter] corner default 5000 Hz, range 3000–8000 Hz.

◆ kCrosstalkTiltMaxDb

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kCrosstalkTiltMaxDb = 6.0
staticconstexpr

[design parameter] default +6 dB total, range 4–9 dB.

The realized first-order shelf reaches +5.53 dB above its 1 kHz reference and rises at a measured ~1.9 dB/octave through its transition, inside the source spec's stated 2–6 dB/octave window at the shallow end.

◆ kDriveDbMax

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kDriveDbMax = 24.0
staticconstexpr

Drive maps [0, 1] onto this dB span, which is then multiplied by the bias parabola.

The floor is 0 dB so drive = 0 leaves the hysteresis stage at unity input — the condition an EQ measurement needs. [design parameter] default span 0 .. +24 dB, range 12–36 dB at the top.

◆ kDriveDefault

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kDriveDefault = 0.3
staticconstexpr

◆ kInstabilityNominalMs

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kInstabilityNominalMs = 2.0
staticconstexpr

Nominal read offset of the wow/flutter delay line, in ms.

Must exceed the deepest modulation the reused age table can ask for — 1.2 ms of wow plus 0.4 ms × (1 + 0.5 + 0.25) of the flutter harmonic stack = 1.9 ms — and is held CONSTANT so latency_samples() never moves with archetype, curve, speed or age. [design parameter] default 2.0 ms, range 1.9–5.0 ms.

◆ kOverBiasHfCutDb

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kOverBiasHfCutDb = 6.0
staticconstexpr

Over-bias darkens; under-bias brightens.

Both are documented DIRECTIONS with no citable closed-form transfer curve — commercial treatments reach for full Preisach/Stoner-Wohlfarth simulation, which is out of scope for an insert. Honest gap: these three constants encode the direction and rough magnitude of a documented trade, not any machine's measured curve. [design parameter] default 6 dB per unit, range 3–10.

◆ kPreEchoAsymmetryDb

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kPreEchoAsymmetryDb = 4.0
staticconstexpr

Pre-echo is documented as the weaker of the two in the tails-out storage convention professionals use.

[design parameter] default 4 dB, range 2–8.

◆ kPrintThroughDbMax

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kPrintThroughDbMax = -38.0
staticconstexpr

◆ kPrintThroughDbMin

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kPrintThroughDbMin = -80.0
staticconstexpr

Print-through level bounds.

These ARE the age table's extremes, so a baked parameter cannot ask for a level the age axis cannot produce.

◆ kPrintThroughOffsetMsDefault

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kPrintThroughOffsetMsDefault = 700.0
staticconstexpr

Wrap offset between the printing layer and the printed one.

The real offset depends on reel radius, tape thickness and wind position and genuinely varies through a reel; a single representative constant is an honest simplification, not a measured value. [design parameter] default 700 ms, range 200–2000 ms.

◆ kPrintThroughOffsetMsMax

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kPrintThroughOffsetMsMax = 2000.0
staticconstexpr

◆ kPrintThroughOffsetMsMin

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kPrintThroughOffsetMsMin = 200.0
staticconstexpr

◆ kUnderBiasHfBoostDb

template<typename SampleType = float>
double pulp::signal::TapeMachineT< SampleType >::kUnderBiasHfBoostDb = 4.0
staticconstexpr

[design parameter] default 4 dB per unit, range 2–8.


The documentation for this class was generated from the following file: