From bb494875c85bc22bf102bc11466cbe142d4cfe8e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 22:41:24 +0000 Subject: [PATCH 1/5] =?UTF-8?q?tap.vca~:=20standalone=20VCA=20=E2=80=94=20?= =?UTF-8?q?clean=20multiply=20or=20303=20warm=20transistor=20stage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS --- REVIVAL.md | 17 ++ docs/tap.vca~.maxref.xml | 106 +++++++ help/tap.vca~.maxhelp | 285 ++++++++++++++++++ plans/tap.303.md | 5 +- plans/tap.808.md | 5 +- source/projects/tap.vca_tilde/CMakeLists.txt | 36 +++ .../projects/tap.vca_tilde/tap.vca_tilde.cpp | 158 ++++++++++ .../tap.vca_tilde/tap.vca_tilde_test.cpp | 99 ++++++ submodules/taptools | 2 +- 9 files changed, 710 insertions(+), 3 deletions(-) create mode 100644 docs/tap.vca~.maxref.xml create mode 100644 help/tap.vca~.maxhelp create mode 100644 source/projects/tap.vca_tilde/CMakeLists.txt create mode 100644 source/projects/tap.vca_tilde/tap.vca_tilde.cpp create mode 100644 source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp diff --git a/REVIVAL.md b/REVIVAL.md index 1740258f..0990067f 100644 --- a/REVIVAL.md +++ b/REVIVAL.md @@ -1058,6 +1058,23 @@ GitHub Actions CI. network, eight voices* (the `tr808_*` headers — the bridged-T solved on its states, the calibration lesson), and *Time as a function of phase* (`step_seq.h` — the O(1) derivation, gate-hold look-ahead, armed-recall re-derivation). +- ✅ **Net-new object — `tap.vca~` (2026-07-18).** The 303's transistor VCA stage, lifted out + where it can amplify anything. Kernel: **`vca.h`** (`taptools::vca`) — the two-circuit `svf.h` + idiom applied to a gain stage: `clean` is the pure linear multiply (bit-identical to `*~`), + `warm` is the one-transistor class-A saturator `S(v) = (tanh(d·v+b) − tanh(b)) / (d·sech²(b))` + (stock `d=2.0`, `b=0.3` — the probe-calibrated 303 constants, now exposed as `drive`/`bias`), + applied to the post-gain signal so the even-harmonic warmth and compression **track the control + voltage** the way a discrete VCA does and a multiply cannot, plus an optional output-coupling DC + block (`dcblock`) for the shaper's signal-dependent offset. **`tb303_voice.h` was refactored to + compose `vca::shape()`** — one implementation shared between `tap.303~` and the standalone; the + voice keeps its own Open303 output high-pass and gain, so the extraction left it **bit-identical** + (pinned by `tests/vca_test.cpp`, which matches `shape()` to the old inline formula and re-runs the + 303 warm-vs-clean scenarios). Wrapper `tap.vca_tilde` (audio in + signal/float gain CV in), + `circuit clean|warm` + `drive`/`bias`/`dcblock`/`bypass`/`mute`, the full vertical slice (maxref + + help patcher). The 808's swing-type VCA is still modeled linearly (`swing_vca()` = `x·env`); when + the flagged 808 circuit-sim phase models its "many high harmonics," that two-transistor character + lands here as a third circuit mode rather than inside eight drum voices. Remaining: runtime + validation in Max. --- diff --git a/docs/tap.vca~.maxref.xml b/docs/tap.vca~.maxref.xml new file mode 100644 index 00000000..2eaf2ed3 --- /dev/null +++ b/docs/tap.vca~.maxref.xml @@ -0,0 +1,106 @@ + + + + + + + Voltage-controlled amplifier — clean multiply or warm transistor stage + + + + The tap.vca~ object is a voltage-controlled amplifier. The left inlet is the + audio; the right inlet is the gain / control voltage — a signal for true per-sample + modulation, or a float to set the gain attribute. The circuit + attribute chooses the character: clean is a pure linear multiply, bit-identical + to *~ — transparent, with no added harmonics or DC. warm is the Roland + TB-303's one-transistor class-A VCA stage — the same kernel tap.303~ runs — a + biased, slope-normalized tanh applied after the gain: quiet signals stay essentially + clean, while hot signals pick up even-harmonic warmth and gentle compression that track + the control voltage, the way a discrete VCA does and a plain multiply cannot. An output + DC block (the dcblock attribute) sheds the shaped signal's DC offset, as the + hardware's output coupling capacitor does. The drive and bias + attributes shape the warm circuit (stock at the 303's values). One channel per object; + wrap in mc. for multichannel. + + + + + + 74 Objects + TapTools + Audio + MSP Modifiers + Multichannel + 64-bit + + + + + + + + In the left inlet, the audio to amplify. In the right inlet, the gain / + control voltage (linear), applied per sample. + + + + + + In the right inlet, sets the gain attribute. Ignored while a + signal is connected to that inlet. + + + + Reset the stage's DC-block state. + + + + + + + + Amplifier circuit: clean (default, the pure linear multiply, + bit-identical to *~) or warm (the TB-303 one-transistor class-A stage — biased-tanh + saturation applied after the gain, with even-harmonic warmth and compression that + ride the control voltage, plus the output DC block). + + + Linear gain applied when no signal is connected to the right inlet. A + signal in the right inlet overrides this per sample. + + + Warm-circuit drive into the transistor stage's tanh (0.1-12; default 2, + the stock 303 value). Higher drive means more harmonics and more compression. Ignored + by the clean circuit. + + + Warm-circuit operating-point bias (-2..2; default 0.3, the stock 303 + value). The asymmetry that produces even harmonics; 0 is a symmetric (odd-only) + shaper. Ignored by the clean circuit. + + + Warm-circuit output DC block (default on). The biased shaper leaves a + signal-dependent DC offset on AC material; this one-pole coupling high-pass removes + it, as the hardware's output capacitor does. Turn off to hear the raw + asymmetry. + + + Pass the input through unprocessed. + + + Silence the output. + + + + + + + + + + + + + + + diff --git a/help/tap.vca~.maxhelp b/help/tap.vca~.maxhelp new file mode 100644 index 00000000..656eddf8 --- /dev/null +++ b/help/tap.vca~.maxhelp @@ -0,0 +1,285 @@ +{ + "patcher" : { + "fileversion" : 1, + "appversion" : { + "major" : 8, + "minor" : 5, + "revision" : 0, + "architecture" : "x64", + "modernui" : 1 + } +, + "classnamespace" : "box", + "rect" : [ 84.0, 104.0, 640.0, 520.0 ], + "bglocked" : 0, + "openinpresentation" : 0, + "default_fontsize" : 12.0, + "default_fontface" : 0, + "default_fontname" : "Arial", + "gridonopen" : 1, + "gridsize" : [ 15.0, 15.0 ], + "gridsnaponopen" : 1, + "objectsnaponopen" : 1, + "statusbarvisible" : 2, + "toolbarvisible" : 1, + "lefttoolbarpinned" : 0, + "toptoolbarpinned" : 0, + "righttoolbarpinned" : 0, + "bottomtoolbarpinned" : 0, + "toolbars_unpinned_last_save" : 0, + "tallnewobj" : 0, + "boxanimatetime" : 200, + "enablehscroll" : 1, + "enablevscroll" : 1, + "devicewidth" : 0.0, + "description" : "", + "digest" : "", + "tags" : "", + "style" : "", + "subpatcher_template" : "", + "boxes" : [ { + "box" : { + "fontface" : 1, + "fontsize" : 14.0, + "id" : "obj-1", + "maxclass" : "comment", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 20.0, 12.0, 420.0, 23.0 ], + "text" : "tap.vca~ : voltage-controlled amplifier (clean or warm)" + } + + } +, { + "box" : { + "id" : "obj-2", + "linecount" : 3, + "maxclass" : "comment", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 20.0, 38.0, 600.0, 49.0 ], + "text" : "Left inlet: audio. Right inlet: gain / control voltage (a signal for per-sample modulation, or a float). circuit clean is a pure multiply (like *~); circuit warm is the TB-303 transistor stage — even-harmonic warmth and compression that ride the control voltage, with an output DC block." + } + + } +, { + "box" : { + "id" : "obj-3", + "maxclass" : "newobj", + "numinlets" : 1, + "numoutlets" : 1, + "outlettype" : [ "signal" ], + "patching_rect" : [ 20.0, 110.0, 74.0, 22.0 ], + "text" : "cycle~ 220" + } + + } +, { + "box" : { + "id" : "obj-4", + "maxclass" : "newobj", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "signal" ], + "patching_rect" : [ 150.0, 175.0, 60.0, 22.0 ], + "text" : "line~" + } + + } +, { + "box" : { + "id" : "obj-5", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 150.0, 138.0, 78.0, 22.0 ], + "text" : "1. 1000" + } + + } +, { + "box" : { + "id" : "obj-6", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 234.0, 138.0, 78.0, 22.0 ], + "text" : "0. 1000" + } + + } +, { + "box" : { + "id" : "obj-7", + "maxclass" : "comment", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 318.0, 141.0, 200.0, 20.0 ], + "text" : "a control-voltage envelope" + } + + } +, { + "box" : { + "id" : "obj-8", + "maxclass" : "newobj", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "signal" ], + "patching_rect" : [ 20.0, 230.0, 130.0, 22.0 ], + "text" : "tap.vca~ @circuit warm" + } + + } +, { + "box" : { + "id" : "obj-9", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 165.0, 230.0, 96.0, 22.0 ], + "text" : "circuit clean" + } + + } +, { + "box" : { + "id" : "obj-10", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 165.0, 258.0, 96.0, 22.0 ], + "text" : "circuit warm" + } + + } +, { + "box" : { + "id" : "obj-11", + "maxclass" : "comment", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 270.0, 244.0, 250.0, 20.0 ], + "text" : "switch the amplifier circuit" + } + + } +, { + "box" : { + "id" : "obj-12", + "maxclass" : "newobj", + "numinlets" : 2, + "numoutlets" : 0, + "patching_rect" : [ 20.0, 340.0, 130.0, 22.0 ], + "text" : "ezdac~" + } + + } +, { + "box" : { + "id" : "obj-13", + "maxclass" : "newobj", + "numinlets" : 1, + "numoutlets" : 1, + "outlettype" : [ "signal" ], + "patching_rect" : [ 20.0, 290.0, 74.0, 22.0 ], + "text" : "*~ 0.5" + } + + } +, { + "box" : { + "id" : "obj-14", + "maxclass" : "newobj", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 318.0, 290.0, 130.0, 130.0 ], + "text" : "scope~" + } + + } + ], + "lines" : [ { + "patchline" : { + "destination" : [ "obj-8", 0 ], + "source" : [ "obj-3", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-4", 0 ], + "source" : [ "obj-5", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-4", 0 ], + "source" : [ "obj-6", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-8", 1 ], + "source" : [ "obj-4", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-8", 0 ], + "source" : [ "obj-9", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-8", 0 ], + "source" : [ "obj-10", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-13", 0 ], + "source" : [ "obj-8", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-14", 0 ], + "source" : [ "obj-8", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-12", 0 ], + "source" : [ "obj-13", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-12", 1 ], + "source" : [ "obj-13", 0 ] + } + + } + ], + "dependency_cache" : [ { + "name" : "tap.vca~.mxo", + "type" : "iLaX" + } + ], + "autosave" : 0 + } + +} diff --git a/plans/tap.303.md b/plans/tap.303.md index beccecbe..72ff921c 100644 --- a/plans/tap.303.md +++ b/plans/tap.303.md @@ -252,7 +252,10 @@ each gated by A/B evidence (the 808 plan's go/no-go discipline) — all resolved slope-normalized biased saturator in the hardware order (post-envelope-gain, pre-output-coupling). Measured: 5.4% difference signal on quiet notes vs 11.5% on hot accents — the distortion tracks the envelope. `clean` stays the default, - bit-identical to phase 1. + bit-identical to phase 1. **Extracted to the shared `vca.h` kernel (`taptools::vca`) + (2026-07-18)** — the voice now composes `vca::shape()`, one implementation shared with + the standalone `tap.vca~` object; the refactor is bit-identical (pinned by the kernel's + `vca_test.cpp`). - **Square-shaper exactness** — ✅ **resolved in slice 4**: Open303's measured shaper constants adopted verbatim (−tanh(10^(36.9/20)·saw + 4.37)). - **WDF pass on the filter** — ✅ **documented no-go, author-approved diff --git a/plans/tap.808.md b/plans/tap.808.md index fb586bc4..27eae8a7 100644 --- a/plans/tap.808.md +++ b/plans/tap.808.md @@ -101,7 +101,10 @@ selecting between two related sounds sharing one circuit): rapid pulses — the "multiple hands" transient — plus a parallel slower noise decay), and MA (high-passed noise burst with a very fast envelope). 4. **Swing-type VCAs and RC decay envelopes** — the percussive gain shapes are simple - discharge curves and one-transistor VCAs, not ADSRs. + discharge curves and one-transistor VCAs, not ADSRs. Modeled linearly today + (`swing_vca()` = `x·env`); when the flagged circuit-sim phase models the swing VCA's + "many high harmonics" (Service Notes), that nonlinearity lands in the shared `vca.h` + kernel as a third circuit mode — the home the standalone `tap.vca~` set up for it. 5. **The accent bus** — a shared voltage, scaled by the AC level knob, that raises the trigger pulse amplitude of every voice sounding on an accented step (and thereby, on some voices, subtly shifts timbre — the excitation is hotter, not just louder). diff --git a/source/projects/tap.vca_tilde/CMakeLists.txt b/source/projects/tap.vca_tilde/CMakeLists.txt new file mode 100644 index 00000000..e32b752d --- /dev/null +++ b/source/projects/tap.vca_tilde/CMakeLists.txt @@ -0,0 +1,36 @@ +# Copyright 1999-2026 Timothy Place. Distributed under the New BSD License. + +cmake_minimum_required(VERSION 3.19) + +set(C74_MIN_API_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../min-api) +include(${C74_MIN_API_DIR}/script/min-pretarget.cmake) + +############################################################# +# MAX EXTERNAL +############################################################# + +include_directories("${C74_INCLUDES}") +# The portable DSP kernel (header-only), pinned as the submodules/taptools submodule. +include_directories("${TAPTOOLS_KERNEL_DIR}/include") + +set(SOURCES + ${PROJECT_NAME}.cpp + ${TAPTOOLS_KERNEL_DIR}/include/taptools/vca.h +) + +add_library( + ${PROJECT_NAME} + MODULE + ${SOURCES} +) + +include(${C74_MIN_API_DIR}/script/min-posttarget.cmake) + +# TapTools targets C++20. Min's posttarget pins the standard to C++17, so override it here. +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + +# Catch-based unit tests (built when the _test.cpp is present; run via ctest). +include(${C74_MIN_API_DIR}/test/min-object-unittest.cmake) diff --git a/source/projects/tap.vca_tilde/tap.vca_tilde.cpp b/source/projects/tap.vca_tilde/tap.vca_tilde.cpp new file mode 100644 index 00000000..df926f3a --- /dev/null +++ b/source/projects/tap.vca_tilde/tap.vca_tilde.cpp @@ -0,0 +1,158 @@ +/// @file +/// @brief tap.vca~ — voltage-controlled amplifier: a clean multiply or a warm transistor stage. +/// @details A thin Min wrapper around the portable kernel in vca.h (all DSP lives there — see its +/// header comment for the model). The left inlet is the audio; the right inlet is the +/// gain / control voltage (a signal for true per-sample modulation, or a float to set +/// the gain attribute). Two circuits, the svf~ idiom: +/// +/// - circuit clean (default): the pure linear multiply, out = in * gain — bit-identical +/// to a *~. Transparent, cheap, no added harmonics or DC. +/// - circuit warm: the TB-303's one-transistor class-A VCA stage — the same code +/// tap.303~ runs (shared kernel, vca.h). A slope-normalized biased tanh applied to +/// the post-gain signal: unity gain through zero (quiet signals near-clean), even +/// harmonics and gentle compression that ride the control voltage on hot signals, and +/// an output-coupling DC block (the dcblock attribute) that sheds the shaper's +/// signal-dependent offset the way the hardware's coupling capacitor does. +/// +/// Single-channel: wrap in mc. for multichannel (the kernel is reusable outside Max). +/// @author Timothy Place +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Timothy Place. + +#include + +#include + +#include "c74_min.h" + +using namespace c74::min; + +class vca : public object, public vector_operator<> { + private: + // Constructed before the attributes below so their defaults can forward into it. + taptools::vca m_vca; + + public: + MIN_DESCRIPTION{"A voltage-controlled amplifier. The left inlet is the audio, the right inlet " + "is the gain / control voltage (a signal for per-sample modulation, or a float " + "for a fixed gain). The clean circuit is a pure linear multiply (bit-identical " + "to *~). The warm circuit is the TB-303's one-transistor class-A VCA stage — a " + "biased, slope-normalized tanh applied after the gain, so quiet signals stay " + "essentially clean while hot signals pick up even-harmonic warmth and gentle " + "compression that track the control voltage; an output DC block removes the " + "stage's signal-dependent offset. Single-channel: wrap in mc. for multichannel."}; + MIN_TAGS{"filters"}; + MIN_AUTHOR{"Timothy Place"}; + MIN_RELATED{"tap.303~, tap.crossfade~, tap.pan~, *~, gain~, matrix~"}; + + inlet<> m_in{this, "(signal) audio input"}; + inlet<> m_in_gain{this, "(signal/float) gain / control voltage (linear)"}; + outlet<> m_out{this, "(signal) amplified output", "signal"}; + + vca(const atoms& args = {}) { m_vca.prepare(samplerate()); } + + attribute circuit{ + this, "circuit", "clean", range{"clean", "warm"}, + setter{ MIN_FUNCTION { + m_vca.set_mode(args[0] == "warm" ? taptools::vca::mode_warm : taptools::vca::mode_clean); + return args; + }}, + description{"clean: the pure linear multiply (transparent, bit-identical to *~). " + "warm: the TB-303 one-transistor class-A stage — biased-tanh saturation applied " + "after the gain, with even-harmonic warmth and compression that ride the control " + "voltage, plus the output DC block (see the dcblock attribute)."}}; + + attribute gain{this, "gain", 1.0, + description{"Linear gain applied when no signal is connected to the right " + "inlet. A signal in the right inlet overrides this per sample."}}; + + attribute drive{ + this, "drive", taptools::vca::k_default_drive, range{0.1, 12.0}, + setter{ MIN_FUNCTION { + const double v = std::clamp(static_cast(args[0]), 0.1, 12.0); + m_vca.set_drive(v); + return {v}; + }}, + description{"Warm-circuit drive into the transistor stage's tanh (0.1..12; default 2.0, the " + "stock 303 value). Higher drive means more harmonics and more compression. " + "Ignored by the clean circuit."}}; + + attribute bias{ + this, "bias", taptools::vca::k_default_bias, range{-2.0, 2.0}, + setter{ MIN_FUNCTION { + const double v = std::clamp(static_cast(args[0]), -2.0, 2.0); + m_vca.set_bias(v); + return {v}; + }}, + description{"Warm-circuit operating-point bias (-2..2; default 0.3, the stock 303 value). " + "The asymmetry that produces even harmonics; 0 is a symmetric (odd-only) shaper. " + "Ignored by the clean circuit."}}; + + attribute dcblock{this, "dcblock", true, + setter{ MIN_FUNCTION { + m_vca.set_dc_block(static_cast(args[0])); + return args; + }}, + description{"Warm-circuit output DC block (default on). The biased shaper " + "leaves a signal-dependent DC offset on AC material; this " + "one-pole coupling high-pass removes it, as the hardware's " + "output capacitor does. Turn off to hear the raw asymmetry."}}; + + attribute bypass{this, "bypass", false, description{"Pass the input through unprocessed."}}; + + attribute mute{this, "mute", false, description{"Silence the output."}}; + + // -- messages ------------------------------------------------------------------------------- + + message<> m_number{this, "number", "A float in the right inlet sets the gain.", + MIN_FUNCTION { + if (inlet == 1) { + gain = args[0]; + } + return {}; + }}; + + message<> clear{this, "clear", "Reset the stage's DC-block state.", + MIN_FUNCTION { + m_vca.reset(); + return {}; + }}; + + message<> dspsetup{this, "dspsetup", "Reconfigure for the sample rate when DSP starts.", + MIN_FUNCTION { + m_vca.prepare(samplerate()); + return {}; + }}; + + // -- perform -------------------------------------------------------------------------------- + + void operator()(audio_bundle input, audio_bundle output) override { + const long n = input.frame_count(); + const double* in = input.samples(0); + double* out = output.samples(0); + + if (mute) { + std::fill_n(out, n, 0.0); + } + else if (bypass) { + std::copy_n(in, n, out); + } + else if (m_in_gain.has_signal_connection()) { + const double* g = input.samples(1); + for (long i = 0; i < n; ++i) { + out[i] = m_vca.process(in[i], g[i]); // true per-sample gain / CV modulation + } + } + else { + const double g = gain; + for (long i = 0; i < n; ++i) { + out[i] = m_vca.process(in[i], g); + } + } + } + + // Exposed for unit tests. + taptools::vca& stage() { return m_vca; } +}; + +MIN_EXTERNAL(vca); diff --git a/source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp b/source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp new file mode 100644 index 00000000..20a7d3a7 --- /dev/null +++ b/source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp @@ -0,0 +1,99 @@ +/// @file +/// @brief Unit tests for tap.vca~ and its vca.h kernel. +/// @details Kernel scenarios exercise taptools::vca directly; one scenario goes through the Min +/// mock for the wrapper's attribute defaults, forwarding, and clamping. The deep DSP +/// correctness tests live in the kernel repo (tests/vca_test.cpp); this pins the +/// Min-level behavior. The harness pins this translation unit to C++17. +/// @author Timothy Place +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Timothy Place. + +#include +#include + +#include "c74_min_unittest.h" +#include "tap.vca_tilde.cpp" + +namespace { + + constexpr double k_sr = 48000.0; + + taptools::vca make_stage(int mode = taptools::vca::mode_clean) { + taptools::vca a; + a.prepare(k_sr); + a.set_mode(mode); + return a; + } + +} // namespace + +SCENARIO("the clean circuit is an exact linear multiply") { + auto a = make_stage(taptools::vca::mode_clean); + THEN("process(x, gain) == x * gain to the bit") { + for (double g : {0.0, 0.5, 1.0, 2.0, -0.5}) { + for (double x : {-0.8, 0.0, 0.42}) { + REQUIRE(a.process(x, g) == x * g); + } + } + } +} + +SCENARIO("the warm circuit adds tracking saturation but leaves quiet signals near-clean") { + auto a = make_stage(taptools::vca::mode_warm); + THEN("a tiny signal passes at ~unity gain") { + REQUIRE(a.shape(1e-4) == Approx(1e-4).epsilon(1e-3)); + } + THEN("a hot signal is compressed relative to clean") { + REQUIRE(std::abs(a.shape(1.5)) < 1.5); + } + THEN("the shaper is monotonic across the working range") { + double prev = a.shape(-2.0); + for (double v = -1.9; v <= 2.0; v += 0.1) { + REQUIRE(a.shape(v) > prev); + prev = a.shape(v); + } + } +} + +SCENARIO("the Min wrapper instantiates and takes its attributes") { + ext_main(nullptr); + GIVEN("an instance of tap.vca~") { + test_wrapper an_instance; + vca& my_object = an_instance; + + THEN("the defaults are sane") { + REQUIRE(my_object.circuit == symbol("clean")); + REQUIRE(static_cast(my_object.gain) == 1.0); + REQUIRE(static_cast(my_object.drive) == taptools::vca::k_default_drive); + REQUIRE(static_cast(my_object.bias) == taptools::vca::k_default_bias); + REQUIRE(static_cast(my_object.dcblock) == true); + REQUIRE(my_object.stage().circuit() == taptools::vca::mode_clean); + } + + WHEN("the circuit is switched to warm") { + my_object.circuit = "warm"; + THEN("the stage follows") { + REQUIRE(my_object.circuit == symbol("warm")); + REQUIRE(my_object.stage().circuit() == taptools::vca::mode_warm); + } + } + + WHEN("drive and bias are set out of range") { + my_object.drive = 100.0; // clamps to 12 + my_object.bias = -9.0; // clamps to -2 + THEN("they clamp to the documented range and reach the stage") { + REQUIRE(static_cast(my_object.drive) == 12.0); + REQUIRE(static_cast(my_object.bias) == -2.0); + REQUIRE(my_object.stage().drive() == 12.0); + REQUIRE(my_object.stage().bias() == -2.0); + } + } + + WHEN("dcblock is turned off") { + my_object.dcblock = false; + THEN("the stage follows") { + REQUIRE(my_object.stage().dc_block() == false); + } + } + } +} diff --git a/submodules/taptools b/submodules/taptools index fbcf3261..253d5503 160000 --- a/submodules/taptools +++ b/submodules/taptools @@ -1 +1 @@ -Subproject commit fbcf32617d3731b98327d575229298dd946f2159 +Subproject commit 253d55030721ab21317fb24a8e79ce4d8d860ed2 From 0d3ae7cb0894172ed35f73a9e4cf7554315111ed Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 23:24:03 +0000 Subject: [PATCH 2/5] 808 swing-VCA harmonics: drive on the noise voices, swing circuit on tap.vca~ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS --- REVIVAL.md | 21 ++++++++++--- docs/tap.808.clap~.maxref.xml | 9 ++++++ docs/tap.808.snare~.maxref.xml | 9 ++++++ docs/tap.808.tom~.maxref.xml | 9 ++++++ docs/tap.vca~.maxref.xml | 14 +++++---- plans/tap.808.md | 9 +++--- .../tap.808.clap_tilde/tap.808.clap_tilde.cpp | 10 +++++++ .../tap.808.snare_tilde.cpp | 10 +++++++ .../tap.808.tom_tilde/tap.808.tom_tilde.cpp | 10 +++++++ .../projects/tap.vca_tilde/tap.vca_tilde.cpp | 30 ++++++++++++------- .../tap.vca_tilde/tap.vca_tilde_test.cpp | 18 +++++++++++ submodules/taptools | 2 +- 12 files changed, 126 insertions(+), 25 deletions(-) diff --git a/REVIVAL.md b/REVIVAL.md index 0990067f..a039ee12 100644 --- a/REVIVAL.md +++ b/REVIVAL.md @@ -1071,10 +1071,23 @@ GitHub Actions CI. (pinned by `tests/vca_test.cpp`, which matches `shape()` to the old inline formula and re-runs the 303 warm-vs-clean scenarios). Wrapper `tap.vca_tilde` (audio in + signal/float gain CV in), `circuit clean|warm` + `drive`/`bias`/`dcblock`/`bypass`/`mute`, the full vertical slice (maxref + - help patcher). The 808's swing-type VCA is still modeled linearly (`swing_vca()` = `x·env`); when - the flagged 808 circuit-sim phase models its "many high harmonics," that two-transistor character - lands here as a third circuit mode rather than inside eight drum voices. Remaining: runtime - validation in Max. + help patcher). Remaining: runtime validation in Max. +- ✅ **The 808 swing-VCA harmonics — `vca.h` `swing` mode + the noise voices (2026-07-18).** The + first piece of the flagged 808 circuit-sim work, and the lowest-risk one: the swing-type VCA's + "many high harmonics" (Service Notes, RS/CL VCA), which `swing_vca()` had modeled as a flat + `x·env`. Kernel: **`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 → no coupling block needed). 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 + stays **bit-identical** until it opts in (the full existing `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 output), **tom** + (noise "reverberation") — each exposing a `drive` circuit-bend attribute (0..12). `tap.vca~` gains + the matching `circuit swing`. The heavier WDF `@circuit` pass (the kick bridged-T etc.) stays + where the field guide left it — gated on an A/B showing an audible delta the informed model misses, + which the DAFx-14 finding suggests may never open. Remaining: A/B calibration of the swing `drive` + against reference, and runtime validation in Max. --- diff --git a/docs/tap.808.clap~.maxref.xml b/docs/tap.808.clap~.maxref.xml index cc5634ac..4ad29374 100644 --- a/docs/tap.808.clap~.maxref.xml +++ b/docs/tap.808.clap~.maxref.xml @@ -90,6 +90,15 @@ it, leaving only the three teeth; above 1 exaggerates the wash. + + Circuit bend: swing-VCA drive on the output VCA (default 0 = linear) + + Swing-VCA drive on the output VCA, 0..12, for both the clap and maracas models. + 0 is the calibrated linear model (default); higher engages the swing VCA's + symmetric harmonic saturation — grit and compression that ride the envelope. + See tap.vca~ (the swing circuit). + + Noise seed (deterministic) diff --git a/docs/tap.808.snare~.maxref.xml b/docs/tap.808.snare~.maxref.xml index c0d31ce4..1511f121 100644 --- a/docs/tap.808.snare~.maxref.xml +++ b/docs/tap.808.snare~.maxref.xml @@ -99,6 +99,15 @@ (~238/476 Hz) — about 1.4 here. + + Circuit bend: swing-VCA drive on the snappy path (default 0 = linear) + + Swing-VCA drive on the snappy noise path, 0..12. 0 is the calibrated linear + model (default); higher engages the swing VCA's symmetric harmonic saturation — + grit and compression that ride the snappy envelope, hardest on the transient + crack. See tap.vca~ (the swing circuit). + + Noise seed (deterministic) diff --git a/docs/tap.808.tom~.maxref.xml b/docs/tap.808.tom~.maxref.xml index 67f1b1d1..b40e46fb 100644 --- a/docs/tap.808.tom~.maxref.xml +++ b/docs/tap.808.tom~.maxref.xml @@ -91,6 +91,15 @@ Output level Output level, 0..1 (default 1). + + Circuit bend: swing-VCA drive on the noise layer (default 0 = linear) + + Swing-VCA drive on the noise "reverberation" layer, 0..12 (toms only — congas + have no noise layer). 0 is the calibrated linear model (default); higher engages + the swing VCA's symmetric harmonic saturation riding the noise envelope. See + tap.vca~ (the swing circuit). + + Noise seed (toms; deterministic) Seed (>= 1) for the tom noise layer; give mc. instances different seeds. diff --git a/docs/tap.vca~.maxref.xml b/docs/tap.vca~.maxref.xml index 2eaf2ed3..f8537490 100644 --- a/docs/tap.vca~.maxref.xml +++ b/docs/tap.vca~.maxref.xml @@ -60,18 +60,20 @@ Amplifier circuit: clean (default, the pure linear multiply, - bit-identical to *~) or warm (the TB-303 one-transistor class-A stage — biased-tanh - saturation applied after the gain, with even-harmonic warmth and compression that - ride the control voltage, plus the output DC block). + bit-identical to *~), warm (the TB-303 one-transistor class-A stage — asymmetric + biased-tanh saturation with even harmonics, plus the output DC block), or swing (the + TR-808 swing-type VCA — symmetric saturation with odd harmonics and no DC, the same + stage the 808 noise voices use). In warm and swing the warmth and compression ride the + control voltage; bias is ignored by swing. Linear gain applied when no signal is connected to the right inlet. A signal in the right inlet overrides this per sample. - Warm-circuit drive into the transistor stage's tanh (0.1-12; default 2, - the stock 303 value). Higher drive means more harmonics and more compression. Ignored - by the clean circuit. + Drive into the transistor stage's tanh for the warm and swing circuits + (0.1-12; default 2, the stock 303 value). Higher drive means more harmonics and more + compression. Ignored by the clean circuit. Warm-circuit operating-point bias (-2..2; default 0.3, the stock 303 diff --git a/plans/tap.808.md b/plans/tap.808.md index 27eae8a7..6f5fbd2d 100644 --- a/plans/tap.808.md +++ b/plans/tap.808.md @@ -101,10 +101,11 @@ selecting between two related sounds sharing one circuit): rapid pulses — the "multiple hands" transient — plus a parallel slower noise decay), and MA (high-passed noise burst with a very fast envelope). 4. **Swing-type VCAs and RC decay envelopes** — the percussive gain shapes are simple - discharge curves and one-transistor VCAs, not ADSRs. Modeled linearly today - (`swing_vca()` = `x·env`); when the flagged circuit-sim phase models the swing VCA's - "many high harmonics" (Service Notes), that nonlinearity lands in the shared `vca.h` - kernel as a third circuit mode — the home the standalone `tap.vca~` set up for it. + discharge curves and one-transistor VCAs, not ADSRs. The swing VCA's "many high + harmonics" (Service Notes) shipped 2026-07-18 as `vca.h`'s `swing` mode (a symmetric + `swing_shape`), wired into the noise voices (snare/clap/tom) behind an opt-in `drive` + attribute that defaults to 0 → the calibrated linear model, bit-identical. The heavier + WDF `@circuit` pass on the resonant voices remains A/B-gated (see the field guide). 5. **The accent bus** — a shared voltage, scaled by the AC level knob, that raises the trigger pulse amplitude of every voice sounding on an accented step (and thereby, on some voices, subtly shifts timbre — the excitation is hotter, not just louder). diff --git a/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp b/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp index e9fb2177..10acf5f9 100644 --- a/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp +++ b/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp @@ -72,6 +72,16 @@ class clap808 : public object, public sample_operator<1, 1> { description{"Circuit bend (clap model): reverberation-tail level, 0..2. 1 is stock; 0 " "disconnects the wash, leaving only the three teeth."}}; + attribute drive{this, "drive", 0.0, setter{MIN_FUNCTION{ + const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); + m_clap.set_drive(v); + return {v}; + }}, + description{"Circuit bend: swing-VCA drive on the output VCA (0..12), for both the " + "clap and maracas models. 0 is the calibrated linear model (default); " + "higher engages the swing VCA's symmetric harmonic saturation — grit and " + "compression that ride the envelope."}}; + attribute seed{this, "seed", 1, setter{MIN_FUNCTION{ const int v = std::max(1, static_cast(args[0])); m_clap.set_seed(static_cast(v)); diff --git a/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp b/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp index 9a133ca6..eac02bf9 100644 --- a/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp +++ b/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp @@ -74,6 +74,16 @@ class snare808 : public object, public sample_operator<1, 1> { description{"Circuit bend: pitch as a ratio of the stock tuning (0.25..4), scaling " "both resonators together. 1 is the schematic (~173/336 Hz)."}}; + attribute drive{this, "drive", 0.0, setter{MIN_FUNCTION{ + const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); + m_snare.set_drive(v); + return {v}; + }}, + description{"Circuit bend: swing-VCA drive on the snappy noise path (0..12). 0 is the " + "calibrated linear model (default); higher engages the swing VCA's " + "symmetric harmonic saturation — grit and compression that ride the snappy " + "envelope, hardest on the transient crack."}}; + attribute seed{this, "seed", 1, setter{MIN_FUNCTION{ const int v = std::max(1, static_cast(args[0])); m_snare.set_seed(static_cast(v)); diff --git a/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp b/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp index 05368e19..2b9eb1b0 100644 --- a/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp +++ b/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp @@ -82,6 +82,16 @@ class tom808 : public object, public sample_operator<1, 1> { }}, description{"Output level, 0..1."}}; + attribute drive{this, "drive", 0.0, setter{MIN_FUNCTION{ + const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); + m_tom.set_drive(v); + return {v}; + }}, + description{"Circuit bend: swing-VCA drive on the noise 'reverberation' layer (0..12; " + "toms only — congas have no noise layer). 0 is the calibrated linear model " + "(default); higher engages the swing VCA's symmetric harmonic saturation " + "riding the noise envelope."}}; + attribute seed{this, "seed", 1, setter{MIN_FUNCTION{ const int v = std::max(1, static_cast(args[0])); m_tom.set_seed(static_cast(v)); diff --git a/source/projects/tap.vca_tilde/tap.vca_tilde.cpp b/source/projects/tap.vca_tilde/tap.vca_tilde.cpp index df926f3a..8d7043e7 100644 --- a/source/projects/tap.vca_tilde/tap.vca_tilde.cpp +++ b/source/projects/tap.vca_tilde/tap.vca_tilde.cpp @@ -40,10 +40,12 @@ class vca : public object, public vector_operator<> { "biased, slope-normalized tanh applied after the gain, so quiet signals stay " "essentially clean while hot signals pick up even-harmonic warmth and gentle " "compression that track the control voltage; an output DC block removes the " - "stage's signal-dependent offset. Single-channel: wrap in mc. for multichannel."}; + "stage's signal-dependent offset. The swing circuit is the TR-808 swing-type " + "VCA — symmetric (odd-harmonic) saturation, the same stage the 808 noise voices " + "use. Single-channel: wrap in mc. for multichannel."}; MIN_TAGS{"filters"}; MIN_AUTHOR{"Timothy Place"}; - MIN_RELATED{"tap.303~, tap.crossfade~, tap.pan~, *~, gain~, matrix~"}; + MIN_RELATED{"tap.303~, tap.808.snare~, tap.crossfade~, tap.pan~, *~, gain~, matrix~"}; inlet<> m_in{this, "(signal) audio input"}; inlet<> m_in_gain{this, "(signal/float) gain / control voltage (linear)"}; @@ -52,15 +54,23 @@ class vca : public object, public vector_operator<> { vca(const atoms& args = {}) { m_vca.prepare(samplerate()); } attribute circuit{ - this, "circuit", "clean", range{"clean", "warm"}, + this, "circuit", "clean", range{"clean", "warm", "swing"}, setter{ MIN_FUNCTION { - m_vca.set_mode(args[0] == "warm" ? taptools::vca::mode_warm : taptools::vca::mode_clean); + int m = taptools::vca::mode_clean; + if (args[0] == "warm") + m = taptools::vca::mode_warm; + else if (args[0] == "swing") + m = taptools::vca::mode_swing; + m_vca.set_mode(m); return args; }}, description{"clean: the pure linear multiply (transparent, bit-identical to *~). " - "warm: the TB-303 one-transistor class-A stage — biased-tanh saturation applied " - "after the gain, with even-harmonic warmth and compression that ride the control " - "voltage, plus the output DC block (see the dcblock attribute)."}}; + "warm: the TB-303 one-transistor class-A stage — asymmetric biased-tanh " + "saturation (even harmonics) applied after the gain, plus the output DC block " + "(see dcblock). swing: the TR-808 swing-type VCA — symmetric saturation " + "(odd harmonics, no DC), the same stage the 808 noise voices use. In warm and " + "swing the warmth and compression ride the control voltage; bias is ignored by " + "swing (symmetric by construction)."}}; attribute gain{this, "gain", 1.0, description{"Linear gain applied when no signal is connected to the right " @@ -73,9 +83,9 @@ class vca : public object, public vector_operator<> { m_vca.set_drive(v); return {v}; }}, - description{"Warm-circuit drive into the transistor stage's tanh (0.1..12; default 2.0, the " - "stock 303 value). Higher drive means more harmonics and more compression. " - "Ignored by the clean circuit."}}; + description{"Drive into the transistor stage's tanh for the warm and swing circuits " + "(0.1..12; default 2.0, the stock 303 value). Higher drive means more harmonics " + "and more compression. Ignored by the clean circuit."}}; attribute bias{ this, "bias", taptools::vca::k_default_bias, range{-2.0, 2.0}, diff --git a/source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp b/source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp index 20a7d3a7..5a856834 100644 --- a/source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp +++ b/source/projects/tap.vca_tilde/tap.vca_tilde_test.cpp @@ -55,6 +55,16 @@ SCENARIO("the warm circuit adds tracking saturation but leaves quiet signals nea } } +SCENARIO("the swing circuit is the TR-808 symmetric saturator (odd harmonics, no DC)") { + auto a = make_stage(taptools::vca::mode_swing); + a.set_drive(3.0); + THEN("drive 0 is the exact linear passthru; drive > 0 is a compressing odd function") { + REQUIRE(taptools::vca::swing_shape(0.6, 0.0) == 0.6); + REQUIRE(std::abs(a.shape(0.7) + a.shape(-0.7)) < 1e-12); // symmetric + REQUIRE(std::abs(a.shape(1.4)) < 1.4); // compresses hot signals + } +} + SCENARIO("the Min wrapper instantiates and takes its attributes") { ext_main(nullptr); GIVEN("an instance of tap.vca~") { @@ -78,6 +88,14 @@ SCENARIO("the Min wrapper instantiates and takes its attributes") { } } + WHEN("the circuit is switched to swing") { + my_object.circuit = "swing"; + THEN("the stage follows (the TR-808 symmetric saturator)") { + REQUIRE(my_object.circuit == symbol("swing")); + REQUIRE(my_object.stage().circuit() == taptools::vca::mode_swing); + } + } + WHEN("drive and bias are set out of range") { my_object.drive = 100.0; // clamps to 12 my_object.bias = -9.0; // clamps to -2 diff --git a/submodules/taptools b/submodules/taptools index 253d5503..1b3c284a 160000 --- a/submodules/taptools +++ b/submodules/taptools @@ -1 +1 @@ -Subproject commit 253d55030721ab21317fb24a8e79ce4d8d860ed2 +Subproject commit 1b3c284a95f6c929ceb72ae81208849dcab5a480 From 5c16ab4ca1c44de236f952bdbc256164e20fc215 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 02:30:45 +0000 Subject: [PATCH 3/5] 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 Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS --- submodules/taptools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/taptools b/submodules/taptools index 1b3c284a..8e02ac5f 160000 --- a/submodules/taptools +++ b/submodules/taptools @@ -1 +1 @@ -Subproject commit 1b3c284a95f6c929ceb72ae81208849dcab5a480 +Subproject commit 8e02ac5fe25a9baf275821db4eedb6c3838a951e From 9bb0df36638a1bffd67c93c33c5590e13d12c62e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 03:03:43 +0000 Subject: [PATCH 4/5] style: clang-format + mandatory braces on the swing-VCA wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS --- .../tap.808.clap_tilde/tap.808.clap_tilde.cpp | 16 ++++++------- .../tap.808.snare_tilde.cpp | 16 ++++++------- .../tap.808.tom_tilde/tap.808.tom_tilde.cpp | 16 ++++++------- .../projects/tap.vca_tilde/tap.vca_tilde.cpp | 24 +++++++++++++------ 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp b/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp index 10acf5f9..c7fb94ad 100644 --- a/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp +++ b/source/projects/tap.808.clap_tilde/tap.808.clap_tilde.cpp @@ -73,14 +73,14 @@ class clap808 : public object, public sample_operator<1, 1> { "disconnects the wash, leaving only the three teeth."}}; attribute drive{this, "drive", 0.0, setter{MIN_FUNCTION{ - const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); - m_clap.set_drive(v); - return {v}; - }}, - description{"Circuit bend: swing-VCA drive on the output VCA (0..12), for both the " - "clap and maracas models. 0 is the calibrated linear model (default); " - "higher engages the swing VCA's symmetric harmonic saturation — grit and " - "compression that ride the envelope."}}; + const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); + m_clap.set_drive(v); + return {v}; + }}, + description{"Circuit bend: swing-VCA drive on the output VCA (0..12), for both the " + "clap and maracas models. 0 is the calibrated linear model (default); " + "higher engages the swing VCA's symmetric harmonic saturation — grit and " + "compression that ride the envelope."}}; attribute seed{this, "seed", 1, setter{MIN_FUNCTION{ const int v = std::max(1, static_cast(args[0])); diff --git a/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp b/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp index eac02bf9..5368d43d 100644 --- a/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp +++ b/source/projects/tap.808.snare_tilde/tap.808.snare_tilde.cpp @@ -75,14 +75,14 @@ class snare808 : public object, public sample_operator<1, 1> { "both resonators together. 1 is the schematic (~173/336 Hz)."}}; attribute drive{this, "drive", 0.0, setter{MIN_FUNCTION{ - const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); - m_snare.set_drive(v); - return {v}; - }}, - description{"Circuit bend: swing-VCA drive on the snappy noise path (0..12). 0 is the " - "calibrated linear model (default); higher engages the swing VCA's " - "symmetric harmonic saturation — grit and compression that ride the snappy " - "envelope, hardest on the transient crack."}}; + const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); + m_snare.set_drive(v); + return {v}; + }}, + description{"Circuit bend: swing-VCA drive on the snappy noise path (0..12). 0 is the " + "calibrated linear model (default); higher engages the swing VCA's " + "symmetric harmonic saturation — grit and compression that ride the snappy " + "envelope, hardest on the transient crack."}}; attribute seed{this, "seed", 1, setter{MIN_FUNCTION{ const int v = std::max(1, static_cast(args[0])); diff --git a/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp b/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp index 2b9eb1b0..1b8d49c9 100644 --- a/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp +++ b/source/projects/tap.808.tom_tilde/tap.808.tom_tilde.cpp @@ -83,14 +83,14 @@ class tom808 : public object, public sample_operator<1, 1> { description{"Output level, 0..1."}}; attribute drive{this, "drive", 0.0, setter{MIN_FUNCTION{ - const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); - m_tom.set_drive(v); - return {v}; - }}, - description{"Circuit bend: swing-VCA drive on the noise 'reverberation' layer (0..12; " - "toms only — congas have no noise layer). 0 is the calibrated linear model " - "(default); higher engages the swing VCA's symmetric harmonic saturation " - "riding the noise envelope."}}; + const double v = MIN_CLAMP(static_cast(args[0]), 0.0, 12.0); + m_tom.set_drive(v); + return {v}; + }}, + description{"Circuit bend: swing-VCA drive on the noise 'reverberation' layer (0..12; " + "toms only — congas have no noise layer). 0 is the calibrated linear model " + "(default); higher engages the swing VCA's symmetric harmonic saturation " + "riding the noise envelope."}}; attribute seed{this, "seed", 1, setter{MIN_FUNCTION{ const int v = std::max(1, static_cast(args[0])); diff --git a/source/projects/tap.vca_tilde/tap.vca_tilde.cpp b/source/projects/tap.vca_tilde/tap.vca_tilde.cpp index 8d7043e7..bc8a7fc8 100644 --- a/source/projects/tap.vca_tilde/tap.vca_tilde.cpp +++ b/source/projects/tap.vca_tilde/tap.vca_tilde.cpp @@ -54,13 +54,18 @@ class vca : public object, public vector_operator<> { vca(const atoms& args = {}) { m_vca.prepare(samplerate()); } attribute circuit{ - this, "circuit", "clean", range{"clean", "warm", "swing"}, + this, + "circuit", + "clean", + range{"clean", "warm", "swing"}, setter{ MIN_FUNCTION { int m = taptools::vca::mode_clean; - if (args[0] == "warm") + if (args[0] == "warm") { m = taptools::vca::mode_warm; - else if (args[0] == "swing") + } + else if (args[0] == "swing") { m = taptools::vca::mode_swing; + } m_vca.set_mode(m); return args; }}, @@ -77,7 +82,10 @@ class vca : public object, public vector_operator<> { "inlet. A signal in the right inlet overrides this per sample."}}; attribute drive{ - this, "drive", taptools::vca::k_default_drive, range{0.1, 12.0}, + this, + "drive", + taptools::vca::k_default_drive, + range{0.1, 12.0}, setter{ MIN_FUNCTION { const double v = std::clamp(static_cast(args[0]), 0.1, 12.0); m_vca.set_drive(v); @@ -88,7 +96,10 @@ class vca : public object, public vector_operator<> { "and more compression. Ignored by the clean circuit."}}; attribute bias{ - this, "bias", taptools::vca::k_default_bias, range{-2.0, 2.0}, + this, + "bias", + taptools::vca::k_default_bias, + range{-2.0, 2.0}, setter{ MIN_FUNCTION { const double v = std::clamp(static_cast(args[0]), -2.0, 2.0); m_vca.set_bias(v); @@ -98,8 +109,7 @@ class vca : public object, public vector_operator<> { "The asymmetry that produces even harmonics; 0 is a symmetric (odd-only) shaper. " "Ignored by the clean circuit."}}; - attribute dcblock{this, "dcblock", true, - setter{ MIN_FUNCTION { + attribute dcblock{this, "dcblock", true, setter{ MIN_FUNCTION { m_vca.set_dc_block(static_cast(args[0])); return args; }}, From 18881126a32382f692f648e3478d6bf82d7af540 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 03:27:50 +0000 Subject: [PATCH 5/5] Re-point kernel pin to the merged main tip (76dc616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01GQDkcxQY5w8AV5cJdiHMHS --- submodules/taptools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/taptools b/submodules/taptools index 8e02ac5f..76dc6166 160000 --- a/submodules/taptools +++ b/submodules/taptools @@ -1 +1 @@ -Subproject commit 8e02ac5fe25a9baf275821db4eedb6c3838a951e +Subproject commit 76dc6166a51d7b4f1a4ae2de3d18fd6229b11f62