Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
tape_loss.hpp
Go to the documentation of this file.
1#pragma once
2
3// Wallace playback loss — the physical tape tier's loss stage.
4//
5// The model (Wallace 1951; physical ranges from Bertram 1994) is three
6// multiplied magnitude terms:
7//
8// spacing H_sp (f) = exp(-k·d) k = 2πf / v
9// thickness H_th (f) = (1 - exp(-k·δ)) / (k·δ)
10// gap H_gap(f) = sin(k·g/2) / (k·g/2)
11//
12// They are realized by DIFFERENT filters, because they are different KINDS of
13// response and a single realization cannot serve both:
14//
15// * spacing × thickness is a smooth monotone tilt whose corner moves with
16// tape speed and head-tape spacing. At the slow, worn end of the age axis
17// — 1.875 ips with 25 µm spacing, which is exactly the cassette-era
18// territory this tier exists for — that corner sits near 100 Hz. A
19// minimum-phase FIR spanning ~2 ms cannot represent a 100 Hz corner at any
20// order the loop can afford: measured 15 dB of error at 97 taps and still
21// 2 dB at 513. So the tilt is carried by a LOW-ORDER IIR cascade (two
22// one-poles and a first-order shelf), least-squares fitted to the analytic
23// magnitude. Four parameters buy what several hundred FIR taps could not.
24// * the gap term is a sinc with a genuine null at f = v/g. Nulls are what
25// FIRs are for and no low-order IIR puts one there, so the minimum-phase
26// FIR keeps exactly this term — a purely high-frequency feature it
27// represents comfortably at the specified order.
28//
29// The gap term depends only on speed and head geometry, never on age, so its
30// FIR is designed once per tape speed. Only the IIR moves along the age axis.
31//
32// NORMATIVE, and a classic trap: when the tape SPEED changes, both filters are
33// redesigned and the change is made by running TWO COMPLETE FILTER INSTANCES
34// and crossfading their OUTPUTS. Never interpolate IIR coefficients between two
35// designs — the state of a recursive filter is meaningful only against the
36// coefficients that produced it, so a blended coefficient set is applied to
37// state that belongs to neither, which rings or blows up rather than
38// crossfading. (Moving the IIR smoothly along the AGE axis is a different
39// thing and is fine: those coefficients are recomputed from continuously
40// interpolated PHYSICAL parameters — a cutoff in hertz, a shelf in decibels —
41// which is ordinary filter modulation, not a blend of two designs.)
42
45#include <pulp/signal/fft.hpp>
47
48#include <algorithm>
49#include <array>
50#include <cmath>
51#include <complex>
52#include <cstddef>
53#include <numeric>
54#include <vector>
55
57
60 double speed_ips = 7.5;
61 double spacing_m = 5e-6;
64};
65
71inline constexpr double kTapeLossFloorDb = -60.0;
72
73inline double tape_wavenumber(double frequency_hz, double speed_ips) noexcept {
74 const double velocity = std::max(speed_ips * 0.0254, 1e-6);
75 return 2.0 * kPi * std::max(frequency_hz, 0.0) / velocity;
76}
77
79inline double tape_loss_smooth_magnitude(double frequency_hz,
80 const TapeLossGeometry& geometry) noexcept {
81 const double wavenumber = tape_wavenumber(frequency_hz, geometry.speed_ips);
82 const double spacing_loss = std::exp(-wavenumber * geometry.spacing_m);
83 const double thickness_argument = wavenumber * geometry.thickness_m;
84 const double thickness_loss = (thickness_argument < 1e-9)
85 ? 1.0
86 : (1.0 - std::exp(-thickness_argument)) / thickness_argument;
87 return spacing_loss * thickness_loss;
88}
89
91inline double tape_loss_gap_magnitude(double frequency_hz,
92 const TapeLossGeometry& geometry) noexcept {
93 const double argument =
94 tape_wavenumber(frequency_hz, geometry.speed_ips) * geometry.gap_m * 0.5;
95 if (argument < 1e-9) return 1.0;
96 return std::abs(std::sin(argument) / argument);
97}
98
100inline double tape_loss_magnitude(double frequency_hz,
101 const TapeLossGeometry& geometry) noexcept {
102 return tape_loss_smooth_magnitude(frequency_hz, geometry) *
103 tape_loss_gap_magnitude(frequency_hz, geometry);
104}
105
109inline double tape_loss_magnitude_floored(double frequency_hz,
110 const TapeLossGeometry& geometry) noexcept {
111 const double floor_linear = std::pow(10.0, kTapeLossFloorDb / 20.0);
112 return std::max(tape_loss_magnitude(frequency_hz, geometry), floor_linear);
113}
114
115// ── The smooth tilt: two fitted shapes, scaled ────────────────────────────
116//
117// The key structural fact: neither smooth term depends on frequency, speed and
118// spacing independently. The spacing term is exp(-2π·f·d/v) — a function of the
119// single product f·d/v — and the thickness term is a function of f·δ/v alone.
120// So each is a FIXED dimensionless shape, and every (speed, spacing) the module
121// can reach is that same shape with its frequency axis scaled by a scalar.
122//
123// That is what makes the age axis exact instead of interpolated: a cascade
124// fitted once to the dimensionless shape is correct at every age with its
125// corners multiplied by v/d. No knots, no parameter interpolation, and a tape
126// speed change becomes a multiply rather than a refit.
127
131template <std::size_t Poles, std::size_t Shelves>
132struct LossShape {
133 std::array<double, Poles> pole_x{};
134 std::array<double, Shelves> shelf_x{};
135 std::array<double, Shelves> shelf_db{};
136};
137
140
144 std::array<double, kLossPoleSections> pole_hz{};
145 std::array<double, kLossShelfSections> shelf_hz{};
146 std::array<double, kLossShelfSections> shelf_db{};
147};
148
150inline double tape_loss_iir_magnitude_db(const TapeLossIirParams& parameters,
151 double frequency_hz) noexcept {
152 double db = 0.0;
153 for (double corner : parameters.pole_hz) {
154 const double ratio = frequency_hz / std::max(corner, 1e-9);
155 db += -10.0 * std::log10(1.0 + ratio * ratio);
156 }
157 for (std::size_t i = 0; i < kLossShelfSections; ++i) {
158 const double gain = std::pow(10.0, parameters.shelf_db[i] / 20.0);
159 const double ratio = frequency_hz / std::max(parameters.shelf_hz[i], 1e-9);
160 db += 10.0 * std::log10((1.0 + gain * gain * ratio * ratio) / (1.0 + ratio * ratio));
161 }
162 return db;
163}
164
167inline double spacing_shape_db(double x) noexcept {
168 return -8.685889638065035 * 2.0 * kPi * x; // 20·log10(exp(-2πx))
169}
170
171inline double thickness_shape_db(double x) noexcept {
172 const double argument = 2.0 * kPi * x;
173 if (argument < 1e-9) return 0.0;
174 return 20.0 * std::log10((1.0 - std::exp(-argument)) / argument);
175}
176
177namespace detail {
178
179using LossSearchVector = std::array<double, kLossSearchDimension>;
180
187template <std::size_t Poles, std::size_t Shelves, typename Target>
189 static_assert(Poles + 2u * Shelves <= kLossSearchDimension,
190 "search vector is sized for the largest shape");
191 constexpr std::size_t kDimension = Poles + 2u * Shelves;
192 const auto points = static_cast<std::size_t>(kLossIirFitPoints);
193
194 std::vector<double> x(points);
195 std::vector<double> target_db(points);
196 for (std::size_t i = 0; i < points; ++i) {
198 static_cast<double>(i) /
199 static_cast<double>(points - 1));
200 target_db[i] = std::max(target(x[i]), kTapeLossFloorDb);
201 }
202
203 auto decode = [](const std::array<double, kDimension>& v) {
205 std::size_t k = 0;
206 for (std::size_t i = 0; i < Poles; ++i)
207 shape.pole_x[i] = std::clamp(std::pow(10.0, v[k++]), 1e-6, 1e4);
208 for (std::size_t i = 0; i < Shelves; ++i) {
209 shape.shelf_x[i] = std::clamp(std::pow(10.0, v[k++]), 1e-6, 1e4);
210 shape.shelf_db[i] = std::clamp(v[k++], kLossShelfMinDb, 0.0);
211 }
212 return shape;
213 };
214
215 auto magnitude = [](const LossShape<Poles, Shelves>& shape, double f) {
216 double db = 0.0;
217 for (double corner : shape.pole_x) {
218 const double r = f / corner;
219 db += -10.0 * std::log10(1.0 + r * r);
220 }
221 for (std::size_t i = 0; i < Shelves; ++i) {
222 const double g = std::pow(10.0, shape.shelf_db[i] / 20.0);
223 const double r = f / shape.shelf_x[i];
224 db += 10.0 * std::log10((1.0 + g * g * r * r) / (1.0 + r * r));
225 }
226 return db;
227 };
228
229 auto objective = [&](const std::array<double, kDimension>& v) {
230 const auto shape = decode(v);
231 double worst = 0.0;
232 double sum = 0.0;
233 for (std::size_t i = 0; i < points; ++i) {
234 // Both sides floored, so error below the floor costs nothing: the
235 // fit spends its freedom where the response is still audible
236 // instead of chasing −300 dB.
237 const double modeled = std::max(magnitude(shape, x[i]), kTapeLossFloorDb);
238 const double error = std::abs(modeled - target_db[i]);
239 worst = std::max(worst, error);
240 sum += error * error;
241 }
242 return worst + 1e-4 * sum;
243 };
244
245 // A simplex collapses as it converges and a collapsed simplex cannot
246 // explore, so each start is re-seeded from its own best point and run again.
247 auto run = [&](std::array<double, kDimension> start) {
248 for (int restart = 0; restart < kLossIirFitRestarts; ++restart) {
249 std::array<std::array<double, kDimension>, kDimension + 1> simplex;
250 std::array<double, kDimension + 1> value{};
251 simplex[0] = start;
252 for (std::size_t i = 0; i < kDimension; ++i) {
253 const bool is_depth = i >= Poles && ((i - Poles) % 2u) == 1u;
254 simplex[i + 1] = start;
255 simplex[i + 1][i] += is_depth ? 3.0 : 0.25;
256 }
257 for (std::size_t i = 0; i <= kDimension; ++i) value[i] = objective(simplex[i]);
258
259 auto combine = [&](const std::array<double, kDimension>& a,
260 const std::array<double, kDimension>& b, double t) {
261 std::array<double, kDimension> out{};
262 for (std::size_t i = 0; i < kDimension; ++i) out[i] = a[i] + t * (b[i] - a[i]);
263 return out;
264 };
265
266 for (int iteration = 0; iteration < kLossIirFitIterations; ++iteration) {
267 std::array<std::size_t, kDimension + 1> order{};
268 std::iota(order.begin(), order.end(), 0u);
269 std::sort(order.begin(), order.end(),
270 [&](std::size_t a, std::size_t b) { return value[a] < value[b]; });
271 const std::size_t best = order.front();
272 const std::size_t worst = order.back();
273 const std::size_t second = order[kDimension - 1];
274
275 std::array<double, kDimension> centroid{};
276 for (std::size_t i = 0; i <= kDimension; ++i) {
277 if (i == worst) continue;
278 for (std::size_t k = 0; k < kDimension; ++k)
279 centroid[k] += simplex[i][k] / static_cast<double>(kDimension);
280 }
281
282 const auto reflected = combine(centroid, simplex[worst], -1.0);
283 const double reflected_value = objective(reflected);
284 if (reflected_value < value[best]) {
285 const auto expanded = combine(centroid, simplex[worst], -2.0);
286 const double expanded_value = objective(expanded);
287 simplex[worst] = (expanded_value < reflected_value) ? expanded : reflected;
288 value[worst] = std::min(expanded_value, reflected_value);
289 } else if (reflected_value < value[second]) {
290 simplex[worst] = reflected;
291 value[worst] = reflected_value;
292 } else {
293 const auto contracted = combine(centroid, simplex[worst], 0.5);
294 const double contracted_value = objective(contracted);
295 if (contracted_value < value[worst]) {
296 simplex[worst] = contracted;
297 value[worst] = contracted_value;
298 } else {
299 for (std::size_t i = 0; i <= kDimension; ++i) {
300 if (i == best) continue;
301 simplex[i] = combine(simplex[best], simplex[i], 0.5);
302 value[i] = objective(simplex[i]);
303 }
304 }
305 }
306 }
307 std::size_t best = 0;
308 for (std::size_t i = 1; i <= kDimension; ++i)
309 if (value[i] < value[best]) best = i;
310 start = simplex[best];
311 }
312 return start;
313 };
314
315 // Multi-start with a GEOMETRIC gain ladder. The target steepens
316 // exponentially in log-frequency, so each successive shelf must cut roughly
317 // twice what the last did; starting from equal cuts leaves the simplex in a
318 // local minimum that misses the top of the band by several decibels.
319 std::array<double, kDimension> best{};
320 double best_value = 0.0;
321 bool have_best = false;
322 for (int start_index = 0; start_index < kLossIirFitStarts; ++start_index) {
323 const double spread = 1.5 + 0.35 * static_cast<double>(start_index);
324 const double base_db = -1.5 - 0.5 * static_cast<double>(start_index);
325 // Anchor the ladder where the target crosses −3 dB.
326 double anchor = kLossShapeMaxX;
327 for (double f = kLossShapeMinX; f < kLossShapeMaxX; f *= 1.02) {
328 if (target(f) <= -3.0) {
329 anchor = f;
330 break;
331 }
332 }
333 std::array<double, kDimension> start{};
334 std::size_t k = 0;
335 for (std::size_t i = 0; i < Poles; ++i)
336 start[k++] = std::log10(anchor * std::pow(spread, static_cast<double>(i)));
337 for (std::size_t i = 0; i < Shelves; ++i) {
338 start[k++] =
339 std::log10(anchor * 0.5 * std::pow(spread, static_cast<double>(i)));
340 start[k++] = std::max(base_db * std::pow(1.7, static_cast<double>(i)),
342 }
343 const auto candidate = run(start);
344 const double value = objective(candidate);
345 if (!have_best || value < best_value) {
346 best = candidate;
347 best_value = value;
348 have_best = true;
349 }
350 }
351 return decode(best);
352}
353
354} // namespace detail
355
362
373
385
389 TapeLossIirParams parameters_for(const TapeLossGeometry& geometry) const noexcept {
390 const double velocity = std::max(geometry.speed_ips * 0.0254, 1e-9);
391 const double spacing_scale = velocity / std::max(geometry.spacing_m, 1e-12);
392 const double thickness_scale = velocity / std::max(geometry.thickness_m, 1e-12);
393
394 TapeLossIirParams parameters;
395 std::size_t pole = 0;
396 std::size_t shelf = 0;
397 for (std::size_t i = 0; i < kSpacingPoleSections; ++i)
398 parameters.pole_hz[pole++] = spacing.pole_x[i] * spacing_scale;
399 for (std::size_t i = 0; i < kSpacingShelfSections; ++i) {
400 parameters.shelf_hz[shelf] = spacing.shelf_x[i] * spacing_scale;
401 parameters.shelf_db[shelf++] = spacing.shelf_db[i];
402 }
403 for (std::size_t i = 0; i < kThicknessPoleSections; ++i)
404 parameters.pole_hz[pole++] = thickness.pole_x[i] * thickness_scale;
405 for (std::size_t i = 0; i < kThicknessShelfSections; ++i) {
406 parameters.shelf_hz[shelf] = thickness.shelf_x[i] * thickness_scale;
407 parameters.shelf_db[shelf++] = thickness.shelf_db[i];
408 }
409 return parameters;
410 }
411};
412
421 static const TapeLossShapes shapes = TapeLossShapes::tabulated();
422 return shapes;
423}
424
428 TapeLossShapes shapes;
429 shapes.fit();
430 return shapes;
431}
432
435public:
436 void set(const TapeLossIirParams& parameters, double fs) noexcept {
437 parameters_ = parameters;
438 for (std::size_t i = 0; i < kLossPoleSections; ++i)
439 poles_[i].set_cutoff(parameters.pole_hz[i], fs);
440 for (std::size_t i = 0; i < kLossShelfSections; ++i)
441 shelves_[i].set(parameters.shelf_hz[i],
442 std::pow(10.0, parameters.shelf_db[i] / 20.0), fs);
443 }
444
445 void reset() noexcept {
446 for (auto& pole : poles_) pole.reset();
447 for (auto& shelf : shelves_) shelf.reset();
448 }
449
450 double process(double x) noexcept {
451 for (auto& pole : poles_) x = pole.lowpass(x);
452 for (auto& shelf : shelves_) x = shelf.process(x);
453 return x;
454 }
455
459 double dc_group_delay_seconds() const noexcept {
460 double seconds = 0.0;
461 for (double corner : parameters_.pole_hz)
462 seconds += 1.0 / (2.0 * kPi * std::max(corner, 1e-9));
463 for (std::size_t i = 0; i < kLossShelfSections; ++i) {
464 const double gain = std::pow(10.0, parameters_.shelf_db[i] / 20.0);
465 seconds += (1.0 - gain) / (2.0 * kPi * std::max(parameters_.shelf_hz[i], 1e-9));
466 }
467 return seconds;
468 }
469
470 const TapeLossIirParams& parameters() const noexcept { return parameters_; }
471
472private:
473 std::array<OnePole, kLossPoleSections> poles_{};
474 std::array<FirstOrderShelf, kLossShelfSections> shelves_{};
475 TapeLossIirParams parameters_;
476};
477
478// ── The gap null: a minimum-phase FIR ─────────────────────────────────────
479
482inline std::size_t tape_gap_fir_taps(double fs) {
483 const auto scaled = static_cast<std::size_t>(
484 std::llround(static_cast<double>(kLossFirOrder48k) * fs / 48000.0));
485 return std::clamp<std::size_t>(scaled, 32u, 512u) + 1u;
486}
487
495inline std::vector<double> design_tape_gap_fir(double fs, std::size_t taps,
496 const TapeLossGeometry& geometry) {
497 int fft_size = 64;
498 while (static_cast<std::size_t>(fft_size) < taps * 8u) fft_size <<= 1;
499 const auto size = static_cast<std::size_t>(fft_size);
500
501 FftT<double> fft(fft_size);
502 std::vector<std::complex<double>> spectrum(size);
503
504 const double floor_linear = std::pow(10.0, kTapeLossFloorDb / 20.0);
505 for (std::size_t bin = 0; bin <= size / 2; ++bin) {
506 const double frequency = static_cast<double>(bin) * fs / static_cast<double>(size);
507 const double magnitude =
508 std::max(tape_loss_gap_magnitude(frequency, geometry), floor_linear);
509 const double log_magnitude = std::log(magnitude);
510 spectrum[bin] = {log_magnitude, 0.0};
511 if (bin > 0 && bin < size / 2) spectrum[size - bin] = {log_magnitude, 0.0};
512 }
513
514 // Real cepstrum folded to its causal part: the minimum-phase spectrum is
515 // the analytic completion of the log-magnitude, and folding the cepstrum is
516 // how you get it without unwrapping a phase.
517 fft.inverse(spectrum.data());
518 std::vector<std::complex<double>> cepstrum(size, std::complex<double>{0.0, 0.0});
519 cepstrum[0] = {spectrum[0].real(), 0.0};
520 for (std::size_t n = 1; n < size / 2; ++n) cepstrum[n] = {2.0 * spectrum[n].real(), 0.0};
521 cepstrum[size / 2] = {spectrum[size / 2].real(), 0.0};
522
523 fft.forward(cepstrum.data());
524 for (auto& value : cepstrum) value = std::exp(value);
525 fft.inverse(cepstrum.data());
526
527 // The window is the TRAILING HALF of a Kaiser, not a symmetric one. A
528 // minimum-phase impulse response concentrates its energy at the START, so a
529 // symmetric window would attenuate h[0] — where nearly all of it is — to
530 // nothing and amplify the tail. That is not a taper, it is a different
531 // filter, and it cost 34 dB of error before it was caught.
532 std::vector<double> symmetric(2u * taps - 1u, 0.0);
534
535 std::vector<double> coefficients(taps, 0.0);
536 for (std::size_t i = 0; i < taps; ++i)
537 coefficients[i] = cepstrum[i].real() * symmetric[taps - 1u + i];
538
539 // The gap term is unity at DC, so normalize the realized DC gain to 1 and
540 // keep windowing from introducing a level shift the loop would compound.
541 double sum = 0.0;
542 for (double c : coefficients) sum += c;
543 if (std::abs(sum) > 1e-9)
544 for (double& c : coefficients) c /= sum;
545 return coefficients;
546}
547
552public:
554 void prepare(double fs, double speed_ips) {
555 sample_rate_ = fs;
556 shapes_ = &tape_loss_shapes();
558 }
559
562 void set_speed(double speed_ips) {
563 speed_ips_ = speed_ips;
564 TapeLossGeometry geometry;
565 geometry.speed_ips = speed_ips;
566 gap_taps_ = design_tape_gap_fir(sample_rate_, tape_gap_fir_taps(sample_rate_), geometry);
567
568 double weighted = 0.0;
569 double sum = 0.0;
570 for (std::size_t i = 0; i < gap_taps_.size(); ++i) {
571 weighted += static_cast<double>(i) * gap_taps_[i];
572 sum += gap_taps_[i];
573 }
574 gap_group_delay_samples_ = (std::abs(sum) > 1e-12) ? weighted / sum : 0.0;
575 }
576
577 bool empty() const noexcept { return gap_taps_.empty(); }
578 double speed_ips() const noexcept { return speed_ips_; }
579 const std::vector<double>& gap_taps() const noexcept { return gap_taps_; }
580 double gap_group_delay_samples() const noexcept { return gap_group_delay_samples_; }
581 const TapeLossShapes& shapes() const noexcept { return *shapes_; }
582
585 TapeLossGeometry geometry_at(double age) const noexcept {
586 TapeLossGeometry geometry;
587 geometry.speed_ips = speed_ips_;
588 geometry.spacing_m =
589 interpolate_knots(kAgeAxis, kAgeSpacingUm, std::clamp(age, 0.0, 1.0)) * 1e-6;
590 return geometry;
591 }
592
595 TapeLossIirParams parameters_at(double age) const noexcept {
596 return shapes_->parameters_for(geometry_at(age));
597 }
598
599private:
600 const TapeLossShapes* shapes_ = &tape_loss_shapes();
601 std::vector<double> gap_taps_;
602 double gap_group_delay_samples_ = 0.0;
603 double sample_rate_ = 48000.0;
604 double speed_ips_ = 7.5;
605};
606
611public:
612 void prepare(std::size_t max_taps) { fir_.prepare(max_taps); }
613
614 void reset() noexcept {
615 fir_.reset();
616 iir_.reset();
617 }
618
619 void discard_history() noexcept {
620 fir_.discard_history();
621 iir_.reset();
622 }
623
627 void reset_recursive() noexcept { iir_.reset(); }
628
629 void set_parameters(const TapeLossIirParams& parameters, double fs) noexcept {
630 iir_.set(parameters, fs);
631 }
632
633 void push(double x) noexcept { fir_.push(x); }
634
635 double process(const TapeLossDesign& design) noexcept {
636 return iir_.process(fir_.convolve(design.gap_taps().data(), design.gap_taps().size()));
637 }
638
641 void copy_history_from(const TapeLossStage& other) { fir_ = other.fir_; }
642
643 double dc_group_delay_seconds() const noexcept { return iir_.dc_group_delay_seconds(); }
644 const TapeLossIirParams& parameters() const noexcept { return iir_.parameters(); }
645
646private:
647 FixedFir fir_;
648 TapeLossIir iir_;
649};
650
651} // namespace pulp::signal::chardelay
Definition fft.hpp:176
void inverse(std::complex< SampleType > *data) const
Definition fft.hpp:372
void forward(std::complex< SampleType > *data) const
Definition fft.hpp:350
Definition primitives.hpp:403
Everything designed for one tape speed: the gap FIR (speed-only) and the scaled IIR parameters,...
Definition tape_loss.hpp:551
TapeLossIirParams parameters_at(double age) const noexcept
Filter parameters at age.
Definition tape_loss.hpp:595
TapeLossGeometry geometry_at(double age) const noexcept
The geometry at a given age: spacing walks the age table, everything else is fixed.
Definition tape_loss.hpp:585
const std::vector< double > & gap_taps() const noexcept
Definition tape_loss.hpp:579
void prepare(double fs, double speed_ips)
Expensive: fits both dimensionless shapes. Once per instance.
Definition tape_loss.hpp:554
const TapeLossShapes & shapes() const noexcept
Definition tape_loss.hpp:581
double speed_ips() const noexcept
Definition tape_loss.hpp:578
bool empty() const noexcept
Definition tape_loss.hpp:577
void set_speed(double speed_ips)
Cheap by comparison: only the gap FIR is redesigned, because only it depends on the speed in a way a ...
Definition tape_loss.hpp:562
double gap_group_delay_samples() const noexcept
Definition tape_loss.hpp:580
The realized IIR cascade for one channel.
Definition tape_loss.hpp:434
void reset() noexcept
Definition tape_loss.hpp:445
double dc_group_delay_seconds() const noexcept
Group delay at DC, in seconds.
Definition tape_loss.hpp:459
void set(const TapeLossIirParams &parameters, double fs) noexcept
Definition tape_loss.hpp:436
const TapeLossIirParams & parameters() const noexcept
Definition tape_loss.hpp:470
double process(double x) noexcept
Definition tape_loss.hpp:450
One complete, independently-stateful loss filter: the gap FIR followed by the fitted IIR tilt.
Definition tape_loss.hpp:610
void set_parameters(const TapeLossIirParams &parameters, double fs) noexcept
Definition tape_loss.hpp:629
double dc_group_delay_seconds() const noexcept
Definition tape_loss.hpp:643
void discard_history() noexcept
Definition tape_loss.hpp:619
void reset() noexcept
Definition tape_loss.hpp:614
void copy_history_from(const TapeLossStage &other)
The stage's own history, so a newly adopted design starts from the same input rather than from silenc...
Definition tape_loss.hpp:641
void push(double x) noexcept
Definition tape_loss.hpp:633
void prepare(std::size_t max_taps)
Definition tape_loss.hpp:612
double process(const TapeLossDesign &design) noexcept
Definition tape_loss.hpp:635
void reset_recursive() noexcept
Reset only the recursive half.
Definition tape_loss.hpp:627
const TapeLossIirParams & parameters() const noexcept
Definition tape_loss.hpp:644
Definition design_adherence.hpp:32
Definition tape_loss.hpp:177
std::array< double, kLossSearchDimension > LossSearchVector
Definition tape_loss.hpp:179
LossShape< Poles, Shelves > fit_loss_shape(Target &&target)
Generic fit of Poles one-poles and Shelves first-order shelves to a dimensionless target,...
Definition tape_loss.hpp:188
Definition bbd.hpp:36
constexpr std::size_t kLossFirOrder48k
Wallace playback loss (Wallace 1951; physical ranges from Bertram 1994) is split across two realizati...
Definition tables.hpp:398
constexpr int kLossIirFitPoints
The smooth spacing × thickness tilt goes to a fitted IIR cascade: how many log-spaced points the leas...
Definition tables.hpp:403
constexpr std::size_t kSpacingPoleSections
Sections in each fitted cascade.
Definition tables.hpp:421
double thickness_shape_db(double x) noexcept
Definition tape_loss.hpp:171
constexpr std::array< double, kSpacingShelfSections > kSpacingShapeShelfX
Definition tables.hpp:455
constexpr double kTapeHeadGapM
Definition tables.hpp:506
constexpr std::size_t kThicknessShelfSections
Definition tables.hpp:424
std::vector< double > design_tape_gap_fir(double fs, std::size_t taps, const TapeLossGeometry &geometry)
Design a minimum-phase FIR realizing the GAP term only.
Definition tape_loss.hpp:495
constexpr double kPi
Definition primitives.hpp:38
constexpr std::array< double, 3 > kAgeAxis
Age macro.
Definition tables.hpp:518
constexpr std::array< double, kSpacingPoleSections > kSpacingShapePoleX
THE FITTED SHAPES, as shipped.
Definition tables.hpp:453
std::size_t tape_gap_fir_taps(double fs)
FIR length at a given sample rate: the specified order at 48 kHz, scaled with the rate so the filter ...
Definition tape_loss.hpp:482
constexpr int kLossIirFitStarts
Definition tables.hpp:483
double tape_loss_magnitude(double frequency_hz, const TapeLossGeometry &geometry) noexcept
The complete modeled response.
Definition tape_loss.hpp:100
double tape_wavenumber(double frequency_hz, double speed_ips) noexcept
Definition tape_loss.hpp:73
TapeLossShapes fit_tape_loss_shapes()
The offline derivation.
Definition tape_loss.hpp:427
constexpr std::size_t kLossPoleSections
Definition tables.hpp:426
constexpr double kHysteresisHalfBandStopbandDb
Definition tables.hpp:388
double tape_loss_magnitude_floored(double frequency_hz, const TapeLossGeometry &geometry) noexcept
The response the realization actually targets, floor included.
Definition tape_loss.hpp:109
double interpolate_knots(const std::array< double, N > &axis, const std::array< double, N > &values, double c) noexcept
Definition primitives.hpp:125
constexpr std::array< double, kThicknessShelfSections > kThicknessShapeShelfDb
Definition tables.hpp:465
constexpr double kTapeCoatingThicknessM
Fixed tape geometry, in metres.
Definition tables.hpp:505
constexpr std::array< double, kThicknessPoleSections > kThicknessShapePoleX
Definition tables.hpp:461
constexpr std::size_t kThicknessPoleSections
Definition tables.hpp:423
LossShape< kSpacingPoleSections, kSpacingShelfSections > SpacingShape
Definition tape_loss.hpp:138
constexpr std::array< double, 3 > kAgeSpacingUm
Definition tables.hpp:519
double tape_loss_gap_magnitude(double frequency_hz, const TapeLossGeometry &geometry) noexcept
The gap null. Carried by the minimum-phase FIR.
Definition tape_loss.hpp:91
constexpr std::array< double, kSpacingShelfSections > kSpacingShapeShelfDb
Definition tables.hpp:458
constexpr std::array< double, kThicknessShelfSections > kThicknessShapeShelfX
Definition tables.hpp:463
constexpr int kLossIirFitRestarts
Definition tables.hpp:482
LossShape< kThicknessPoleSections, kThicknessShelfSections > ThicknessShape
Definition tape_loss.hpp:139
double spacing_shape_db(double x) noexcept
The two dimensionless targets, in dB, as functions of their own normalized frequency.
Definition tape_loss.hpp:167
constexpr double kLossShapeMinX
Normalized-frequency range each shape is fitted over, and how many log-spaced points.
Definition tables.hpp:471
constexpr double kLossShapeMaxX
Definition tables.hpp:472
const TapeLossShapes & tape_loss_shapes()
The process-wide fitted shapes.
Definition tape_loss.hpp:420
constexpr std::size_t kLossSearchDimension
Search dimension of ONE shape fit (the larger of the two).
Definition tables.hpp:432
constexpr double kTapeLossFloorDb
Floor applied to a modeled loss magnitude before anything is fitted to it.
Definition tape_loss.hpp:71
constexpr std::size_t kSpacingShelfSections
Definition tables.hpp:422
double tape_loss_smooth_magnitude(double frequency_hz, const TapeLossGeometry &geometry) noexcept
The smooth tilt: spacing × thickness. Carried by the IIR cascade.
Definition tape_loss.hpp:79
double tape_loss_iir_magnitude_db(const TapeLossIirParams &parameters, double frequency_hz) noexcept
Magnitude of the cascade, in dB. Used by the fitter and by tests.
Definition tape_loss.hpp:150
constexpr std::size_t kLossShelfSections
Definition tables.hpp:428
constexpr int kLossIirFitIterations
Simplex iterations per start, restarts per start, and how many starts.
Definition tables.hpp:481
constexpr double kLossShelfMinDb
Deepest cut any one shelf may ask for.
Definition tables.hpp:486
@ second
Definition dither.hpp:24
@ c
Definition spectrum_trace.hpp:35
void kaiser_window(std::vector< double > &output, double beta)
Definition windowed_sinc_design.hpp:46
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
double kaiser_beta_for_stopband(double stopband_db)
Definition windowed_sinc_design.hpp:27
One fitted cascade in dimensionless frequency: corners are in units of the shape's own normalized var...
Definition tape_loss.hpp:132
std::array< double, Shelves > shelf_db
Definition tape_loss.hpp:135
std::array< double, Poles > pole_x
Definition tape_loss.hpp:133
std::array< double, Shelves > shelf_x
Definition tape_loss.hpp:134
Head/tape geometry for the Wallace loss model. All lengths in metres.
Definition tape_loss.hpp:59
double gap_m
Definition tape_loss.hpp:63
double spacing_m
Definition tape_loss.hpp:61
double speed_ips
Definition tape_loss.hpp:60
double thickness_m
Definition tape_loss.hpp:62
The realized cascade's parameters in hertz, poles and shelves from both shapes concatenated.
Definition tape_loss.hpp:143
std::array< double, kLossPoleSections > pole_hz
Definition tape_loss.hpp:144
std::array< double, kLossShelfSections > shelf_db
Definition tape_loss.hpp:146
std::array< double, kLossShelfSections > shelf_hz
Definition tape_loss.hpp:145
Fit both dimensionless shapes.
Definition tape_loss.hpp:359
SpacingShape spacing
Definition tape_loss.hpp:360
ThicknessShape thickness
Definition tape_loss.hpp:361
TapeLossIirParams parameters_for(const TapeLossGeometry &geometry) const noexcept
Corners in hertz for a given geometry.
Definition tape_loss.hpp:389
void fit()
Re-derive the shapes from the physics.
Definition tape_loss.hpp:367
static TapeLossShapes tabulated() noexcept
The shipped values.
Definition tape_loss.hpp:375
Shared Kaiser-windowed-sinc FIR design helpers.