Skip to content

vca.h: shared transistor-VCA kernel (warm + swing), with 808 calibration notebook#17

Merged
tap merged 5 commits into
mainfrom
claude/vca-object-808-303-vtyl35
Jul 19, 2026
Merged

vca.h: shared transistor-VCA kernel (warm + swing), with 808 calibration notebook#17
tap merged 5 commits into
mainfrom
claude/vca-object-808-303-vtyl35

Conversation

@tap

@tap tap commented Jul 19, 2026

Copy link
Copy Markdown
Owner

What this is

Extracts the TB-303's transistor VCA into a standalone, shared vca.h kernel, adds the TR-808 swing-type VCA as a second nonlinear circuit, and lands a calibration notebook that measures both against a real 808 to decide where they're a fidelity win vs. a creative bend. Companion to the Max-side PR tap/TapTools-Max (which pins this kernel and ships tap.vca~).

Kernel changes

  • vca.h (new)taptools::vca, the svf.h two-circuit idiom applied to a gain stage:
    • clean — pure linear multiply, bit-identical to *~.
    • warm — the 303 one-transistor class-A stage: S(v) = (tanh(d·v+b) − tanh(b)) / (d·sech²(b)) (stock d=2.0, b=0.3), applied post-gain so the even-harmonic warmth + compression track the control voltage; optional output DC block.
    • swing — the TR-808 swing-type VCA: symmetric (odd-harmonic) tanh(d·v)/d, no DC. A static swing_shape(v, drive) is the one implementation shared by mode_swing and swing_vca().
  • tb303_voice.h — now composes vca::shape() instead of an inline saturator. The voice keeps its own Open303 output high-pass + gain, so it's bit-identical (pinned by tests/vca_test.cpp, which matches shape() to the old inline formula and re-runs the warm-vs-clean scenarios).
  • swing_vca.h — the 808 noise-voice VCA gains an opt-in drive (default 0 → exact x·env passthru), delegating to the shared swing_shape. Wired into tr808_snare/clap/tom (snappy / CP+maracas / noise-reverb paths).
  • tr808_rim.h — the rimshot's inline tanh(ring·k_rs_drive·env)/k_rs_drive is unified onto swing_vca(ring, env, k_rs_drive) (bit-identical); claves gains an opt-in drive.
  • tr808_cowbell.h — opt-in drive on the oscillator pair (default 0).
  • tools/render/tr808_render.cpp--set drive on snare/clap/tom/rimshot/claves/cowbell for the calibration sweep.

Default is 0 / linear everywhere — every calibrated 808 voice stays bit-identical until it opts in. The full tr808_* + tb303 suites pass unchanged; tests/vca_test.cpp adds the two-circuit + swing contract and the snare test adds an off-is-bit-identical / on-saturates scenario.

The calibration finding (notebooks/tr808_swing_vca.ipynb)

