Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_tape_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Tape machine — bake-layer catalog node.
4//
5// Wraps pulp::signal::TapeMachineT as a lowerable custom node, following the
6// character delay's per-variant factory shape: one factory, three registered
7// realizations with stable per-archetype type ids.
8//
9// STEREO, two ports in and two out — unlike the saturator's mono node, and not
10// for symmetry's sake. This module has genuine cross-channel state: the
11// crosstalk stage feeds each channel a tilted copy of the other, which is a
12// headline specification on a professional machine and the thing two instanced
13// mono nodes could not reproduce at all.
14//
15// ── What is a realization here, and what is a parameter ───────────────────
16//
17// Three things are fixed at registration rather than injected, each for a
18// reason that would be a bug if ignored:
19//
20// * **The archetype**, obviously — it is what the three registrations ARE.
21//
22// * **The tape speed.** It looks like the most natural knob on the whole
23// machine, and it cannot be one. Changing speed reaches the reused physical
24// tier's speed-dependent minimum-phase loss FIR, which is REDESIGNED — a
25// cepstral fold over an FFT — and therefore allocates. A baked parameter is
26// applied on the audio thread; a baked parameter that allocates is a
27// dropout waiting for a busy session. It is also barely a control: the
28// cassette archetype has exactly one legal speed, and a knob with one
29// position is not a knob.
30//
31// * **`pre_echo_enabled`.** It changes `latency_samples()`, and a node whose
32// reported latency moves under the audio thread breaks the host's delay
33// compensation — the same rule that makes the saturator's alias policy a
34// realization rather than a param.
35//
36// The EQ curve MAY be a parameter, which is the one case where this file departs
37// from the saturator's "enums are realizations" instinct. It departs because on
38// the machines being modelled the EQ standard is a FRONT-PANEL SWITCH — the
39// A800's master EQ selector, the cassette deck's Type I/II tape-type selector —
40// and because unlike a waveshaper shape it is allocation-free, latency-neutral,
41// and confined to four coefficients of a first-order section whose entire state
42// is one sample. It is omitted entirely when the archetype has only one legal
43// curve; a one-position selector is implementation detail, not a capability.
44//
45// ── Control parameters are applied per BLOCK, not per sample ──────────────
46//
47// The saturator's node writes every setter once per sample, because its setters
48// recompute three doubles. This one must not, and the ratio says why: `set_age`
49// re-derives the reused loss cascade's parameters, redesigns a head-bump bell,
50// and recomputes the reproduce alignment gain — which evaluates two FIR
51// magnitudes over up to 385 taps each. That is on the order of a thousand
52// floating-point operations to prepare ONE sample of audio, and it would be
53// spent again on the next sample, and the next.
54//
55// So each declared parameter is read once per block, at offset 0, and applied
56// only if it changed. Nothing musical is lost: these are machine-alignment
57// controls — how worn the tape is, which EQ standard is selected, how much
58// crosstalk the head stack has — not gestures. Sample-accurate automation of
59// "how many hours are on this machine" is not a thing anyone wants.
60//
61// ── `worst_case_gain` is NOT APPLICABLE, deliberately ─────────────────────
62//
63// Stated rather than omitted, so a reviewer does not read an absent field as an
64// oversight. This design has no feedback path: it is a pure insert, record
65// chain then playback chain, both feed-forward. Series law 8's requirement —
66// that a registry's `worst_case_gain` cite a bound the module's own suite
67// asserts — has no subject here. `tape_machine_insertion_gain_bound()` supplies
68// the number a reviewer actually wants instead, and it is asserted by the
69// suite rather than estimated.
70
71#include <pulp/host/detail/forge_realization_identity.hpp>
74
76
77#include <algorithm>
78#include <cmath>
79#include <cstddef>
80#include <cstdint>
81
83
86
87// ── Stable type ids ───────────────────────────────────────────────────────
88inline constexpr const char* kAmpexTypeId = "tape.ampex350_440";
89inline constexpr const char* kStuderTypeId = "tape.studer_a800";
90inline constexpr const char* kCassetteTypeId = "tape.cassette";
91
92// ── Injectable param ids ──────────────────────────────────────────────────
93// Node-local; the framework namespaces per node so two nodes never collide.
94inline constexpr state::ParamID kEqCurve = 1; // enum index, stepped
95inline constexpr state::ParamID kBias = 2; // −1 .. +1
96inline constexpr state::ParamID kDrive = 3; // 0 .. 1
97inline constexpr state::ParamID kAge = 4; // 0 .. 1
98inline constexpr state::ParamID kCrosstalkDb = 5; // dB, negative
99inline constexpr state::ParamID kCompanding = 6; // bool
100inline constexpr state::ParamID kPrintThroughDb = 7; // dB, negative
101inline constexpr state::ParamID kPrintOffsetMs = 8; // ms
102inline constexpr state::ParamID kMix = 9; // 0 .. 1
103
106inline constexpr bool kWorstCaseGainApplicable = false;
107
114 Curve min_curve = Curve::nab;
115 Curve max_curve = Curve::nab;
116};
117
118inline CurveRange curve_range(Archetype archetype) noexcept {
119 switch (archetype) {
120 case Archetype::studer_a800:
121 return {Curve::nab, Curve::iec_ccir};
122 case Archetype::cassette_deck:
123 return {Curve::cassette_type1, Curve::cassette_type2};
124 case Archetype::ampex_350_440:
125 default:
126 return {Curve::nab, Curve::nab};
127 }
128}
129
130inline const char* tape_type_id(Archetype archetype) noexcept {
131 switch (archetype) {
132 case Archetype::studer_a800:
133 return kStuderTypeId;
134 case Archetype::cassette_deck:
135 return kCassetteTypeId;
136 case Archetype::ampex_350_440:
137 default:
138 return kAmpexTypeId;
139 }
140}
141
142inline const char* tape_default_name(Archetype archetype) noexcept {
143 switch (archetype) {
144 case Archetype::studer_a800:
145 return "Tape Machine (Studer A800-class)";
146 case Archetype::cassette_deck:
147 return "Tape Machine (Cassette Deck)";
148 case Archetype::ampex_350_440:
149 default:
150 return "Tape Machine (Ampex 350/440-class)";
151 }
152}
153
154inline Archetype normalized_archetype(Archetype archetype) noexcept {
155 switch (archetype) {
156 case Archetype::ampex_350_440:
157 case Archetype::studer_a800:
158 case Archetype::cassette_deck:
159 return archetype;
160 default:
161 return Archetype::ampex_350_440;
162 }
163}
164
176float tape_machine_insertion_gain_bound(Archetype archetype, double sample_rate);
177
180
181 // Last applied values, so a block that changes nothing costs nothing. The
182 // sentinels are outside every declared range on purpose: the first block
183 // must apply, and "outside the range" is a cheaper way to say that than a
184 // parallel set of dirty flags.
185 float curve = -1.0f;
186 float bias = -99.0f;
187 float drive = -99.0f;
188 float age = -99.0f;
189 float crosstalk_db = 0.0f;
190 float companding = -1.0f;
191 float print_db = 0.0f;
192 float print_offset_ms = -1.0f;
193 float mix = -1.0f;
194
197 void invalidate_cache() noexcept {
198 curve = -1.0f;
199 bias = -99.0f;
200 drive = -99.0f;
201 age = -99.0f;
202 crosstalk_db = 0.0f;
203 companding = -1.0f;
204 print_db = 0.0f;
205 print_offset_ms = -1.0f;
206 mix = -1.0f;
207 }
208};
209
215CustomNodeType make_tape_machine_node(Archetype archetype, double speed_ips = 0.0,
216 bool pre_echo_enabled = false);
217
219
220} // namespace pulp::host::tape
Definition forge_tape_catalog.hpp:82
constexpr bool kWorstCaseGainApplicable
worst_case_gain does not apply to this design — there is no feedback path.
Definition forge_tape_catalog.hpp:106
CustomNodeType make_tape_machine_node(Archetype archetype, double speed_ips=0.0, bool pre_echo_enabled=false)
One factory, three registered realizations.
constexpr state::ParamID kDrive
Definition forge_tape_catalog.hpp:96
ForgeNodeDescriptor descriptor()
float tape_machine_insertion_gain_bound(Archetype archetype, double sample_rate)
The largest gain any single stage of this insert can present, at the realization's own speed and the ...
constexpr state::ParamID kMix
Definition forge_tape_catalog.hpp:102
constexpr state::ParamID kEqCurve
Definition forge_tape_catalog.hpp:94
constexpr state::ParamID kPrintOffsetMs
Definition forge_tape_catalog.hpp:101
constexpr const char * kStuderTypeId
Definition forge_tape_catalog.hpp:89
constexpr state::ParamID kCompanding
Definition forge_tape_catalog.hpp:99
signal::TapeCurve Curve
Definition forge_tape_catalog.hpp:85
const char * tape_default_name(Archetype archetype) noexcept
Definition forge_tape_catalog.hpp:142
constexpr const char * kAmpexTypeId
Definition forge_tape_catalog.hpp:88
constexpr state::ParamID kBias
Definition forge_tape_catalog.hpp:95
const char * tape_type_id(Archetype archetype) noexcept
Definition forge_tape_catalog.hpp:130
signal::TapeArchetype Archetype
Definition forge_tape_catalog.hpp:84
Archetype normalized_archetype(Archetype archetype) noexcept
Definition forge_tape_catalog.hpp:154
constexpr state::ParamID kCrosstalkDb
Definition forge_tape_catalog.hpp:98
constexpr state::ParamID kAge
Definition forge_tape_catalog.hpp:97
constexpr const char * kCassetteTypeId
Definition forge_tape_catalog.hpp:90
constexpr state::ParamID kPrintThroughDb
Definition forge_tape_catalog.hpp:100
CurveRange curve_range(Archetype archetype) noexcept
Definition forge_tape_catalog.hpp:118
TapeCurve
Which recording standard's record/playback network pair is in force.
Definition tape_machine_components.hpp:25
TapeMachineT< float > TapeMachine
Definition tape_machine.hpp:910
TapeArchetype
Which machine's full preset a fresh instance starts from.
Definition tape_machine_components.hpp:33
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
Definition custom_node_type.hpp:90
Definition forge_param_descriptor.hpp:127
The EQ curves an archetype's front panel exposes, as the inclusive [min, max] of the stepped kEqCurve...
Definition forge_tape_catalog.hpp:113
Curve max_curve
Definition forge_tape_catalog.hpp:115
Curve min_curve
Definition forge_tape_catalog.hpp:114
Definition forge_tape_catalog.hpp:178
float mix
Definition forge_tape_catalog.hpp:193
float bias
Definition forge_tape_catalog.hpp:186
float print_db
Definition forge_tape_catalog.hpp:191
float companding
Definition forge_tape_catalog.hpp:190
float crosstalk_db
Definition forge_tape_catalog.hpp:189
float drive
Definition forge_tape_catalog.hpp:187
float print_offset_ms
Definition forge_tape_catalog.hpp:192
void invalidate_cache() noexcept
Force the next block to apply every parameter.
Definition forge_tape_catalog.hpp:197
float age
Definition forge_tape_catalog.hpp:188
signal::TapeMachine machine
Definition forge_tape_catalog.hpp:179
float curve
Definition forge_tape_catalog.hpp:185
A tape machine as an INSERT — record chain in, playback chain out, no delay and no feedback anywhere ...