Namespaces | |
| namespace | detail |
Classes | |
| class | BbdChannel |
| One channel of the clocked BBD core, compander included. More... | |
| class | ModulatedAllpass |
| One modulated Schroeder allpass: y[n] = −g·x[n] + v[n−d], v[n] = x[n] + g·y[n]. More... | |
| class | ReverbTank |
| Dattorro's figure-of-eight reverb tank, one per channel. More... | |
| class | DiffusionChain |
| The four-stage chain for one channel. More... | |
| struct | Langevin |
| Langevin function L(x) = coth(x) − 1/x and its first two derivatives. More... | |
| class | JilesAthertonHysteresis |
| The hysteresis stage for one channel, in normalized units. More... | |
| class | HysteresisHalfBandOversampler |
| Fixed-depth linear-phase half-band oversampling around a per-sample callback. More... | |
| class | OversampledHysteresis |
| Production-owned composition of a fixed wrapper and magnetic solver. More... | |
| class | Xorshift32 |
| class | Smoother |
| class | FractionalDelayLine |
| class | OnePole |
| class | FirstOrderShelf |
| class | Svf2 |
| class | Butterworth6Lowpass |
| class | FixedFir |
| class | EnvelopeFollower |
| class | ReverseSegmenter |
| class | TapeInstability |
| Wow + flutter generator for one channel. Shared by both tape tiers. More... | |
| class | TapeStandardChannel |
| Standard-tier tape coloration for one channel: the pre-line record EQ + saturation + playback EQ, and the post-line loss stand-in + DC blocker. More... | |
| struct | TapeLossGeometry |
| Head/tape geometry for the Wallace loss model. All lengths in metres. More... | |
| struct | LossShape |
| One fitted cascade in dimensionless frequency: corners are in units of the shape's own normalized variable and become hertz when multiplied by that term's scale factor. More... | |
| struct | TapeLossIirParams |
| The realized cascade's parameters in hertz, poles and shelves from both shapes concatenated. More... | |
| struct | TapeLossShapes |
| Fit both dimensionless shapes. More... | |
| class | TapeLossIir |
| The realized IIR cascade for one channel. More... | |
| class | TapeLossDesign |
| Everything designed for one tape speed: the gap FIR (speed-only) and the scaled IIR parameters, which are a pure function of geometry once the dimensionless shapes are fitted. More... | |
| class | TapeLossStage |
| One complete, independently-stateful loss filter: the gap FIR followed by the fitted IIR tilt. More... | |
| class | TapeChew |
| Intermittent dropout. More... | |
| class | TapePhysicalChannel |
| The physical tier for one channel. More... | |
| class | VintageChannel |
Typedefs | |
| using | HalfBandOversampler4x = HysteresisHalfBandOversampler<2> |
| The two depths the module has a measured reason to name. | |
| using | HysteresisOversampler8x = HysteresisHalfBandOversampler<3> |
| using | OversampledHysteresis4x = OversampledHysteresis<HalfBandOversampler4x> |
| using | OversampledHysteresis8x = OversampledHysteresis<HysteresisOversampler8x> |
| using | SpacingShape = LossShape<kSpacingPoleSections, kSpacingShelfSections> |
| using | ThicknessShape = LossShape<kThicknessPoleSections, kThicknessShelfSections> |
Functions | |
| double | flush_denormal (double v) noexcept |
| double | fast_tanh (double v) noexcept |
| template<std::size_t N> | |
| double | interpolate_knots (const std::array< double, N > &axis, const std::array< double, N > &values, double c) noexcept |
| double | tape_wavenumber (double frequency_hz, double speed_ips) noexcept |
| double | tape_loss_smooth_magnitude (double frequency_hz, const TapeLossGeometry &geometry) noexcept |
| The smooth tilt: spacing × thickness. Carried by the IIR cascade. | |
| double | tape_loss_gap_magnitude (double frequency_hz, const TapeLossGeometry &geometry) noexcept |
| The gap null. Carried by the minimum-phase FIR. | |
| double | tape_loss_magnitude (double frequency_hz, const TapeLossGeometry &geometry) noexcept |
| The complete modeled response. | |
| double | tape_loss_magnitude_floored (double frequency_hz, const TapeLossGeometry &geometry) noexcept |
| The response the realization actually targets, floor included. | |
| 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. | |
| double | spacing_shape_db (double x) noexcept |
| The two dimensionless targets, in dB, as functions of their own normalized frequency. | |
| double | thickness_shape_db (double x) noexcept |
| const TapeLossShapes & | tape_loss_shapes () |
| The process-wide fitted shapes. | |
| TapeLossShapes | fit_tape_loss_shapes () |
| The offline derivation. | |
| 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 spans the same amount of TIME on every host. | |
| 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. | |
| double | physical_tape_effective_feedback (double requested_feedback, double age) noexcept |
| Apply the physical tier's measured whole-loop stability calibration. | |
Variables | |
| constexpr double | kPi = 3.14159265358979323846 |
| constexpr double | kDenormalFloor = 1e-18 |
| constexpr double | kMaxDelayMs = 2000.0 |
| Maximum left-channel base delay and stereo-offset bounds. | |
| constexpr double | kTimeOffsetMin = 0.5 |
| constexpr double | kTimeOffsetMax = 1.5 |
| constexpr double | kMaxAddressableDelayMs = kMaxDelayMs * kTimeOffsetMax |
| constexpr int | kControlInterval = 32 |
| Coefficient recomputation and stochastic-walk stepping cadence, in samples. | |
| constexpr double | kParamGlideS = 0.005 |
| De-zipper glide for scalar params (feedback, crossfeed, duck, freeze, and the character table's per-sample outputs), in seconds. | |
| constexpr double | kUnsaturatedFeedbackMax = 0.98 |
| Feedback ceiling for the characters with NO in-loop saturator. | |
| constexpr double | kSaturatedFeedbackMax = 1.1 |
| Feedback ceiling for the saturating characters. | |
| constexpr double | kModRateMinHz = 0.05 |
| Generic modulation LFO: log rate map and maximum depth as a fraction of delay time. | |
| constexpr double | kModRateMaxHz = 10.0 |
| constexpr double | kModMaxDepth = 0.05 |
| constexpr double | kLoopToneHpMinHz = 20.0 |
| User-facing in-loop tone stage. | |
| constexpr double | kLoopToneHpMaxHz = 2000.0 |
| constexpr double | kLoopToneLpMinHz = 200.0 |
| constexpr double | kLoopToneLpMaxHz = 20000.0 |
| constexpr double | kLoopToneNyquistFraction = 0.45 |
| Keep the TPT prewarp strictly below Nyquist. | |
| constexpr double | kLoopToneResMin = 0.5 |
| Resonance range for both loop-tone filters. | |
| constexpr double | kLoopToneResMax = 2.0 |
| constexpr double | kStereoDecorr = 0.95 |
| Right-channel depth scaler for every shared-LFO modulation. | |
| constexpr std::uint32_t | kPrngSeed = 74207u |
| PRNG seed. | |
| constexpr double | kTimeSlewCleanMs = 15.0 |
| constexpr double | kTimeSlewVintageMs = 50.0 |
| constexpr double | kTimeSlewTapeMs = 250.0 |
| constexpr double | kTimeSlewBbdMs = 80.0 |
| constexpr double | kTimeSlewDiffusionMs = 15.0 |
| constexpr std::array< double, 3 > | kCleanAxis = {0.0, 0.5, 1.0} |
| constexpr std::array< double, 3 > | kCleanLoopLpHz = {20000.0, 12000.0, 6000.0} |
| constexpr std::array< double, 3 > | kCleanLoopHpHz = {20.0, 20.0, 120.0} |
| constexpr double | kLoopLpBypassHz = 20000.0 |
| A loop lowpass at or above this cutoff is BYPASSED rather than run. | |
| constexpr std::array< double, 4 > | kVintageAxis = {0.0, 0.33, 0.67, 1.0} |
| constexpr std::array< double, 4 > | kVintageInternalRateHz |
| constexpr std::array< double, 4 > | kVintageBits = {14.0, 12.0, 12.0, 10.0} |
| constexpr double | kVintageAntiAliasFraction = 0.45 |
| Anti-alias / reconstruction cutoff as a fraction of the internal rate. | |
| constexpr double | kVintageEmphasisHz = 3183.0 |
| 3183 Hz is the 50 µs time constant — the published CD/FM-era emphasis standard (IEC 60908), not a tuned value. | |
| constexpr double | kVintageEmphasisDb = 9.0 |
| constexpr std::array< double, 4 > | kTapeAxis = {0.0, 0.33, 0.67, 1.0} |
| constexpr std::array< double, 4 > | kTapeDrive = {1.5, 2.5, 4.0, 6.0} |
| constexpr std::array< double, 4 > | kTapeBias = {0.0, 0.10, 0.20, 0.30} |
| constexpr std::array< double, 4 > | kTapeWowDepthMs = {0.0, 0.15, 0.5, 1.2} |
| constexpr std::array< double, 4 > | kTapeFlutterDepthMs = {0.0, 0.05, 0.15, 0.4} |
| constexpr std::array< double, 4 > | kTapeLossLpHz = {12000.0, 9000.0, 6000.0, 3500.0} |
| constexpr std::array< double, 4 > | kTapeBumpDb = {1.5, 2.0, 3.0, 4.0} |
| constexpr std::array< double, 4 > | kTapePhysicalFeedbackCompensation |
| Physical-loop compensation at each age knot. | |
| constexpr double | kTapeEmphasisDb = 3.0 |
| Record/playback EQ bracketing the saturator: boost HF into saturation, cut it after, so the saturator works the highs harder than the lows. | |
| constexpr double | kTapeEmphasisHz = 1600.0 |
| constexpr double | kTapeBumpHz = 60.0 |
| Loss stand-in head bump (the physical tier replaces this with a speed-dependent center; see kHeadBumpIps/kHeadBumpHz). | |
| constexpr double | kTapeBumpQ = 1.5 |
| constexpr double | kTapeDcBlockHz = 15.0 |
| In-loop DC blocker. | |
| constexpr double | kWowRateHz = 1.0 |
| Wow: periodic + stochastic. | |
| constexpr double | kWowOuTheta = 1.5 |
| constexpr double | kWowOuSigma = 0.6 |
| constexpr double | kWowOuNoiseLpHz = 10.0 |
| constexpr double | kFlutterBaseHz = 8.0 |
| Flutter: published band 5–100 Hz. | |
| constexpr std::array< double, 3 > | kFlutterAmplitudes = {1.0, 0.5, 0.25} |
| constexpr std::array< double, 3 > | kFlutterPhases = {0.0, 0.33, 0.71} |
| constexpr double | kFlutterPhaseRight = 0.41 |
| Right-channel flutter phase offset, in cycles. | |
| constexpr std::array< double, 3 > | kBbdAxis = {0.0, 0.5, 1.0} |
| constexpr std::array< double, 3 > | kBbdStages = {4096.0, 2048.0, 1024.0} |
| PUBLISHED chip stage counts (Panasonic MN3005 / MN3008 / MN3007 datasheets), not design parameters: they are what sets each device's delay-per-clock and therefore its bandwidth at a given time. | |
| constexpr std::array< double, 3 > | kBbdDrive = {0.10, 0.30, 0.50} |
| constexpr std::array< double, 3 > | kBbdJitterAmount = {0.0, 0.3, 0.6} |
| constexpr std::array< double, 3 > | kBbdFlutterDepth = {0.0, 0.005, 0.015} |
| constexpr std::size_t | kBbdMaxStages = 4096 |
| Largest stage count any table knot may request; sizes the bucket buffer. | |
| constexpr int | kBbdOversample = 16 |
| Internal oversampling of the clocked core. | |
| constexpr double | kBbdBandwidthDivisor = 3.0 |
| Bandwidth law: the usable passband must sit well below the clock's Nyquist (the published aliasing constraint in both BBD papers). | |
| constexpr double | kBbdBandwidthMinHz = 300.0 |
| constexpr double | kBbdBandwidthMaxHz = 10000.0 |
| constexpr double | kBbdFilterCutoffScale = 2.1 |
| The clock law names the complete wet path's audible -3 dB edge. | |
| constexpr double | kBbdCompanderTauMs = 10.0 |
| Compander: hardware τ is set by an external capacitor, so real units vary. | |
| constexpr double | kBbdCompanderFloor = 0.01 |
| constexpr double | kBbdCompanderDriveScale = 0.3 |
| The compander holds loop level near the rail, so the waveshaper's drive is backed off when it runs behind one. | |
| constexpr double | kBbdDriveGainSpan = 9.0 |
| Asymmetric soft clip: gain map and DC offset for even-harmonic content. | |
| constexpr double | kBbdShape = 0.25 |
| constexpr double | kBbdJitterMax = 5e-4 |
| Clock jitter, as a fraction of the clock period. [1e-4–2e-3]. | |
| constexpr double | kBbdJitterSmoothness = 0.5 |
| constexpr double | kBbdFlutterRateBaseHz = 0.3 |
| BBD flutter rate map, Hz. | |
| constexpr double | kBbdFlutterRateSpanHz = 1.2 |
| constexpr std::array< double, 4 > | kDiffusionStageMs |
| constexpr std::array< double, 4 > | kDiffusionStageGain = {0.75, 0.75, 0.625, 0.625} |
| constexpr std::array< double, 3 > | kDiffusionAxis = {0.0, 0.5, 1.0} |
| constexpr std::array< double, 3 > | kDiffusionSizeScale = {0.5, 1.0, 1.7} |
| constexpr std::array< double, 3 > | kDiffusionGainScale = {0.7, 1.0, 1.12} |
| constexpr double | kDiffusionGainMax = 0.85 |
| Stability / metallicity guard. | |
| constexpr double | kDiffusionModDepthSamples = 2.0 |
| Anti-metallic modulation of stages 1 and 3 (Dattorro modulates his diffusers). | |
| constexpr std::array< double, 2 > | kDiffusionModRatesHz = {0.40, 0.53} |
| constexpr double | kDiffusionLoopHpHz = 20.0 |
| Diffusion loop filters. [LP is a design parameter]. | |
| constexpr double | kDiffusionLoopLpHz = 9000.0 |
| constexpr std::array< double, 3 > | kDiffusionTankAxis = {0.0, 0.5, 1.0} |
| constexpr std::array< double, 2 > | kDiffusionTankDelayAMs = {30.51, 44.79} |
| Branch delays in ms, mutually prime-ish so their comb nulls never align. | |
| constexpr std::array< double, 2 > | kDiffusionTankDelayBMs = {37.13, 53.27} |
| constexpr std::array< double, 2 > | kDiffusionTankApAMs = {22.58, 60.48} |
| The two decay-diffuser allpasses per branch. [design parameters]. | |
| constexpr std::array< double, 2 > | kDiffusionTankApBMs = {30.24, 89.24} |
| constexpr double | kDiffusionTankApGain1 = 0.70 |
| constexpr double | kDiffusionTankApGain2 = 0.50 |
| constexpr std::array< double, 3 > | kDiffusionTankSizeScale = {0.6, 1.0, 1.6} |
| Tank size scaler and recirculation decay by character amount. | |
| constexpr std::array< double, 3 > | kDiffusionTankDecay = {0.0, 0.62, 0.80} |
| constexpr std::array< double, 3 > | kDiffusionTankMix = {0.0, 0.55, 0.85} |
| How much of the character's output is the tank rather than the bare diffuser. | |
| constexpr std::array< double, 3 > | kDiffusionTankDampHz = {12000.0, 7000.0, 4200.0} |
| In-tank damping. | |
| constexpr std::array< double, 3 > | kDiffusionTankModDepth = {0.0, 14.0, 42.0} |
| Tank modulation — this is what makes the cloud granular rather than static. | |
| constexpr std::array< double, 2 > | kDiffusionTankModRatesHz = {0.83, 1.27} |
| constexpr std::array< double, 2 > | kDiffusionTankTapFraction = {0.37, 0.71} |
| Extra scattered taps read at their own modulated offsets, as a fraction of the branch delay. | |
| constexpr double | kDiffusionTankLowGuardHz = 90.0 |
| In-tank low cut. | |
| constexpr double | kDiffusionTankDelaySlewMs = 5.0 |
| Live character automation moves tank taps toward their new positions over a short continuous glide instead of changing fractional reads at control-rate boundaries. | |
| constexpr int | kReverseFadeSamples = 96 |
| Raised-cosine splice fade at each end of a reversed segment, in samples. | |
| constexpr double | kReverseLineMs = 30.0 |
| Delay the CLOCK-DOMAIN characters' own line runs at while reverse is engaged, in milliseconds. | |
| constexpr double | kDuckAttackS = 0.005 |
| constexpr double | kDuckReleaseS = 0.250 |
| constexpr double | kDuckThreshold = 0.1 |
| constexpr double | kJaSaturationMagnetization = 3.5e5 |
| PUBLISHED γ-Fe2O3 material constants (Jiles, Thoelke & Devine 1992, reproduced in Chowdhury 2019). | |
| constexpr double | kJaAnhystereticShape = 2.2e4 |
| constexpr double | kJaPinning = 2.7e4 |
| constexpr double | kJaReversibility = 0.2 |
| constexpr double | kJaInterdomainCoupling = 1.6e-3 |
| constexpr int | kJaMaxIterations = 8 |
| Newton-Raphson budget. | |
| constexpr double | kJaConvergenceThreshold = 6e-8 |
| constexpr double | kJaLangevinClamp = 30.0 |
| Langevin argument clamp — keeps coth() and its derivative in the range where the rational approximation is accurate and the solver well-scaled. | |
| constexpr double | kJaSilenceThreshold = 1e-7 |
| Below this |H| and |dH/dt|, magnetization is snapped to 0 so a silent loop cannot hold a frozen DC magnetization. | |
| constexpr std::size_t | kHysteresisHalfBandTaps = kHouseHalfBandTaps |
| Hysteresis oversampling factor and half-band design. | |
| constexpr double | kHysteresisHalfBandStopbandDb = kHouseHalfBandStopbandDb |
| constexpr double | kJaMakeupToleranceDb = 1.0 |
| Makeup-gain tolerance around unity small-signal gain, in dB. | |
| constexpr std::size_t | kLossFirOrder48k = 96 |
| Wallace playback loss (Wallace 1951; physical ranges from Bertram 1994) is split across two realizations — see tape_loss.hpp for why. | |
| constexpr int | kLossIirFitPoints = 64 |
| The smooth spacing × thickness tilt goes to a fitted IIR cascade: how many log-spaced points the least-squares fit is evaluated at, and how many simplex iterations it gets. | |
| constexpr std::size_t | kSpacingPoleSections = 3 |
| Sections in each fitted cascade. | |
| constexpr std::size_t | kSpacingShelfSections = 8 |
| constexpr std::size_t | kThicknessPoleSections = 1 |
| constexpr std::size_t | kThicknessShelfSections = 2 |
| constexpr std::size_t | kLossPoleSections |
| constexpr std::size_t | kLossShelfSections |
| constexpr std::size_t | kLossSearchDimension |
| Search dimension of ONE shape fit (the larger of the two). | |
| constexpr std::array< double, kSpacingPoleSections > | kSpacingShapePoleX |
| THE FITTED SHAPES, as shipped. | |
| constexpr std::array< double, kSpacingShelfSections > | kSpacingShapeShelfX |
| constexpr std::array< double, kSpacingShelfSections > | kSpacingShapeShelfDb |
| constexpr std::array< double, kThicknessPoleSections > | kThicknessShapePoleX |
| constexpr std::array< double, kThicknessShelfSections > | kThicknessShapeShelfX |
| constexpr std::array< double, kThicknessShelfSections > | kThicknessShapeShelfDb |
| constexpr double | kLossShapeMinX = 1e-3 |
| Normalized-frequency range each shape is fitted over, and how many log-spaced points. | |
| constexpr double | kLossShapeMaxX = 4.0 |
| constexpr int | kLossIirFitIterations = 4000 |
| Simplex iterations per start, restarts per start, and how many starts. | |
| constexpr int | kLossIirFitRestarts = 3 |
| constexpr int | kLossIirFitStarts = 16 |
| constexpr double | kLossShelfMinDb = -40.0 |
| Deepest cut any one shelf may ask for. | |
| constexpr double | kLossBankCrossfadeS = 0.030 |
| NOTE: there are no age knots and no interpolation along the age axis. | |
| constexpr double | kTapeCoatingThicknessM = 12e-6 |
| Fixed tape geometry, in metres. | |
| constexpr double | kTapeHeadGapM = 3e-6 |
| constexpr std::array< double, 5 > | kTapeSpeedsIps = {1.875, 3.75, 7.5, 15.0, 30.0} |
| The IEC/NAB standard tape-speed set, in inches per second (published). | |
| constexpr std::array< double, 5 > | kHeadBumpIps = {1.875, 3.75, 7.5, 15.0, 30.0} |
| Head-bump center against speed. | |
| constexpr std::array< double, 5 > | kHeadBumpHz = {35.0, 45.0, 60.0, 80.0, 105.0} |
| constexpr std::array< double, 3 > | kAgeAxis = {0.0, 0.5, 1.0} |
| Age macro. | |
| constexpr std::array< double, 3 > | kAgeSpacingUm = {5.0, 12.0, 25.0} |
| constexpr std::array< double, 3 > | kAgeHissDbfs = {-120.0, -78.0, -60.0} |
| constexpr std::array< double, 3 > | kAgeChewDepth = {0.0, 0.2, 0.6} |
| constexpr std::array< double, 3 > | kAgeDegrade = {0.0, 0.2, 0.5} |
| constexpr double | kHissLpHz = 4000.0 |
| Hiss: white noise through a 1-pole lowpass, injected AFTER the delay line so it accumulates per repeat the way real tape self-noise does. | |
| constexpr double | kChewCleanScaleS = 1.5 |
| Chew — an intermittent dropout modeled as a two-state machine. | |
| constexpr double | kChewDegradedScaleS = 0.4 |
| constexpr double | kChewVariance = 1.5 |
| constexpr double | kChewTransitionS = 0.008 |
| constexpr double | kChewPower = 2.0 |
| constexpr double | kChewLpHz = 3000.0 |
| constexpr double | kDegradeMinLpHz = 500.0 |
| Degrade — continuous wear. | |
| constexpr double | kDegradeGainDipDb = -1.5 |
| constexpr int | kDegradeUpdateSamples = 1024 |
| constexpr double | kTapeLossFloorDb = -60.0 |
| Floor applied to a modeled loss magnitude before anything is fitted to it. | |
The two depths the module has a measured reason to name.
8x is a DELIBERATE deviation from the reference. Chowdhury (DAFx-19, §4.2) reports 2x acceptable at moderate drive, 4x good across the range, and >= 8x diminishing returns, and this module's spec adopted 4x on that basis. At MAX drive it does not hold: the exact-bin 2947 Hz acceptance measurement found a folded high-order product at about -40 dBc, against a -60 dBc contract. One more house half-band stage moves the nonlinear solve to 8x and clears that contract without retuning the magnetic model's drive, which is why the shipping depth is 8x and not the cited 4x. Do not "optimize" it back down.
4x is retained because the acceptance test runs it as the direct regression control for that same measurement — it must stay bit-identical to the first two stages of the shipping cascade, which naming it as a depth of the one template guarantees.
| using pulp::signal::chardelay::OversampledHysteresis4x = OversampledHysteresis<HalfBandOversampler4x> |
| using pulp::signal::chardelay::OversampledHysteresis8x = OversampledHysteresis<HysteresisOversampler8x> |
| using pulp::signal::chardelay::SpacingShape = LossShape<kSpacingPoleSections, kSpacingShelfSections> |
| using pulp::signal::chardelay::ThicknessShape = LossShape<kThicknessPoleSections, kThicknessShelfSections> |
|
inline |
Design a minimum-phase FIR realizing the GAP term only.
Minimum phase rather than linear phase because this runs inside the feedback loop: a linear-phase kernel of this length would add its whole group delay to every recirculation. The cepstral fold is the standard construction.
Allocating; control thread only.
|
inlinenoexcept |
|
inline |
The offline derivation.
Control thread, and slow — the acceptance suite uses it to prove the shipped table still reproduces the physics.
|
inlinenoexcept |
|
inlinenoexcept |
|
inlinenoexcept |
Apply the physical tier's measured whole-loop stability calibration.
Sub-unity feedback retains the measured margin at the selected tape age. The explicit 1.0–1.1 range releases that margin continuously so the maximum setting still reaches the physical model's bounded self-oscillation point. requested_feedback is clamped to that 0–1.1 character contract first: the release ramp is defined only across it, and extrapolating past 1.1 would hand the loop more than unity gain with no saturator margin left to bound it.
|
inlinenoexcept |
The two dimensionless targets, in dB, as functions of their own normalized frequency.
|
inline |
FIR length at a given sample rate: the specified order at 48 kHz, scaled with the rate so the filter spans the same amount of TIME on every host.
|
inlinenoexcept |
The gap null. Carried by the minimum-phase FIR.
|
inlinenoexcept |
Magnitude of the cascade, in dB. Used by the fitter and by tests.
|
inlinenoexcept |
The complete modeled response.
|
inlinenoexcept |
The response the realization actually targets, floor included.
The acceptance suite compares the realized cascade against THIS, not against the unbounded physics.
|
inline |
The process-wide fitted shapes.
Fitting is a multi-start simplex search and takes a moment; doing it once is possible only because the shapes are dimensionless — they are properties of the PHYSICS, not of any particular machine, sample rate or setting. Every instance, at every speed and every age, is that same pair of shapes with their frequency axes scaled.
|
inlinenoexcept |
The smooth tilt: spacing × thickness. Carried by the IIR cascade.
|
inlinenoexcept |
|
inlinenoexcept |
|
inlineconstexpr |
Age macro.
In the physical tier character_amount becomes the age axis; wow and flutter depths reuse kTapeTable's columns. [all design parameters]
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Bandwidth law: the usable passband must sit well below the clock's Nyquist (the published aliasing constraint in both BBD papers).
The /3 margin keeps a 2-pole transition band below f_clock/2. [limits are design parameters]
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
The compander holds loop level near the rail, so the waveshaper's drive is backed off when it runs behind one.
[0.2–0.5]
|
inlineconstexpr |
|
inlineconstexpr |
Compander: hardware τ is set by an external capacitor, so real units vary.
[1–20 ms]. The floor is a divide-by-near-zero guard, not a tuned value.
|
inlineconstexpr |
|
inlineconstexpr |
Asymmetric soft clip: gain map and DC offset for even-harmonic content.
|
inlineconstexpr |
The clock law names the complete wet path's audible -3 dB edge.
Two cascaded filters plus the clocked hold place that edge below either filter's coefficient cutoff, so calibrate the implementation cutoff upward while leaving bandwidth_hz() as the externally meaningful target.
|
inlineconstexpr |
|
inlineconstexpr |
BBD flutter rate map, Hz.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Clock jitter, as a fraction of the clock period. [1e-4–2e-3].
|
inlineconstexpr |
|
inlineconstexpr |
Largest stage count any table knot may request; sizes the bucket buffer.
|
inlineconstexpr |
Internal oversampling of the clocked core.
Must comfortably exceed the clock rate across the delay range. [8–24]
|
inlineconstexpr |
|
inlineconstexpr |
PUBLISHED chip stage counts (Panasonic MN3005 / MN3008 / MN3007 datasheets), not design parameters: they are what sets each device's delay-per-clock and therefore its bandwidth at a given time.
|
inlineconstexpr |
Chew — an intermittent dropout modeled as a two-state machine.
Durations are drawn as scale × r^variance from the shared PRNG.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Coefficient recomputation and stochastic-walk stepping cadence, in samples.
32 at 48 kHz is 1.5 kHz of control bandwidth — far above any modulation in this module and far below the cost of doing it per sample.
|
inlineconstexpr |
|
inlineconstexpr |
Degrade — continuous wear.
Bandwidth falls exponentially toward this floor, with a small noise floor and gain dip at maximum.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Stability / metallicity guard.
The published useful range for a Schroeder allpass coefficient tops out well below 1.
|
inlineconstexpr |
|
inlineconstexpr |
Diffusion loop filters. [LP is a design parameter].
|
inlineconstexpr |
|
inlineconstexpr |
Anti-metallic modulation of stages 1 and 3 (Dattorro modulates his diffusers).
Depths in samples, rates in Hz. [design parameters]
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
The two decay-diffuser allpasses per branch. [design parameters].
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
In-tank damping.
A reverb without it is a bright metallic ring; rolling the recirculation off is what makes successive passes sound like air.
|
inlineconstexpr |
|
inlineconstexpr |
Branch delays in ms, mutually prime-ish so their comb nulls never align.
[design parameters; Dattorro's tank ratios at a nominal room size]
|
inlineconstexpr |
|
inlineconstexpr |
Live character automation moves tank taps toward their new positions over a short continuous glide instead of changing fractional reads at control-rate boundaries.
|
inlineconstexpr |
In-tank low cut.
The damping filter is a LOWPASS, so without this the low end recirculates undamped and the tank's worst resonant mode sits below 100 Hz, where a flat energy estimate never sees it — measured as a tail that GREW a thousandfold after the input stopped. Every recirculation must lose a little low end, exactly as it loses a little top. [design parameter]
|
inlineconstexpr |
How much of the character's output is the tank rather than the bare diffuser.
At 0 the tank is silent AND unmixed, so the character is the published allpass diffuser and nothing else.
|
inlineconstexpr |
Tank modulation — this is what makes the cloud granular rather than static.
Deeper and faster than the input diffuser's anti-metallic wobble: each branch's first allpass is swept, which scatters the recirculating energy in time and turns a smooth tail into a shimmer of grains. Depth in samples.
|
inlineconstexpr |
|
inlineconstexpr |
Tank size scaler and recirculation decay by character amount.
The decay ceiling is deliberately below unity because the output-only tank still owns an internal figure-of-eight recirculation; unity there would make the cloud an oscillator rather than a reverb.
|
inlineconstexpr |
Extra scattered taps read at their own modulated offsets, as a fraction of the branch delay.
Density without another allpass in the loop.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Flutter: published band 5–100 Hz.
Rotational components produce harmonic structure, so a small harmonic stack; count, ratios, amplitudes and phases are all design parameters.
|
inlineconstexpr |
Right-channel flutter phase offset, in cycles.
Deterministic but uncorrelated with the left channel.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Head-bump center against speed.
The qualitative behavior (bump rises with speed) is published; the mapping is a design parameter.
|
inlineconstexpr |
Hiss: white noise through a 1-pole lowpass, injected AFTER the delay line so it accumulates per repeat the way real tape self-noise does.
|
inlineconstexpr |
|
inlineconstexpr |
Hysteresis oversampling factor and half-band design.
4× is Chowdhury's "good across the range" finding; ≥8× shows diminishing returns. 65 taps at Kaiser β ≈ 8 is the house half-band design — the stopband figure below is the β = 8 point of the standard Kaiser formula, not an independent choice.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Langevin argument clamp — keeps coth() and its derivative in the range where the rational approximation is accurate and the solver well-scaled.
|
inlineconstexpr |
Makeup-gain tolerance around unity small-signal gain, in dB.
|
inlineconstexpr |
Newton-Raphson budget.
The convergence threshold is referenced to the 24-bit noise floor (≈ −144 dBFS) in the normalized magnetization units the solver works in. [iteration cap is a design parameter, 4–8]
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
PUBLISHED γ-Fe2O3 material constants (Jiles, Thoelke & Devine 1992, reproduced in Chowdhury 2019).
Units are A/m except the dimensionless c and α. The user-facing drive/saturation/bias macros re-map onto a, M_s and c; k and α are held at their published values.
|
inlineconstexpr |
Below this |H| and |dH/dt|, magnetization is snapped to 0 so a silent loop cannot hold a frozen DC magnetization.
|
inlineconstexpr |
A loop lowpass at or above this cutoff is BYPASSED rather than run.
That makes the character_amount = 0 configuration an exact null-test baseline: one pass through the wet path then differs from a textbook Lagrange delay by the 20 Hz DC-removal highpass and nothing else, which is the property the clean character is specified to have.
|
inlineconstexpr |
|
inlineconstexpr |
User-facing in-loop tone stage.
This is the player's tone control, distinct from the character-owned loop_highpass/loop_lowpass that model each realization's own bandwidth. It sits in the recirculation path, so the filters compound once per repeat and the tail darkens (or thins) progressively rather than being shaped once on the way out.
The stage is BYPASSED, and bit-exact, while low cut sits at its minimum and high cut at its maximum — an untouched delay is unchanged.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Keep the TPT prewarp strictly below Nyquist.
At or above Nyquist tan() is periodic rather than a meaningful analog-frequency mapping.
|
inlineconstexpr |
|
inlineconstexpr |
Resonance range for both loop-tone filters.
0.5 is gently damped, 0.707 is Butterworth (flat), and 2.0 is a clear but controlled peak. The ceiling is deliberately low: these filters sit in a feedback loop that already has its own gain. CharacterDelay reduces the unsaturated feedback ceiling by the active filters' worst-case resonant peaks so this range remains stable.
|
inlineconstexpr |
NOTE: there are no age knots and no interpolation along the age axis.
Both loss terms are exact frequency SCALINGS of a fixed dimensionless shape — the spacing term depends on frequency only through f·d/v and the thickness term only through f·δ/v — so a shape fitted once is correct at every spacing and every speed with its corners multiplied by a scalar. That removes the interpolation entirely, which matters: an earlier design that fitted the combined response at nine age knots and interpolated the parameters between them was inside 1 dB AT the knots and 3-4 dB BETWEEN them, because two fits of nearly the same curve can sit in different parameter basins and blending two descriptions describes neither. Crossfade applied when the tape SPEED changes and both filters are redesigned. Two complete instances run and their OUTPUTS are crossfaded. [20–50 ms]
|
inlineconstexpr |
Wallace playback loss (Wallace 1951; physical ranges from Bertram 1994) is split across two realizations — see tape_loss.hpp for why.
The gap null goes to a minimum-phase FIR: order at 48 kHz, scaled ∝ fs. [64–128]
|
inlineconstexpr |
Simplex iterations per start, restarts per start, and how many starts.
The shapes are DIMENSIONLESS: they do not depend on the sample rate, the tape speed or the spacing, only on the form of the physics. So they are fitted exactly once per process (see tape_loss_shapes()) and every instance shares the result — which is what lets this be as thorough as it is without costing anything at plugin load.
|
inlineconstexpr |
The smooth spacing × thickness tilt goes to a fitted IIR cascade: how many log-spaced points the least-squares fit is evaluated at, and how many simplex iterations it gets.
The fit runs on the control thread, once per age knot per tape speed.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Search dimension of ONE shape fit (the larger of the two).
|
inlineconstexpr |
|
inlineconstexpr |
Normalized-frequency range each shape is fitted over, and how many log-spaced points.
The range covers every (frequency, speed, spacing) combination the module can reach.
|
inlineconstexpr |
Deepest cut any one shelf may ask for.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Maximum left-channel base delay and stereo-offset bounds.
Buffer storage is sized from the derived maximum right-channel delay at set_sample_rate().
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Generic modulation LFO: log rate map and maximum depth as a fraction of delay time.
[design parameters; depth range 0.02–0.1]
|
inlineconstexpr |
De-zipper glide for scalar params (feedback, crossfeed, duck, freeze, and the character table's per-sample outputs), in seconds.
|
inlineconstexpr |
|
inlineconstexpr |
PRNG seed.
[design parameter — the requirement is determinism, not the value.] Streams that must not correlate derive their own seeds from it.
|
inlineconstexpr |
Raised-cosine splice fade at each end of a reversed segment, in samples.
[32–256]
|
inlineconstexpr |
Delay the CLOCK-DOMAIN characters' own line runs at while reverse is engaged, in milliseconds.
[design parameter, 10–50]
BBD and Vintage own a delay line the reverse segmenter cannot replace, so in reverse the segmenter sits at the character's input and the two delays add. Letting the clocked line keep the requested time would make the total twice what the caller asked for, which breaks every tempo-synced patch — the layer above multiplies milliseconds and trusts the module. So the segment takes the full requested time and the line runs at this fixed, short value: long enough for the compander and the quantizer to imprint each pass, short enough that the total stays ≈ the requested time.
Tonal consequence, deliberate and flagged for the listening pass: the bandwidth law ties usable passband to clock rate, so a 30 ms line sits near the bright end and reverse-BBD is brighter than forward-BBD at the same time setting. Wanting a darker reverse is a kBbdBandwidthMaxHz data change, not a logic change.
|
inlineconstexpr |
Feedback ceiling for the saturating characters.
Above unity is deliberate: driving a saturating loop past 1.0 is the classic self-oscillating dub behavior, bounded by the loop saturator rather than by a clamp.
|
inlineconstexpr |
Sections in each fitted cascade.
The counts are set by MEASUREMENT, not by taste. The spacing term is exp(-k·d), whose magnitude in dB is linear in FREQUENCY — on a log-frequency axis a curve whose slope steepens without bound, while every filter section contributes a bounded 20 dB/decade. Brute-forced worst-case error against the analytic magnitude:
2 poles + 1 shelf 7.5 ips 2.2 dB 3.75 ips 5.3 dB 1.875 ips 5.3 dB 2 poles + 2 shelves 1.2 2.7 2.5 2 poles + 3 shelves 0.8 1.9 1.5 2 poles + 5 shelves 0.3 0.9 0.6
Five shelves is where +-1 dB is reached across the speed range. The thickness term is far gentler — it falls asymptotically at 20 dB/decade, so it is nearly one pole already — and needs only three sections.
|
inlineconstexpr |
THE FITTED SHAPES, as shipped.
These are constants of the PHYSICS, not of any machine or setting: the spacing term depends on frequency only through f·d/v and the thickness term only through f·δ/v, so one fit of each dimensionless curve is correct at every tape speed, every head-tape spacing and every sample rate, with its corners multiplied by a scalar.
They are shipped rather than fitted at load because the fit is a multi-start simplex search that takes over a second — fine as an offline derivation, unacceptable as a stall when a plugin is instantiated. The derivation is not lost: fit_tape_loss_shapes() still performs it, and the acceptance suite re-derives the fit and checks that these values reproduce the same response, so regenerating the table is a supported operation rather than a rewrite.
Worst-case error against the analytic curves: spacing 0.36 dB, thickness 0.12 dB, both equiripple (which is the signature of a minimax fit sitting at its structural optimum for the section count).
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Right-channel depth scaler for every shared-LFO modulation.
One oscillator with slightly unequal depths reads as natural width without the slow phase drift two free-running oscillators produce. [design parameter, 0.9–0.99]
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Loss stand-in head bump (the physical tier replaces this with a speed-dependent center; see kHeadBumpIps/kHeadBumpHz).
|
inlineconstexpr |
|
inlineconstexpr |
Fixed tape geometry, in metres.
Coating thickness and head gap sit inside Bertram's published physical ranges (thickness 1–20 µm, gap 0.5–10 µm); the specific values are design parameters. Spacing is the age axis.
|
inlineconstexpr |
In-loop DC blocker.
[10–20 Hz, the published range for tape-machine playback electronics]
|
inlineconstexpr |
|
inlineconstexpr |
Record/playback EQ bracketing the saturator: boost HF into saturation, cut it after, so the saturator works the highs harder than the lows.
The pair is exactly inverse, so with the saturator bypassed it is transparent.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Floor applied to a modeled loss magnitude before anything is fitted to it.
The physics is unbounded — the spacing term passes −400 dB inside the audio band at the worn end — and fitting to that wastes the model's degrees of freedom on a region that is already inaudible. −60 dB is far enough down to be gone inside a feedback loop that is itself losing energy every pass.
|
inlineconstexpr |
|
inlineconstexpr |
Physical-loop compensation at each age knot.
This combines the inverse head-bump gain with the measured 4/9 stability margin required by the exact 8x oversampled hysteresis path. The engine applies it through unity, then releases it across the explicit 1.0–1.1 over-unity range so maximum feedback still self-oscillates deliberately.
|
inlineconstexpr |
The IEC/NAB standard tape-speed set, in inches per second (published).
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Feedback ceiling for the characters with NO in-loop saturator.
Without a bounding nonlinearity, unity feedback is a marginally stable integrator, so the clamp — not a NaN sanitizer — is what guarantees decay.
|
inlineconstexpr |
Anti-alias / reconstruction cutoff as a fraction of the internal rate.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
3183 Hz is the 50 µs time constant — the published CD/FM-era emphasis standard (IEC 60908), not a tuned value.
The shelf AMOUNT is a design parameter [6–12 dB].
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |
Wow: periodic + stochastic.
The 0.5–6 Hz band is published (Chowdhury 2019 §3); the specific rate and the Ornstein-Uhlenbeck walk parameters are design choices within it.