Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ __pycache__/
.vscode/
.idea/

# tr808 calibration notebook work dirs (downloaded reference set + renders)
# tr808 calibration notebook work dirs (downloaded reference set + renders + figures)
notebooks/_tr808_ref/
notebooks/_tr808_ours/
notebooks/_swing_figs/
20 changes: 14 additions & 6 deletions include/taptools/swing_vca.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
///
/// - `decay_env` — trigger to a level, one-pole rise (fast, configurable), then
/// exponential decay with a settable time constant: the RC discharge shape.
/// - `swing_vca` — the envelope applied as a gain. Modeled linear; the hardware
/// VCAs add "many high harmonics" (Service Notes on the RS/CL VCA) — a flagged
/// refinement for the circuit-simulation phase.
/// - `swing_vca` — the envelope applied as a gain. Linear by default (`drive` 0),
/// with an opt-in `drive` that engages the swing VCA's symmetric harmonic
/// saturation — the "many high harmonics" the Service Notes note (RS/CL VCA) —
/// via the shared `vca.h` `swing_shape` (also `tap.vca~`'s `swing` circuit). Off
/// by default so every calibrated voice stays bit-identical until a voice opts in.
/// - `white_noise` — the shared white-noise source (the 808 has a single noise
/// generator feeding SD snappy, CP, MA, and the tom "reverberation"). Seeded
/// xorshift64*: deterministic per seed, so renders and tests reproduce and mc.
Expand All @@ -25,6 +27,8 @@
#include <cmath>
#include <cstdint>

#include "vca.h" // taptools::vca::swing_shape — the shared swing-type saturator

