Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::signal::chardelay Namespace Reference

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 &parameters, 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.

Typedef Documentation

◆ HalfBandOversampler4x

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.

◆ HysteresisOversampler8x

◆ OversampledHysteresis4x

◆ OversampledHysteresis8x

◆ SpacingShape

◆ ThicknessShape

Function Documentation

◆ design_tape_gap_fir()

std::vector< double > pulp::signal::chardelay::design_tape_gap_fir ( double fs,
std::size_t taps,
const TapeLossGeometry & geometry )
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.

◆ fast_tanh()

double pulp::signal::chardelay::fast_tanh ( double v)
inlinenoexcept

◆ fit_tape_loss_shapes()

TapeLossShapes pulp::signal::chardelay::fit_tape_loss_shapes ( )
inline

The offline derivation.

Control thread, and slow — the acceptance suite uses it to prove the shipped table still reproduces the physics.

◆ flush_denormal()

double pulp::signal::chardelay::flush_denormal ( double v)
inlinenoexcept

◆ interpolate_knots()

template<std::size_t N>
double pulp::signal::chardelay::interpolate_knots ( const std::array< double, N > & axis,
const std::array< double, N > & values,
double c )
inlinenoexcept

◆ physical_tape_effective_feedback()

double pulp::signal::chardelay::physical_tape_effective_feedback ( double requested_feedback,
double age )
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.

◆ spacing_shape_db()

double pulp::signal::chardelay::spacing_shape_db ( double x)
inlinenoexcept

The two dimensionless targets, in dB, as functions of their own normalized frequency.

◆ tape_gap_fir_taps()

std::size_t pulp::signal::chardelay::tape_gap_fir_taps ( double fs)
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.

◆ tape_loss_gap_magnitude()

double pulp::signal::chardelay::tape_loss_gap_magnitude ( double frequency_hz,
const TapeLossGeometry & geometry )
inlinenoexcept

The gap null. Carried by the minimum-phase FIR.

◆ tape_loss_iir_magnitude_db()

double pulp::signal::chardelay::tape_loss_iir_magnitude_db ( const TapeLossIirParams & parameters,
double frequency_hz )
inlinenoexcept

Magnitude of the cascade, in dB. Used by the fitter and by tests.

◆ tape_loss_magnitude()

double pulp::signal::chardelay::tape_loss_magnitude ( double frequency_hz,
const TapeLossGeometry & geometry )
inlinenoexcept

The complete modeled response.

◆ tape_loss_magnitude_floored()

double pulp::signal::chardelay::tape_loss_magnitude_floored ( double frequency_hz,
const TapeLossGeometry & geometry )
inlinenoexcept

The response the realization actually targets, floor included.

The acceptance suite compares the realized cascade against THIS, not against the unbounded physics.

◆ tape_loss_shapes()

const TapeLossShapes & pulp::signal::chardelay::tape_loss_shapes ( )
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.

◆ tape_loss_smooth_magnitude()

double pulp::signal::chardelay::tape_loss_smooth_magnitude ( double frequency_hz,
const TapeLossGeometry & geometry )
inlinenoexcept

The smooth tilt: spacing × thickness. Carried by the IIR cascade.

◆ tape_wavenumber()

double pulp::signal::chardelay::tape_wavenumber ( double frequency_hz,
double speed_ips )
inlinenoexcept

◆ thickness_shape_db()

double pulp::signal::chardelay::thickness_shape_db ( double x)
inlinenoexcept

Variable Documentation

◆ kAgeAxis

std::array<double, 3> pulp::signal::chardelay::kAgeAxis = {0.0, 0.5, 1.0}
inlineconstexpr

Age macro.

In the physical tier character_amount becomes the age axis; wow and flutter depths reuse kTapeTable's columns. [all design parameters]

◆ kAgeChewDepth

std::array<double, 3> pulp::signal::chardelay::kAgeChewDepth = {0.0, 0.2, 0.6}
inlineconstexpr

◆ kAgeDegrade

std::array<double, 3> pulp::signal::chardelay::kAgeDegrade = {0.0, 0.2, 0.5}
inlineconstexpr

◆ kAgeHissDbfs

std::array<double, 3> pulp::signal::chardelay::kAgeHissDbfs = {-120.0, -78.0, -60.0}
inlineconstexpr

◆ kAgeSpacingUm

std::array<double, 3> pulp::signal::chardelay::kAgeSpacingUm = {5.0, 12.0, 25.0}
inlineconstexpr

◆ kBbdAxis

std::array<double, 3> pulp::signal::chardelay::kBbdAxis = {0.0, 0.5, 1.0}
inlineconstexpr

◆ kBbdBandwidthDivisor

double pulp::signal::chardelay::kBbdBandwidthDivisor = 3.0
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]

◆ kBbdBandwidthMaxHz

double pulp::signal::chardelay::kBbdBandwidthMaxHz = 10000.0
inlineconstexpr

◆ kBbdBandwidthMinHz

double pulp::signal::chardelay::kBbdBandwidthMinHz = 300.0
inlineconstexpr

◆ kBbdCompanderDriveScale

double pulp::signal::chardelay::kBbdCompanderDriveScale = 0.3
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]

◆ kBbdCompanderFloor

double pulp::signal::chardelay::kBbdCompanderFloor = 0.01
inlineconstexpr

◆ kBbdCompanderTauMs

double pulp::signal::chardelay::kBbdCompanderTauMs = 10.0
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.

◆ kBbdDrive

std::array<double, 3> pulp::signal::chardelay::kBbdDrive = {0.10, 0.30, 0.50}
inlineconstexpr

◆ kBbdDriveGainSpan

double pulp::signal::chardelay::kBbdDriveGainSpan = 9.0
inlineconstexpr

Asymmetric soft clip: gain map and DC offset for even-harmonic content.

◆ kBbdFilterCutoffScale

double pulp::signal::chardelay::kBbdFilterCutoffScale = 2.1
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.

◆ kBbdFlutterDepth

std::array<double, 3> pulp::signal::chardelay::kBbdFlutterDepth = {0.0, 0.005, 0.015}
inlineconstexpr

◆ kBbdFlutterRateBaseHz

double pulp::signal::chardelay::kBbdFlutterRateBaseHz = 0.3
inlineconstexpr

BBD flutter rate map, Hz.

◆ kBbdFlutterRateSpanHz

double pulp::signal::chardelay::kBbdFlutterRateSpanHz = 1.2
inlineconstexpr

◆ kBbdJitterAmount

std::array<double, 3> pulp::signal::chardelay::kBbdJitterAmount = {0.0, 0.3, 0.6}
inlineconstexpr

◆ kBbdJitterMax

double pulp::signal::chardelay::kBbdJitterMax = 5e-4
inlineconstexpr

Clock jitter, as a fraction of the clock period. [1e-4–2e-3].

◆ kBbdJitterSmoothness

double pulp::signal::chardelay::kBbdJitterSmoothness = 0.5
inlineconstexpr

◆ kBbdMaxStages

std::size_t pulp::signal::chardelay::kBbdMaxStages = 4096
inlineconstexpr

Largest stage count any table knot may request; sizes the bucket buffer.

◆ kBbdOversample

int pulp::signal::chardelay::kBbdOversample = 16
inlineconstexpr

Internal oversampling of the clocked core.

Must comfortably exceed the clock rate across the delay range. [8–24]

◆ kBbdShape

double pulp::signal::chardelay::kBbdShape = 0.25
inlineconstexpr

◆ kBbdStages

std::array<double, 3> pulp::signal::chardelay::kBbdStages = {4096.0, 2048.0, 1024.0}
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.

◆ kChewCleanScaleS

double pulp::signal::chardelay::kChewCleanScaleS = 1.5
inlineconstexpr

Chew — an intermittent dropout modeled as a two-state machine.

Durations are drawn as scale × r^variance from the shared PRNG.

◆ kChewDegradedScaleS

double pulp::signal::chardelay::kChewDegradedScaleS = 0.4
inlineconstexpr

◆ kChewLpHz

double pulp::signal::chardelay::kChewLpHz = 3000.0
inlineconstexpr

◆ kChewPower

double pulp::signal::chardelay::kChewPower = 2.0
inlineconstexpr

◆ kChewTransitionS

double pulp::signal::chardelay::kChewTransitionS = 0.008
inlineconstexpr

◆ kChewVariance

double pulp::signal::chardelay::kChewVariance = 1.5
inlineconstexpr

◆ kCleanAxis

std::array<double, 3> pulp::signal::chardelay::kCleanAxis = {0.0, 0.5, 1.0}
inlineconstexpr

◆ kCleanLoopHpHz

std::array<double, 3> pulp::signal::chardelay::kCleanLoopHpHz = {20.0, 20.0, 120.0}
inlineconstexpr

◆ kCleanLoopLpHz

std::array<double, 3> pulp::signal::chardelay::kCleanLoopLpHz = {20000.0, 12000.0, 6000.0}
inlineconstexpr

◆ kControlInterval

int pulp::signal::chardelay::kControlInterval = 32
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.

◆ kDegradeGainDipDb

double pulp::signal::chardelay::kDegradeGainDipDb = -1.5
inlineconstexpr

◆ kDegradeMinLpHz

double pulp::signal::chardelay::kDegradeMinLpHz = 500.0
inlineconstexpr

Degrade — continuous wear.

Bandwidth falls exponentially toward this floor, with a small noise floor and gain dip at maximum.

◆ kDegradeUpdateSamples

int pulp::signal::chardelay::kDegradeUpdateSamples = 1024
inlineconstexpr

◆ kDenormalFloor

double pulp::signal::chardelay::kDenormalFloor = 1e-18
inlineconstexpr

◆ kDiffusionAxis

std::array<double, 3> pulp::signal::chardelay::kDiffusionAxis = {0.0, 0.5, 1.0}
inlineconstexpr

◆ kDiffusionGainMax

double pulp::signal::chardelay::kDiffusionGainMax = 0.85
inlineconstexpr

Stability / metallicity guard.

The published useful range for a Schroeder allpass coefficient tops out well below 1.

◆ kDiffusionGainScale

std::array<double, 3> pulp::signal::chardelay::kDiffusionGainScale = {0.7, 1.0, 1.12}
inlineconstexpr

◆ kDiffusionLoopHpHz

double pulp::signal::chardelay::kDiffusionLoopHpHz = 20.0
inlineconstexpr

Diffusion loop filters. [LP is a design parameter].

◆ kDiffusionLoopLpHz

double pulp::signal::chardelay::kDiffusionLoopLpHz = 9000.0
inlineconstexpr

◆ kDiffusionModDepthSamples

double pulp::signal::chardelay::kDiffusionModDepthSamples = 2.0
inlineconstexpr

Anti-metallic modulation of stages 1 and 3 (Dattorro modulates his diffusers).

Depths in samples, rates in Hz. [design parameters]

◆ kDiffusionModRatesHz

std::array<double, 2> pulp::signal::chardelay::kDiffusionModRatesHz = {0.40, 0.53}
inlineconstexpr

◆ kDiffusionSizeScale

std::array<double, 3> pulp::signal::chardelay::kDiffusionSizeScale = {0.5, 1.0, 1.7}
inlineconstexpr

◆ kDiffusionStageGain

std::array<double, 4> pulp::signal::chardelay::kDiffusionStageGain = {0.75, 0.75, 0.625, 0.625}
inlineconstexpr

◆ kDiffusionStageMs

std::array<double, 4> pulp::signal::chardelay::kDiffusionStageMs
inlineconstexpr
Initial value:
= {4.7651, 3.5906, 12.7181,
9.2953}