Three-way A/B (real 808 = Fischer's free Technopolis set / our linear / our swing) for the noise and tonal voices:

  • Noise voices (snare/clap/tom): crest factor shows our linear model already matches the hardware's transient sharpness — drive only compresses past it. Keep linear (consistent with the DAFx-14 "device nonlinearity matters less than folklore" finding).
  • Tonal voices: swing is a fidelity win on exactly one — the rimshot — whose spectral distance to the real RS collapses 1.36 → 0.47 with drive, and which already shipped saturated (k_rs_drive = 2.2). Claves is a near-pure tone (adding harmonics moves away); cowbell's mismatch is oscillator tuning/mix, not the VCA.

Net: the swing VCA is a fidelity feature only where it was already applied (the rimshot). Elsewhere linear stays the default, with drive retained as an opt-in creative bend. The durable win is one swing-VCA implementation shared across tap.303~, tap.vca~, and every 808 voice.

Testing

ctest green (kernel suite, including all unchanged tr808_*/tb303 tests as the bit-identity gate, plus the new vca scenarios). Render tool builds and the drive knob is exercised by the notebook.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS


Generated by Claude Code

claude added 4 commits July 18, 2026 22:40
Lift the TB-303's one-transistor class-A VCA stage out of tb303_voice.h into
a standalone taptools::vca kernel so tap.303~ and the new standalone tap.vca~
run one implementation.

- vca.h: the two-circuit svf.h idiom applied to a gain stage. clean is the
  pure linear multiply (bit-identical to *~); warm is the slope-normalized
  biased saturator S(v) = (tanh(d*v+b) - tanh(b)) / (d*sech^2(b)) applied to
  the post-gain signal, so the even-harmonic warmth and compression track the
  control voltage. drive/bias exposed (stock d=2.0, b=0.3); optional
  output-coupling DC block for the shaper's signal-dependent offset.
- tb303_voice.h now composes vca::shape() with the stock constants and keeps
  its own Open303 output high-pass + gain, so the voice is bit-identical.
- tests/vca_test.cpp: pins the two-circuit contract and the bit-identity of
  shape() against the old inline formula, and re-runs the 303 warm-vs-clean
  path through the shared stage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS
…ices

The swing-type VCA's "many high harmonics" (Service Notes, RS/CL VCA), which
swing_vca() had modeled as a flat x*env. The first and lowest-risk piece of
the flagged 808 circuit-sim work.

- vca.h gains mode_swing: a symmetric (odd-harmonic) tanh(d*v)/d saturator,
  unity-slope-at-0 so quiet tails stay clean and hot transients pick up grit
  and compression, no DC (symmetric needs no coupling block). One
  implementation: a static vca::swing_shape(v, drive) that both mode_swing and
  swing_vca(x, env, drive) route through.
- drive defaults to 0 -> the exact linear passthru, so every calibrated 808
  voice is bit-identical until it opts in. The full tr808_* suite passes
  unchanged; the snare test adds an explicit off-is-bit-identical /
  on-saturates-and-compresses scenario.
- Wired into the three noise-path voices: snare (snappy), clap (CP + maracas),
  tom (noise reverberation), each with a set_drive circuit-bend.

The heavier WDF @circuit pass (kick bridged-T etc.) stays A/B-gated per the
field guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS
Adds tr808_swing_vca.ipynb — the calibration check for the 808 swing-VCA drive
against Fischer's free Technopolis reference set, alongside tr808_calibration.

Finding: the crest-factor (the direct measure of the swing VCA's compression)
shows our linear model already sits at or above the real 808's transient
sharpness on snare/clap/tom — the hardware isn't compressing these hits more
than we already do (consistent with the DAFx-14 "device nonlinearity matters
less than folklore" result). Drive only compresses past the reference, so it
stays a creative bend, not a fidelity default (the default is 0, so no code
change follows). The tonal voices the Service Notes actually cite (rim/claves,
cowbell) are where a symmetric shaper would earn its keep — a separate test.

Also teaches tr808_render the `drive` knob (--set drive N) on snare/clap/tom so
the notebook can render the swing variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS
Runs the same real-808 comparison on the voices the Service Notes actually cite
for the swing VCA's harmonics, and wires the tonal paths so it can.

Finding:
- Rimshot: swing is clearly warranted — spectral distance to the real RS falls
  1.36 -> 0.47 monotonically with drive. It already shipped saturated
  (k_rs_drive = 2.2); this validates that constant.
- Claves: the real CL is a near-pure ~2.5 kHz tone (HF ratio ~0); a symmetric
  shaper only adds harmonics it doesn't have -> keep linear.
- Cowbell: distance to the real CB is large and flat across drive -> the
  mismatch is oscillator tuning/mix, not the VCA -> keep linear.

So the swing VCA is a fidelity feature only where it was already applied (the
rimshot); everywhere else linear stays the default. The durable win is
unification: the rimshot's inline tanh(ring*k_rs_drive*env)/k_rs_drive is now
swing_vca(ring, env, k_rs_drive) on the shared vca.h shaper (bit-identical — the
rim/cowbell kernel tests pass unchanged).

- tr808_rim.h: rimshot unified onto swing_vca; claves gains an opt-in drive
  (sentinel <0 = per-model calibrated: RS 2.2 / CL linear).
- tr808_cowbell.h: opt-in drive on the osc pair (0 = linear, default).
- tr808_render: `--set drive` on rimshot/claves/cowbell for the sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS
Layout-only: satisfies the CI clang-format-18 check over the files touched by
this branch (vca.h, tb303_voice.h, tr808_tom.h, tests, the render tool). No
behavior change — the full kernel suite still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS
@tap
tap merged commit 76dc616 into main Jul 19, 2026
12 checks passed
tap pushed a commit to tap/TapTools-Max that referenced this pull request Jul 19, 2026
tap/TapTools#17 merged (rebase), rewriting the feature commits onto main with
new SHAs — so the previous pin (8e02ac5, a feature-branch commit) is no longer
on kernel main. Re-point submodules/taptools at the merged main tip, which
carries the identical swing-VCA kernel. No change to the built externals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS
tap added a commit to tap/TapTools-Max that referenced this pull request Jul 19, 2026
…e 808 noise voices (#53)

* tap.vca~: standalone VCA — clean multiply or 303 warm transistor stage

A new MSP external wrapping the extracted taptools::vca kernel (submodule pin
bumped to 253d550). The left inlet is audio; the right inlet is the gain /
control voltage (signal for per-sample modulation, or a float).

- circuit clean (default): pure linear multiply, bit-identical to *~.
- circuit warm: the TB-303 one-transistor class-A stage — the same kernel
  tap.303~ runs — biased-tanh saturation applied after the gain, so the
  even-harmonic warmth and compression track the control voltage, with an
  output DC block (dcblock) that sheds the shaper's signal-dependent offset.
- drive/bias shape the warm circuit (stock at the 303's 2.0 / 0.3).

Full vertical slice: wrapper + CMake + wrapper test (attribute defaults,
forwarding, clamping) + maxref + help patcher. REVIVAL.md §7 and the 303/808
plans note the extraction; when the flagged 808 circuit-sim phase models the
swing VCA's harmonics, that character lands here as a third circuit mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS

* 808 swing-VCA harmonics: drive on the noise voices, swing circuit on tap.vca~

Bumps the kernel pin to 1b3c284 (vca.h mode_swing + swing_vca drive).

- tap.808.snare~/clap~/tom~ gain a `drive` circuit-bend attribute (0..12,
  default 0 = the calibrated linear model, bit-identical). Raised, it engages
  the swing VCA's symmetric harmonic saturation on the noise path — grit and
  compression that ride the envelope. Wrapper tests + maxrefs updated.
- tap.vca~ gains `circuit swing` (the TR-808 swing-type VCA, the same stage),
  alongside clean/warm; `drive` now documented for both warm and swing.

REVIVAL.md §7 and plans/tap.808.md record the swing mode landing; the heavier
WDF @circuit pass stays A/B-gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS

* Bump kernel pin to the swing-VCA feature tip (8e02ac5)

Pulls in the bit-identical rimshot unification (its inline swing-VCA tanh is now
the shared vca.h swing_shape) and the calibration notebook / render-tool drive
knobs. No change to the built externals' behavior; keeps the Max package built
against the complete feature kernel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS

* style: clang-format + mandatory braces on the swing-VCA wrappers

Satisfies the Tap House CI style gate:
- clang-format-18 over the touched source/projects/*.cpp (tap.vca~ + the
  tap.808.{snare,clap,tom}~ drive attributes) — layout only.
- braces around the tap.vca~ circuit setter's if/else-if
  (readability-braces-around-statements).

No behavior change; the affected wrapper tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS

* Re-point kernel pin to the merged main tip (76dc616)

tap/TapTools#17 merged (rebase), rewriting the feature commits onto main with
new SHAs — so the previous pin (8e02ac5, a feature-branch commit) is no longer
on kernel main. Re-point submodules/taptools at the merged main tip, which
carries the identical swing-VCA kernel. No change to the built externals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants