74 const double velocity = std::max(speed_ips * 0.0254, 1
e-6);
75 return 2.0 *
kPi * std::max(frequency_hz, 0.0) / velocity;
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 < 1
e-9)
86 : (1.0 - std::exp(-thickness_argument)) / thickness_argument;
87 return spacing_loss * thickness_loss;
93 const double argument =
94 tape_wavenumber(frequency_hz, geometry.speed_ips) * geometry.gap_m * 0.5;
95 if (argument < 1
e-9)
return 1.0;
96 return std::abs(std::sin(argument) / argument);
131template <std::
size_t Poles, std::
size_t Shelves>
144 std::array<double, kLossPoleSections>
pole_hz{};
151 double frequency_hz)
noexcept {
153 for (
double corner : parameters.pole_hz) {
154 const double ratio = frequency_hz / std::max(corner, 1
e-9);
155 db += -10.0 * std::log10(1.0 +
ratio *
ratio);
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], 1
e-9);
168 return -8.685889638065035 * 2.0 *
kPi * x;
172 const double argument = 2.0 *
kPi * x;
173 if (argument < 1
e-9)
return 0.0;
174 return 20.0 * std::log10((1.0 - std::exp(-argument)) / argument);
187template <std::
size_t Poles, std::
size_t Shelves,
typename Target>
190 "search vector is sized for the largest shape");
191 constexpr std::size_t kDimension = Poles + 2u * Shelves;
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));
203 auto decode = [](
const std::array<double, kDimension>& v) {
206 for (std::size_t
i = 0;
i < Poles; ++
i)
207 shape.pole_x[
i] = std::clamp(std::pow(10.0, v[k++]), 1
e-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++]), 1
e-6, 1e4);
217 for (
double corner : shape.pole_x) {
218 const double r = f / corner;
219 db += -10.0 * std::log10(1.0 + r * r);
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));
229 auto objective = [&](
const std::array<double, kDimension>& v) {
230 const auto shape = decode(v);
233 for (std::size_t
i = 0;
i < points; ++
i) {
238 const double error = std::abs(modeled - target_db[
i]);
239 worst = std::max(worst, error);
240 sum += error * error;
242 return worst + 1
e-4 * sum;
247 auto run = [&](std::array<double, kDimension> start) {
249 std::array<std::array<double, kDimension>, kDimension + 1> simplex;
250 std::array<double, kDimension + 1> value{};
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;
257 for (std::size_t
i = 0;
i <= kDimension; ++
i) value[
i] = objective(simplex[
i]);
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]);
267 std::array<std::size_t, kDimension + 1> order{};
268 std::iota(order.begin(), order.end(), 0
u);
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];
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);
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;
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;
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]);
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];
319 std::array<double, kDimension> best{};
320 double best_value = 0.0;
321 bool have_best =
false;
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);
328 if (target(f) <= -3.0) {
333 std::array<double, kDimension> start{};
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) {
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)),
343 const auto candidate = run(start);
344 const double value = objective(candidate);
345 if (!have_best || value < best_value) {
390 const double velocity = std::max(geometry.speed_ips * 0.0254, 1
e-9);
391 const double spacing_scale = velocity / std::max(geometry.spacing_m, 1
e-12);
392 const double thickness_scale = velocity / std::max(geometry.thickness_m, 1
e-12);
395 std::size_t pole = 0;
396 std::size_t shelf = 0;
442 std::pow(10.0,
parameters.shelf_db[
i] / 20.0), fs);
446 for (
auto& pole : poles_) pole.reset();
447 for (
auto& shelf : shelves_) shelf.reset();
451 for (
auto& pole : poles_) x = pole.lowpass(x);
452 for (
auto& shelf : shelves_) x = shelf.process(x);
460 double seconds = 0.0;
461 for (
double corner : parameters_.pole_hz)
462 seconds += 1.0 / (2.0 *
kPi * std::max(corner, 1
e-9));
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], 1
e-9));
473 std::array<OnePole, kLossPoleSections> poles_{};
474 std::array<FirstOrderShelf, kLossShelfSections> shelves_{};
475 TapeLossIirParams parameters_;
483 const auto scaled =
static_cast<std::size_t
>(
485 return std::clamp<std::size_t>(scaled, 32u, 512u) + 1u;
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);
502 std::vector<std::complex<double>> spectrum(size);
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 =
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};
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};
524 for (
auto& value : cepstrum) value = std::exp(value);
532 std::vector<double> symmetric(2u * taps - 1u, 0.0);
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];
542 for (
double c : coefficients) sum +=
c;
543 if (std::abs(sum) > 1
e-9)
544 for (
double&
c : coefficients)
c /= sum;
568 double weighted = 0.0;
570 for (std::size_t
i = 0;
i < gap_taps_.size(); ++
i) {
571 weighted +=
static_cast<double>(
i) * gap_taps_[
i];
574 gap_group_delay_samples_ = (std::abs(sum) > 1
e-12) ? weighted / sum : 0.0;
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_; }
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;
612 void prepare(std::size_t max_taps) { fir_.prepare(max_taps); }
620 fir_.discard_history();
633 void push(
double x)
noexcept { fir_.push(x); }
636 return iir_.process(fir_.convolve(
design.gap_taps().data(),
design.gap_taps().size()));
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 ¶meters, 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 ¶meters, 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
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 ¶meters, 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.