◆ kDiffusionTankApAMs

std::array<double, 2> pulp::signal::chardelay::kDiffusionTankApAMs = {22.58, 60.48}
inlineconstexpr

The two decay-diffuser allpasses per branch. [design parameters].

◆ kDiffusionTankApBMs

std::array<double, 2> pulp::signal::chardelay::kDiffusionTankApBMs = {30.24, 89.24}
inlineconstexpr

◆ kDiffusionTankApGain1

double pulp::signal::chardelay::kDiffusionTankApGain1 = 0.70
inlineconstexpr

◆ kDiffusionTankApGain2

double pulp::signal::chardelay::kDiffusionTankApGain2 = 0.50
inlineconstexpr

◆ kDiffusionTankAxis

std::array<double, 3> pulp::signal::chardelay::kDiffusionTankAxis = {0.0, 0.5, 1.0}
inlineconstexpr

◆ kDiffusionTankDampHz

std::array<double, 3> pulp::signal::chardelay::kDiffusionTankDampHz = {12000.0, 7000.0, 4200.0}
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.

◆ kDiffusionTankDecay

std::array<double, 3> pulp::signal::chardelay::kDiffusionTankDecay = {0.0, 0.62, 0.80}
inlineconstexpr

◆ kDiffusionTankDelayAMs

std::array<double, 2> pulp::signal::chardelay::kDiffusionTankDelayAMs = {30.51, 44.79}
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]

◆ kDiffusionTankDelayBMs

std::array<double, 2> pulp::signal::chardelay::kDiffusionTankDelayBMs = {37.13, 53.27}
inlineconstexpr

◆ kDiffusionTankDelaySlewMs

double pulp::signal::chardelay::kDiffusionTankDelaySlewMs = 5.0
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.

◆ kDiffusionTankLowGuardHz

double pulp::signal::chardelay::kDiffusionTankLowGuardHz = 90.0
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]

◆ kDiffusionTankMix

std::array<double, 3> pulp::signal::chardelay::kDiffusionTankMix = {0.0, 0.55, 0.85}
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.

◆ kDiffusionTankModDepth

std::array<double, 3> pulp::signal::chardelay::kDiffusionTankModDepth = {0.0, 14.0, 42.0}
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.

◆ kDiffusionTankModRatesHz

std::array<double, 2> pulp::signal::chardelay::kDiffusionTankModRatesHz = {0.83, 1.27}
inlineconstexpr

◆ kDiffusionTankSizeScale

std::array<double, 3> pulp::signal::chardelay::kDiffusionTankSizeScale = {0.6, 1.0, 1.6}
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.

◆ kDiffusionTankTapFraction

std::array<double, 2> pulp::signal::chardelay::kDiffusionTankTapFraction = {0.37, 0.71}
inlineconstexpr

Extra scattered taps read at their own modulated offsets, as a fraction of the branch delay.

Density without another allpass in the loop.

◆ kDuckAttackS

double pulp::signal::chardelay::kDuckAttackS = 0.005
inlineconstexpr

◆ kDuckReleaseS

double pulp::signal::chardelay::kDuckReleaseS = 0.250
inlineconstexpr

◆ kDuckThreshold

double pulp::signal::chardelay::kDuckThreshold = 0.1
inlineconstexpr

◆ kFlutterAmplitudes

std::array<double, 3> pulp::signal::chardelay::kFlutterAmplitudes = {1.0, 0.5, 0.25}
inlineconstexpr

◆ kFlutterBaseHz

double pulp::signal::chardelay::kFlutterBaseHz = 8.0
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.

◆ kFlutterPhaseRight

double pulp::signal::chardelay::kFlutterPhaseRight = 0.41
inlineconstexpr

Right-channel flutter phase offset, in cycles.

Deterministic but uncorrelated with the left channel.

◆ kFlutterPhases

std::array<double, 3> pulp::signal::chardelay::kFlutterPhases = {0.0, 0.33, 0.71}
inlineconstexpr