namespace taptools {
namespace tr808 {

Expand Down Expand Up @@ -107,9 +111,13 @@ namespace taptools {
double m_env{0.0}, m_target{0.0};
};

/// The swing-type VCA: envelope as gain. Linear model (see header note).
inline double swing_vca(double x, double env) {
return x * env;
/// The swing-type VCA: envelope as gain. Linear by default (`drive` 0 → `x * env`, the
/// calibrated model, bit-for-bit); `drive > 0` engages the swing VCA's symmetric harmonic
/// saturation (the "many high harmonics" the Service Notes note) on the enveloped signal,
/// via the shared taptools::vca::swing_shape. The character rides the envelope — quiet tails
/// stay clean, hot transients pick up grit and gentle compression.
inline double swing_vca(double x, double env, double drive = 0.0) {
return vca::swing_shape(x * env, drive);
}

} // namespace tr808
Expand Down
1 change: 1 addition & 0 deletions include/taptools/taptools.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
#include "tr808_rim.h"
#include "tr808_snare.h"
#include "tr808_tom.h"
#include "vca.h"
#include "vco.h"
#include "vocoder.h"
57 changes: 31 additions & 26 deletions include/taptools/tb303_voice.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
#include <cstdint>

#include "diode_ladder.h"
#include "vca.h"
#include "vco.h"

namespace taptools {
Expand Down Expand Up @@ -178,8 +179,10 @@ namespace taptools {
enum vca_mode : int { vca_clean = 0, vca_warm, k_num_vca_modes };

// Phase-2 transistor-VCA saturator (see header): informed constants, probe-calibrated.
constexpr double k_vca_sat_drive = 2.0;
constexpr double k_vca_sat_bias = 0.3;
// The saturator itself lives in the shared vca.h kernel (taptools::vca) so tap.303~ and the
// standalone tap.vca~ run one implementation; these are the values the voice configures it with.
constexpr double k_vca_sat_drive = taptools::vca::k_default_drive; // 2.0
constexpr double k_vca_sat_bias = taptools::vca::k_default_bias; // 0.3

constexpr int k_factory_presets = 8; // slots 0..7 ship authored; 8..15 are defaults

Expand Down Expand Up @@ -273,7 +276,10 @@ namespace taptools {
m_osc.set_waveform(vco::wave_saw); // always the saw core; the square is shaped from it
m_osc.snap();
m_filter.prepare(m_sr);
m_filter.set_smooth_ms(0.0); // the voice owns all smoothing
m_filter.set_smooth_ms(0.0); // the voice owns all smoothing
m_vca_stage.set_drive(k_vca_sat_drive); // stock 303 transistor-stage calibration
m_vca_stage.set_bias(k_vca_sat_bias);
m_vca_stage.set_mode(m_vca_mode);
apply_unit_spread();
m_pre_hp.set(k_pre_hpf_hz, m_sr);
m_post_hp.set(k_post_hpf_hz, m_sr);
Expand Down Expand Up @@ -339,8 +345,11 @@ namespace taptools {

/// Phase-2 VCA circuit: vca_clean (linear, default — bit-identical to phase 1) or
/// vca_warm (the one-transistor stage's biased saturation; see the header).
void set_vca(int mode) { m_vca_mode = std::clamp(mode, 0, k_num_vca_modes - 1); }
int vca() const { return m_vca_mode; }
void set_vca(int mode) {
m_vca_mode = std::clamp(mode, 0, k_num_vca_modes - 1);
m_vca_stage.set_mode(m_vca_mode);
}
int vca() const { return m_vca_mode; }

/// Deterministic per-unit component spread: `seed` picks the unit, `tolerance` (0..1)
/// scales how far off nominal it is. tolerance 0 = the nominal schematic exactly.
Expand Down Expand Up @@ -499,7 +508,7 @@ namespace taptools {
// hardware order: the transistor stage saturates the enveloped signal, then the
// output coupling removes the shaper's signal-dependent DC
const double v = filtered * m_vca * accent_boost;
const double s = (std::tanh(k_vca_sat_drive * v + k_vca_sat_bias) - m_vca_sat_off) * m_vca_sat_norm;
const double s = m_vca_stage.shape(v); // shared transistor-stage saturator (vca.h)
return m_post_hp.tick(s) * m_out_gain;
}
const double shaped = m_post_hp.tick(filtered);
Expand Down Expand Up @@ -615,22 +624,19 @@ namespace taptools {
// Recompute the per-unit component offsets and every coefficient built on them.
// tolerance 0 leaves the nominal schematic exactly (all factors 1, imperfect 0).
void apply_unit_spread() {
m_vca_sat_off = std::tanh(k_vca_sat_bias);
const double sech_sq = 1.0 - m_vca_sat_off * m_vca_sat_off;
m_vca_sat_norm = 1.0 / (k_vca_sat_drive * sech_sq);
const double t = m_tolerance;
m_unit_tune_cents = 8.0 * t * unit_draw(m_seed, 0); // converter trim
m_unit_cutoff_scale = std::exp2(0.12 * t * unit_draw(m_seed, 1)); // VCF tracking
m_unit_env = 1.0 + 0.10 * t * unit_draw(m_seed, 2); // envelope caps
m_unit_vca = 1.0 + 0.10 * t * unit_draw(m_seed, 3);
m_unit_slide = 1.0 + 0.15 * t * unit_draw(m_seed, 4); // slide RC
m_unit_c13 = 1.0 + 0.15 * t * unit_draw(m_seed, 5); // accent-sweep RC
m_unit_attack = 1.0 + 0.20 * t * unit_draw(m_seed, 6);
m_meg_attack = attack_coef(k_meg_attack_ms);
m_vca_decay = decay_coef(k_vca_decay_ms * m_unit_vca);
m_vca_release = decay_coef(k_vca_release_ms);
m_c13_charge = one_pole_coef(k_c13_charge_ms * m_unit_c13);
m_c13_drain = one_pole_coef(k_c13_discharge_ms * m_unit_c13);
const double t = m_tolerance;
m_unit_tune_cents = 8.0 * t * unit_draw(m_seed, 0); // converter trim
m_unit_cutoff_scale = std::exp2(0.12 * t * unit_draw(m_seed, 1)); // VCF tracking
m_unit_env = 1.0 + 0.10 * t * unit_draw(m_seed, 2); // envelope caps
m_unit_vca = 1.0 + 0.10 * t * unit_draw(m_seed, 3);
m_unit_slide = 1.0 + 0.15 * t * unit_draw(m_seed, 4); // slide RC
m_unit_c13 = 1.0 + 0.15 * t * unit_draw(m_seed, 5); // accent-sweep RC
m_unit_attack = 1.0 + 0.20 * t * unit_draw(m_seed, 6);
m_meg_attack = attack_coef(k_meg_attack_ms);
m_vca_decay = decay_coef(k_vca_decay_ms * m_unit_vca);
m_vca_release = decay_coef(k_vca_release_ms);
m_c13_charge = one_pole_coef(k_c13_charge_ms * m_unit_c13);
m_c13_drain = one_pole_coef(k_c13_discharge_ms * m_unit_c13);
m_osc.set_seed(m_seed);
m_osc.set_imperfect(0.6 * t); // waveform imperfection rides the same tolerance
push_filter_params();
Expand Down Expand Up @@ -663,10 +669,9 @@ namespace taptools {
double m_vca_decay{0.9999}, m_vca_release{0.99};
double m_slide_coef{0.001};

// phase-2 VCA circuit
int m_vca_mode{vca_clean};
double m_vca_sat_off{0.291313}; // tanh(bias); recomputed in apply_unit_spread
double m_vca_sat_norm{0.546366}; // 1/(drive*sech^2(bias))
// phase-2 VCA circuit — the saturator math lives in the shared taptools::vca stage
int m_vca_mode{vca_clean};
::taptools::vca m_vca_stage; // shape() only; the voice runs its own coupling + gain

// per-unit component spread (seed/tolerance)
double m_unit_tune_cents{0.0};
Expand Down
12 changes: 10 additions & 2 deletions include/taptools/tr808_clap.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ namespace taptools {
/// wash, leaving only the three teeth).
void set_tail(double amount) { m_tail = std::clamp(amount, 0.0, 2.0); }

/// Swing-VCA drive on the output VCA (0 = the calibrated linear model, bit-identical;
/// > 0 engages the swing VCA's symmetric harmonic saturation — grit and compression
/// that ride the envelope). Applies to both the clap and maracas models. See
/// swing_vca.h / vca.h swing_shape.
void set_drive(double amount) { m_drive = std::max(0.0, amount); }
double drive() const { return m_drive; }

/// Noise-source seed (deterministic; mc. instances decorrelate by seed).
void set_seed(uint64_t seed) { m_noise.set_seed(seed); }

Expand All @@ -155,7 +162,7 @@ namespace taptools {

if (m_model == model_maracas) {
const double y =
swing_vca(m_ma_lp.process(m_ma_hp2.process(m_ma_hp1.process(n))), m_ma_env.process());
swing_vca(m_ma_lp.process(m_ma_hp2.process(m_ma_hp1.process(n))), m_ma_env.process(), m_drive);
return y * m_level * k_ma_out_scale;
}

Expand Down Expand Up @@ -185,7 +192,7 @@ namespace taptools {
m_bp2_z2 = -m_bp_b0 * b1 - m_bp_a2 * bp;

const double env = m_tooth_env + k_cp_tail_level * m_tail * m_tail_env;
return swing_vca(bp, env) * m_level * k_cp_out_scale;
return swing_vca(bp, env, m_drive) * m_level * k_cp_out_scale;
}

private:
Expand All @@ -207,6 +214,7 @@ namespace taptools {
white_noise m_noise;

double m_level{1.0}, m_tail{1.0}, m_accent{1.0};
double m_drive{0.0}; // swing-VCA saturation on the output VCA; 0 = linear (default)
};

} // namespace tr808
Expand Down
9 changes: 8 additions & 1 deletion include/taptools/tr808_cowbell.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ namespace taptools {
/// Output level, 0..1 (VR5, CB LEVEL).
void set_level(double amount) { m_level = std::clamp(amount, 0.0, 1.0); }

/// Swing-VCA drive on the oscillator pair (0 = calibrated linear model, bit-identical;
/// > 0 engages the swing VCA's symmetric harmonic saturation before the bandpass — the
/// experimental fidelity hook for the tonal-voice sweep, plans/tap.808.md).
void set_drive(double amount) { m_drive = std::max(0.0, amount); }
double drive() const { return m_drive; }

void set_tuning(double ratio) { m_bank.set_tuning(ratio); }
void set_tolerance(double amount) { m_bank.set_tolerance(amount); }
void set_seed(uint64_t seed) { m_bank.set_seed(seed); }
Expand All @@ -96,7 +102,7 @@ namespace taptools {
// The gates pass only the trimpot pair (oscillators #5 and #6: 800 / 540 Hz).
const double pair = 0.5 * (m_bank.osc(4) + m_bank.osc(5));
const double env = m_env_fast.process() + m_env_tail.process();
return m_bp.process(swing_vca(pair, env)) * m_level * k_cb_out_scale;
return m_bp.process(swing_vca(pair, env, m_drive)) * m_level * k_cb_out_scale;
}

private:
Expand All @@ -107,6 +113,7 @@ namespace taptools {
decay_env m_env_fast, m_env_tail;

double m_level{1.0};
double m_drive{0.0}; // swing-VCA saturation on the osc pair; 0 = linear (default)
};

} // namespace tr808
Expand Down
26 changes: 19 additions & 7 deletions include/taptools/tr808_rim.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ namespace taptools {
/// Output level, 0..1 (VR16, RS/CL LEVEL).
void set_level(double amount) { m_level = std::clamp(amount, 0.0, 1.0); }

/// Swing-VCA drive (the Q62 harmonic VCA). Sentinel < 0 (default) uses each model's
/// calibrated value — rimshot k_rs_drive (2.2, as always shipped), claves linear (0).
/// A value >= 0 overrides both models — the experimental hook for the claves fidelity
/// sweep (plans/tap.808.md; the rimshot already ships saturated).
void set_drive(double amount) { m_drive = std::max(-1.0, amount); }
double drive() const { return m_drive; }

void trigger(double accent = 1.0) {
const double a = std::clamp(accent, 0.0, 1.0);
m_vtrig = k_rim_vtrig_min + a * (k_rim_vtrig_max - k_rim_vtrig_min);
Expand All @@ -117,19 +124,23 @@ namespace taptools {
--m_pulse_remaining;
}

const double drive = v_pulse * 0.02;
double ring = m_hi.process(drive, 0.0, 0.0);
const double exc = v_pulse * 0.02;
double ring = m_hi.process(exc, 0.0, 0.0);
if (m_model == model_rimshot) {
ring = ring * k_rs_hi_mix + m_lo.process(drive, 0.0, 0.0);
ring = ring * k_rs_hi_mix + m_lo.process(exc, 0.0, 0.0);
}

const double env = m_env.process();
if (m_model == model_rimshot) {
// The Q62 swing VCA's harmonic generation: drive the gated sum.
return std::tanh(ring * k_rs_drive * env) / k_rs_drive * m_level * k_rim_out_scale
* k_rim_vtrig_max;
// The Q62 swing VCA's harmonic generation (Service Notes, RS/CL VCA): the shared
// swing_shape (vca.h) IS tanh(d*v)/d, so swing_vca(ring, env, k_rs_drive) is the
// exact tanh(ring*k_rs_drive*env)/k_rs_drive this always shipped — now unified.
const double d = (m_drive < 0.0) ? k_rs_drive : m_drive;
return swing_vca(ring, env, d) * m_level * k_rim_out_scale * k_rim_vtrig_max;
}
return swing_vca(ring, env) * k_cl_mix * m_level * k_rim_out_scale;
// Claves: linear by default (m_drive < 0 → 0), with the same opt-in swing saturation.
const double d = (m_drive < 0.0) ? 0.0 : m_drive;
return swing_vca(ring, env, d) * k_cl_mix * m_level * k_rim_out_scale;
}

private:
Expand Down Expand Up @@ -168,6 +179,7 @@ namespace taptools {
decay_env m_env;

double m_level{1.0};
double m_drive{-1.0}; // swing-VCA drive; <0 = per-model calibrated (RS 2.2 / CL linear)
double m_vtrig{0.0};
int m_pulse_remaining{0};
};
Expand Down
19 changes: 14 additions & 5 deletions include/taptools/tr808_snare.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
/// K. J. Werner's TR-808 SD analysis). STOCK HERE IS THE LATER REVISION; the
/// `tuning` bend scales both resonators from there.
///
/// Behavioral simplifications: the hardware VCA's harmonic generation is modeled
/// linear (see swing_vca.h); the slight re-excitation of the resonators by the
/// snappy envelope (the composite-trigger path through Q47) is omitted, as in
/// Werner's model; accent maps to the common trigger bus voltage as in the kick.
/// Behavioral simplifications: the hardware VCA's harmonic generation is linear by
/// default, with an opt-in `drive` for the swing VCA's symmetric saturation (see
/// swing_vca.h); the slight re-excitation of the resonators by the snappy envelope
/// (the composite-trigger path through Q47) is omitted, as in Werner's model; accent
/// maps to the common trigger bus voltage as in the kick.
///
/// §7.2 calibration (2026-07-17), vs the Fischer 1994 set (unit 103852):
/// fundamentals within 1.2% at every dial position, including the tone-max
Expand Down Expand Up @@ -177,6 +178,13 @@ namespace taptools {

// -- circuit bends (stock hardware at the defaults) ----------------------------

/// Swing-VCA drive on the snappy noise path (0 = the calibrated linear model,
/// bit-identical; > 0 engages the swing VCA's symmetric harmonic saturation — grit and
/// compression that ride the snappy envelope, hardest on the transient crack). See
/// swing_vca.h / vca.h swing_shape.
void set_drive(double amount) { m_drive = std::max(0.0, amount); }
double drive() const { return m_drive; }

/// Pitch as a ratio of the stock tuning (0.25..4): scales both resonators' arm
/// capacitors together. 1.0 = the late-revision schematic (~173/336 Hz).
void set_tuning(double ratio) {
Expand Down Expand Up @@ -211,7 +219,7 @@ namespace taptools {

const double snap = swing_vca(m_noise_lp2.process(m_noise_lp.process(
m_noise_hp2.process(m_noise_hp1.process(m_noise.process())))),
m_env.process())
m_env.process(), m_drive)
* std::pow(m_snappy, k_sd_snappy_taper) * k_sd_snappy_gain;

const double mix = f * (1.0 - m_tone) + h * m_tone + snap;
Expand All @@ -228,6 +236,7 @@ namespace taptools {

double m_tone{0.5}, m_snappy{0.5}, m_level{1.0};
double m_tuning{1.0};
double m_drive{0.0}; // swing-VCA saturation on the snappy path; 0 = linear (default)
double m_vtrig{0.0};
int m_pulse_remaining{0};
};
Expand Down
Loading
Loading