DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
TapeMachine.h File Reference

Physical tape machine: JA hysteresis, EQ standards, losses, transport. More...

#include "../Core/AudioBuffer.h"
#include "../Core/AudioSpec.h"
#include "../Core/Biquad.h"
#include "../Core/DenormalGuard.h"
#include "../Core/DspMath.h"
#include "../Core/Hysteresis.h"
#include "../Core/Oversampling.h"
#include "../Core/SimdOps.h"
#include "../Core/StateBlob.h"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <numbers>
#include <vector>
Include dependency graph for TapeMachine.h:
This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Classes

class  dspark::TapeMachine< T >
 Reel-to-reel tape emulation with physical hysteresis and transport. More...
 

Namespaces

namespace  dspark
 Main namespace for the DSPark framework.
 

Detailed Description

Physical tape machine: JA hysteresis, EQ standards, losses, transport.

A complete record/playback chain built around the Jiles-Atherton hysteresis core (Core/Hysteresis.h), modelling the five things that make tape sound like tape:

  1. Record/playback equalization (NAB or CCIR, speed-dependent time constants): the signal is HF-emphasized (and LF-boosted for NAB) before hitting the "tape", and de-emphasized on playback with the exact digital inverse. The round trip is flat by construction - but the hysteresis sees the emphasized signal, so highs saturate first at slow speeds, exactly as on hardware.
  2. Magnetic hysteresis with REAL AC bias at 4x oversampling by DEFAULT (configurable per RF-009/ADR-011, see setOversampling): an ultrasonic carrier (0.375 * internal rate, exact 8-phase table) is summed with the signal into a push-pull pair of JA instances per channel (+carrier / -carrier, output averaged, like a centre-tapped record head). The carrier erases the loop's branch memory exactly as hardware bias does, the push-pull cancels the carrier, its odd harmonics and the remanence DC, and the even folds die in the downsampler. bias maps to carrier amplitude: under-bias drops below the erase threshold (real grit and level instability), over-bias adds the self-erasure roll-off of short wavelengths. Gain is calibrated per setting through the same biased chain, so drive changes saturation, not level.
  3. Playback loss effects with the physical formulas (Kadis): spacing loss 54.6*d/lambda dB, gap loss sinc(pi*g/lambda), thickness loss (1-e^(-4*pi*delta/lambda))/(4*pi*delta/lambda), with lambda = speed/frequency - rendered into a 63-tap linear-phase FIR per speed, plus the speed-dependent head-bump resonance (45/90/180 Hz at 7.5/15/30 ips).
  4. Wow & flutter: one shared transport modulation (slow drift random walk + 0.55 Hz wow + 8.3/23 Hz flutter + scrape band) driving a fractional delay - identical on all channels, like a real capstan. The hiss generator uses its own RNG stream, so enabling noise never changes the transport's realisation.
  5. Tape hiss (optional, default off).
  6. Physical band edges: a 2nd-order 24 Hz high-pass models the AC-coupled playback amplifier (kills subsonics and any residual DC, like the hardware's LF roll-off), and the loss FIR always applies the hard reproduce-gap cutoff above ~21.5 kHz (no real head reads shorter wavelengths; it also removes the last even-order bias intermodulation sidebands near the base-rate Nyquist).

The dry path of the mix control is delay-compensated to getLatency(). The mix is applied per block without smoothing: the wet stream is correlated with and aligned to the dry, so a mix step moves the output by the (small) timbral difference only - measured below the steady-state sample delta. Channels beyond the prepared count pass through untouched.

Threading model: parameter setters/getters are std::atomic based and safe from any thread (non-finite values are ignored; parameter changes are published with a release store and consumed at the next block). prepare() is setup-thread only (allocates; invalid specs are ignored and an unprepared instance passes audio through). reset() belongs to the stream owner. getState()/setState() are setup/UI threads. The recompute on a parameter change runs a short scratch calibration on the audio thread (allocation-free, a few thousand samples of model time).

Cost: the push-pull biased core runs two JA solvers at the active oversampling factor (4x default), ~11% of one core at 48 kHz stereo on a desktop CPU at 4x - the price of physical AC bias (history-independent LF response, exact even-harmonic cancellation, ultrasonic residue below -75 dB). A SIMD lane-parallel JA (2 channels x 2 polarities) is the noted future optimisation if a lighter budget is ever needed.

Oversampling transparency (RF-009/ADR-011). setOversampling(int) (setup thread only; it reallocates and re-calibrates like prepare()) selects the internal factor in {1,2,4,8,16}. DEFAULT 4. The AC-bias carrier is always 0.375 * the internal rate, so the factor sets how far the carrier sits above the audio band: at 4x/48k it is ultrasonic (72 kHz) and its even folds die in the downsampler; CPU scales ~linearly with the factor. factor = 1 means OFF - no internal resampling and zero added oversampler latency, but then the carrier lands at 0.375 * the base rate (18 kHz at 48k), i.e. IN-BAND, so 1x is only sensible when the host runs a high sample rate or the surrounding chain is already oversampled (the AC-bias model needs ultrasonic headroom). getLatency() always reflects the active factor (oversampler group delay + loss FIR + transport delay), so hosts get correct PDC.

Dependencies: Core/Hysteresis.h, Core/Oversampling.h, Core/Biquad.h, Core/SimdOps.h, Core/AudioSpec.h, Core/AudioBuffer.h, Core/DspMath.h, Core/DenormalGuard.h, Core/StateBlob.h.

Definition in file TapeMachine.h.