◆ kHeadBumpHz

std::array<double, 5> pulp::signal::chardelay::kHeadBumpHz = {35.0, 45.0, 60.0, 80.0, 105.0}
inlineconstexpr

◆ kHeadBumpIps

std::array<double, 5> pulp::signal::chardelay::kHeadBumpIps = {1.875, 3.75, 7.5, 15.0, 30.0}
inlineconstexpr

Head-bump center against speed.

The qualitative behavior (bump rises with speed) is published; the mapping is a design parameter.

◆ kHissLpHz

double pulp::signal::chardelay::kHissLpHz = 4000.0
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.

◆ kHysteresisHalfBandStopbandDb

double pulp::signal::chardelay::kHysteresisHalfBandStopbandDb = kHouseHalfBandStopbandDb
inlineconstexpr

◆ kHysteresisHalfBandTaps

std::size_t pulp::signal::chardelay::kHysteresisHalfBandTaps = kHouseHalfBandTaps
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.

◆ kJaAnhystereticShape

double pulp::signal::chardelay::kJaAnhystereticShape = 2.2e4
inlineconstexpr

◆ kJaConvergenceThreshold

double pulp::signal::chardelay::kJaConvergenceThreshold = 6e-8
inlineconstexpr

◆ kJaInterdomainCoupling

double pulp::signal::chardelay::kJaInterdomainCoupling = 1.6e-3
inlineconstexpr

◆ kJaLangevinClamp

double pulp::signal::chardelay::kJaLangevinClamp = 30.0
inlineconstexpr

Langevin argument clamp — keeps coth() and its derivative in the range where the rational approximation is accurate and the solver well-scaled.

◆ kJaMakeupToleranceDb

double pulp::signal::chardelay::kJaMakeupToleranceDb = 1.0
inlineconstexpr

Makeup-gain tolerance around unity small-signal gain, in dB.

◆ kJaMaxIterations

int pulp::signal::chardelay::kJaMaxIterations = 8
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]

◆ kJaPinning

double pulp::signal::chardelay::kJaPinning = 2.7e4
inlineconstexpr

◆ kJaReversibility

double pulp::signal::chardelay::kJaReversibility = 0.2
inlineconstexpr

◆ kJaSaturationMagnetization

double pulp::signal::chardelay::kJaSaturationMagnetization = 3.5e5
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.

◆ kJaSilenceThreshold

double pulp::signal::chardelay::kJaSilenceThreshold = 1e-7
inlineconstexpr

Below this |H| and |dH/dt|, magnetization is snapped to 0 so a silent loop cannot hold a frozen DC magnetization.

◆ kLoopLpBypassHz

double pulp::signal::chardelay::kLoopLpBypassHz = 20000.0
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.

◆ kLoopToneHpMaxHz

double pulp::signal::chardelay::kLoopToneHpMaxHz = 2000.0
inlineconstexpr

◆ kLoopToneHpMinHz

double pulp::signal::chardelay::kLoopToneHpMinHz = 20.0
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.

◆ kLoopToneLpMaxHz

double pulp::signal::chardelay::kLoopToneLpMaxHz = 20000.0
inlineconstexpr

◆ kLoopToneLpMinHz

double pulp::signal::chardelay::kLoopToneLpMinHz = 200.0
inlineconstexpr

◆ kLoopToneNyquistFraction

double pulp::signal::chardelay::kLoopToneNyquistFraction = 0.45
inlineconstexpr

Keep the TPT prewarp strictly below Nyquist.

At or above Nyquist tan() is periodic rather than a meaningful analog-frequency mapping.

◆ kLoopToneResMax

double pulp::signal::chardelay::kLoopToneResMax = 2.0
inlineconstexpr

◆ kLoopToneResMin

double pulp::signal::chardelay::kLoopToneResMin = 0.5
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.

◆ kLossBankCrossfadeS

double pulp::signal::chardelay::kLossBankCrossfadeS = 0.030
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]

◆ kLossFirOrder48k

std::size_t pulp::signal::chardelay::kLossFirOrder48k = 96
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]

◆ kLossIirFitIterations

int pulp::signal::chardelay::kLossIirFitIterations = 4000
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.

◆ kLossIirFitPoints

int pulp::signal::chardelay::kLossIirFitPoints = 64
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.

◆ kLossIirFitRestarts

int pulp::signal::chardelay::kLossIirFitRestarts = 3
inlineconstexpr

◆ kLossIirFitStarts

int pulp::signal::chardelay::kLossIirFitStarts = 16
inlineconstexpr

◆ kLossPoleSections

std::size_t pulp::signal::chardelay::kLossPoleSections
inlineconstexpr
Initial value:
=
constexpr std::size_t kSpacingPoleSections
Sections in each fitted cascade.
Definition tables.hpp:421
constexpr std::size_t kThicknessPoleSections
Definition tables.hpp:423

◆ kLossSearchDimension

std::size_t pulp::signal::chardelay::kLossSearchDimension
inlineconstexpr
Initial value:
=
constexpr std::size_t kSpacingShelfSections
Definition tables.hpp:422

Search dimension of ONE shape fit (the larger of the two).

◆ kLossShapeMaxX

double pulp::signal::chardelay::kLossShapeMaxX = 4.0
inlineconstexpr

◆ kLossShapeMinX

double pulp::signal::chardelay::kLossShapeMinX = 1e-3
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.

◆ kLossShelfMinDb

double pulp::signal::chardelay::kLossShelfMinDb = -40.0
inlineconstexpr

Deepest cut any one shelf may ask for.

◆ kLossShelfSections

std::size_t pulp::signal::chardelay::kLossShelfSections
inlineconstexpr
Initial value:
=
constexpr std::size_t kThicknessShelfSections
Definition tables.hpp:424

◆ kMaxAddressableDelayMs

double pulp::signal::chardelay::kMaxAddressableDelayMs = kMaxDelayMs * kTimeOffsetMax
inlineconstexpr

◆ kMaxDelayMs

double pulp::signal::chardelay::kMaxDelayMs = 2000.0
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().

◆ kModMaxDepth

double pulp::signal::chardelay::kModMaxDepth = 0.05
inlineconstexpr

◆ kModRateMaxHz

double pulp::signal::chardelay::kModRateMaxHz = 10.0
inlineconstexpr

◆ kModRateMinHz

double pulp::signal::chardelay::kModRateMinHz = 0.05
inlineconstexpr

Generic modulation LFO: log rate map and maximum depth as a fraction of delay time.

[design parameters; depth range 0.02–0.1]

◆ kParamGlideS

double pulp::signal::chardelay::kParamGlideS = 0.005
inlineconstexpr

De-zipper glide for scalar params (feedback, crossfeed, duck, freeze, and the character table's per-sample outputs), in seconds.

◆ kPi

double pulp::signal::chardelay::kPi = 3.14159265358979323846
inlineconstexpr

◆ kPrngSeed

std::uint32_t pulp::signal::chardelay::kPrngSeed = 74207u
inlineconstexpr

PRNG seed.

[design parameter — the requirement is determinism, not the value.] Streams that must not correlate derive their own seeds from it.

◆ kReverseFadeSamples

int pulp::signal::chardelay::kReverseFadeSamples = 96
inlineconstexpr

Raised-cosine splice fade at each end of a reversed segment, in samples.

[32–256]

◆ kReverseLineMs

double pulp::signal::chardelay::kReverseLineMs = 30.0
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.

◆ kSaturatedFeedbackMax

double pulp::signal::chardelay::kSaturatedFeedbackMax = 1.1
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.

◆ kSpacingPoleSections

std::size_t pulp::signal::chardelay::kSpacingPoleSections = 3
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.

◆ kSpacingShapePoleX

std::array<double, kSpacingPoleSections> pulp::signal::chardelay::kSpacingShapePoleX
inlineconstexpr
Initial value:
= {
0.1736328177, 0.5872051207, 0.7066518389}

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).

◆ kSpacingShapeShelfDb

std::array<double, kSpacingShelfSections> pulp::signal::chardelay::kSpacingShapeShelfDb
inlineconstexpr
Initial value:
= {
-4.870968905, -22.02414799, -32.23365298, -35.9700058,
-20.85639305, -35.24359399, -23.08806288, -40.0}

◆ kSpacingShapeShelfX

std::array<double, kSpacingShelfSections> pulp::signal::chardelay::kSpacingShapeShelfX
inlineconstexpr
Initial value:
= {
0.0379101878, 0.8617586156, 1.195357912, 0.546821599,
1.874319242, 1.250559327, 1.349688739, 0.5618309026}

◆ kSpacingShelfSections

std::size_t pulp::signal::chardelay::kSpacingShelfSections = 8
inlineconstexpr

◆ kStereoDecorr

double pulp::signal::chardelay::kStereoDecorr = 0.95
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]

◆ kTapeAxis

std::array<double, 4> pulp::signal::chardelay::kTapeAxis = {0.0, 0.33, 0.67, 1.0}
inlineconstexpr

◆ kTapeBias

std::array<double, 4> pulp::signal::chardelay::kTapeBias = {0.0, 0.10, 0.20, 0.30}
inlineconstexpr

◆ kTapeBumpDb

std::array<double, 4> pulp::signal::chardelay::kTapeBumpDb = {1.5, 2.0, 3.0, 4.0}
inlineconstexpr

◆ kTapeBumpHz

double pulp::signal::chardelay::kTapeBumpHz = 60.0
inlineconstexpr

Loss stand-in head bump (the physical tier replaces this with a speed-dependent center; see kHeadBumpIps/kHeadBumpHz).

◆ kTapeBumpQ

double pulp::signal::chardelay::kTapeBumpQ = 1.5
inlineconstexpr

◆ kTapeCoatingThicknessM

double pulp::signal::chardelay::kTapeCoatingThicknessM = 12e-6
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.

◆ kTapeDcBlockHz

double pulp::signal::chardelay::kTapeDcBlockHz = 15.0
inlineconstexpr

In-loop DC blocker.

[10–20 Hz, the published range for tape-machine playback electronics]

◆ kTapeDrive

std::array<double, 4> pulp::signal::chardelay::kTapeDrive = {1.5, 2.5, 4.0, 6.0}
inlineconstexpr

◆ kTapeEmphasisDb

double pulp::signal::chardelay::kTapeEmphasisDb = 3.0
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.

◆ kTapeEmphasisHz

double pulp::signal::chardelay::kTapeEmphasisHz = 1600.0
inlineconstexpr

◆ kTapeFlutterDepthMs

std::array<double, 4> pulp::signal::chardelay::kTapeFlutterDepthMs = {0.0, 0.05, 0.15, 0.4}
inlineconstexpr

◆ kTapeHeadGapM

double pulp::signal::chardelay::kTapeHeadGapM = 3e-6
inlineconstexpr

◆ kTapeLossFloorDb

double pulp::signal::chardelay::kTapeLossFloorDb = -60.0
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.

◆ kTapeLossLpHz

std::array<double, 4> pulp::signal::chardelay::kTapeLossLpHz = {12000.0, 9000.0, 6000.0, 3500.0}
inlineconstexpr

◆ kTapePhysicalFeedbackCompensation

std::array<double, 4> pulp::signal::chardelay::kTapePhysicalFeedbackCompensation
inlineconstexpr
Initial value:
= {
0.3739533963, 0.3530347705, 0.3146425708, 0.2804254864}

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.

◆ kTapeSpeedsIps

std::array<double, 5> pulp::signal::chardelay::kTapeSpeedsIps = {1.875, 3.75, 7.5, 15.0, 30.0}
inlineconstexpr

The IEC/NAB standard tape-speed set, in inches per second (published).

◆ kTapeWowDepthMs

std::array<double, 4> pulp::signal::chardelay::kTapeWowDepthMs = {0.0, 0.15, 0.5, 1.2}
inlineconstexpr

◆ kThicknessPoleSections

std::size_t pulp::signal::chardelay::kThicknessPoleSections = 1
inlineconstexpr

◆ kThicknessShapePoleX

std::array<double, kThicknessPoleSections> pulp::signal::chardelay::kThicknessShapePoleX
inlineconstexpr
Initial value:
= {
0.2127135512}

◆ kThicknessShapeShelfDb

std::array<double, kThicknessShelfSections> pulp::signal::chardelay::kThicknessShapeShelfDb
inlineconstexpr
Initial value:
= {
-1.705785229, -0.6809460063}

◆ kThicknessShapeShelfX

std::array<double, kThicknessShelfSections> pulp::signal::chardelay::kThicknessShapeShelfX
inlineconstexpr
Initial value:
= {
0.03067430198, 0.1724152232}

◆ kThicknessShelfSections

std::size_t pulp::signal::chardelay::kThicknessShelfSections = 2
inlineconstexpr

◆ kTimeOffsetMax

double pulp::signal::chardelay::kTimeOffsetMax = 1.5
inlineconstexpr

◆ kTimeOffsetMin

double pulp::signal::chardelay::kTimeOffsetMin = 0.5
inlineconstexpr

◆ kTimeSlewBbdMs

double pulp::signal::chardelay::kTimeSlewBbdMs = 80.0
inlineconstexpr

◆ kTimeSlewCleanMs

double pulp::signal::chardelay::kTimeSlewCleanMs = 15.0
inlineconstexpr

◆ kTimeSlewDiffusionMs

double pulp::signal::chardelay::kTimeSlewDiffusionMs = 15.0
inlineconstexpr

◆ kTimeSlewTapeMs

double pulp::signal::chardelay::kTimeSlewTapeMs = 250.0
inlineconstexpr

◆ kTimeSlewVintageMs

double pulp::signal::chardelay::kTimeSlewVintageMs = 50.0
inlineconstexpr

◆ kUnsaturatedFeedbackMax

double pulp::signal::chardelay::kUnsaturatedFeedbackMax = 0.98
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.

◆ kVintageAntiAliasFraction

double pulp::signal::chardelay::kVintageAntiAliasFraction = 0.45
inlineconstexpr

Anti-alias / reconstruction cutoff as a fraction of the internal rate.

◆ kVintageAxis

std::array<double, 4> pulp::signal::chardelay::kVintageAxis = {0.0, 0.33, 0.67, 1.0}
inlineconstexpr

◆ kVintageBits

std::array<double, 4> pulp::signal::chardelay::kVintageBits = {14.0, 12.0, 12.0, 10.0}
inlineconstexpr

◆ kVintageEmphasisDb

double pulp::signal::chardelay::kVintageEmphasisDb = 9.0
inlineconstexpr

◆ kVintageEmphasisHz

double pulp::signal::chardelay::kVintageEmphasisHz = 3183.0
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].

◆ kVintageInternalRateHz

std::array<double, 4> pulp::signal::chardelay::kVintageInternalRateHz
inlineconstexpr
Initial value:
= {32000.0, 24000.0, 16000.0,
8000.0}

◆ kWowOuNoiseLpHz

double pulp::signal::chardelay::kWowOuNoiseLpHz = 10.0
inlineconstexpr

◆ kWowOuSigma

double pulp::signal::chardelay::kWowOuSigma = 0.6
inlineconstexpr

◆ kWowOuTheta

double pulp::signal::chardelay::kWowOuTheta = 1.5
inlineconstexpr

◆ kWowRateHz

double pulp::signal::chardelay::kWowRateHz = 1.0
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.