diff --git a/REVIVAL.md b/REVIVAL.md
index e6a74831..6ca212e4 100644
--- a/REVIVAL.md
+++ b/REVIVAL.md
@@ -1088,6 +1088,34 @@ GitHub Actions CI.
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.
+- ✅ **Net-new object — `tap.overdrive~` (2026-07-22).** The spiritual successor to the Jamoma-era
+ `tap.overdrive~`, deliberately *not* a port (per the overdrive handoff brief): TTOverdrive's two
+ memoryless odd-function curves — the sine shaper's hard ±1 plateau and its aliasing were the
+ "digital" tell — are dropped, replaced by a voiced feedback clipper chasing the *class* of
+ TS-lineage pedals (Mad Professor Little Green Wonder as the listening reference). Kernel:
+ **`overdrive.h`** (`tap::tools::od`) — the nonlinearity sits inside a lowpass **feedback loop**
+ solved zero-delay (the `svf.h` driven-circuit / `ladder.h` solver_fast one-pass scheme), with the
+ LF loop gain pinned so bass stays tight and nearly clean at any drive while mids take the full
+ gain — the frequency-dependent clipping a static curve cannot do, and most of the perceptual
+ distance to the reference. Even harmonics via an `asymmetry` bias (0..1), a unity clean-through so
+ the transfer never flattens, an always-on DC blocker (`R = 0.9997`, the TTDCBlock constant — this
+ time actually in the signal path, unlike TTOverdrive's vestigial one), `body` (−1..+1) sliding the
+ pre-clipper HP corner + CW upper-mid push / CCW treble lift (all linear voicing EQ, placeholders
+ pending the by-ear pass against LGW demos), 1/2/4/8× oversampling (default 4×), and normalized
+ 0..1 parameters throughout (mappable, and Q15/Q31-ready for a future fixed-point M33 target).
+ Kernel Catch2 tests pin the design goals: gain-tilt-grows-with-drive, even-harmonic emergence,
+ DC blocking, never-flat slope, alias-floor improvement with oversampling, body tilt, decay to
+ silence, determinism. Wrapper `tap.overdrive_tilde` (thin `vector_operator`), attributes
+ `drive`/`body`/`asymmetry`/`preamp`/`output`/`oversample`/`smooth`/`bypass`/`mute`, min-api
+ attribute tests, the full vertical slice (maxref + help patcher + a bypass-passthrough
+ `maxtest` starter). Same-day follow-up: the rest of the kernel-repo house treatment — a
+ `taptools_od_*` C ABI + `Overdrive` ctypes class, the **executed verification notebook**
+ (`overdrive.ipynb`: tilt +5/+16.3/+17.2 dB at drive 0/0.5/0.9; H2 −151 → −26 dB across
+ asymmetry; folded H7 −22 → −36 dB at 4×; DC means ~1e-10), and two field-guide chapters —
+ *Distortion with a memory* (new book Part VII, The pedalboard — after `tap.tune~`'s Part VI,
+ Staying in tune, landed the same week) + *The clipper in the loop: overdrive.h* (machine part,
+ now VIII). Remaining: the in-Max voicing pass (the
+ `k_voice_*` constants are the sound of the object) and runtime validation.
---
diff --git a/docs/tap.overdrive~.maxref.xml b/docs/tap.overdrive~.maxref.xml
new file mode 100644
index 00000000..af05c82e
--- /dev/null
+++ b/docs/tap.overdrive~.maxref.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+ Voiced feedback overdrive / saturation
+
+
+
+ The tap.overdrive~ object is a soft-clipping overdrive whose nonlinearity sits
+ inside a lowpass feedback loop — the topology of TS-lineage feedback pedals (voiced
+ against the Mad Professor Little Green Wonder) — rather than a static waveshaping curve
+ applied to the whole spectrum. The result is frequency-dependent clipping: bass stays
+ tight and nearly clean at any drive while mids and highs take the full gain and break up
+ first, and the tilt grows as the drive rises. The asymmetry attribute biases the clipper
+ for even-harmonic warmth (a built-in DC blocker removes the offset this creates), and a
+ unity clean path is summed around the clipper so the transfer keeps rising with reduced
+ slope instead of flattening into a hard digital plateau. The body attribute is the
+ signature voicing control: toward -1 fuller lows reach the clipper and the top gets a
+ slight lift; toward +1 the lows tighten and thin and the upper mids push forward. The
+ clipping stage runs oversampled (default 4x) to keep aliasing down, and all parameters
+ glide smoothly. This object is the spiritual successor to the original Jamoma-era
+ tap.overdrive~, replacing its memoryless waveshaper modes. Single-channel: wrap in mc.
+ for multichannel.
+
+
+
+
+
+ 74 Objects
+ TapTools
+ Audio
+ Distortion
+
+
+
+
+
+
+
+ Reset the filter, feedback, and resampler state.
+
+
+
+
+
+
+
+ Drive (0-1). 0 is edge-of-breakup — a pedal's gain knob full
+ counterclockwise, still warm — and 1 is heavily saturated. The sweep is perceptual
+ (dB of gain into the clipper); use bypass for a true clean signal.
+
+
+ Body voicing (-1..+1, 0 neutral). Toward -1: fuller lows into the
+ clipper and a slight treble lift. Toward +1: thinner, tighter lows and an upper-mid
+ push. Linear pre/post voicing around the clipper, not part of the
+ nonlinearity.
+
+
+ Asymmetry (0-1): even-harmonic amount. 0 is a symmetric clip (odd
+ harmonics only); raising it biases the clipper for even-order warmth. The DC this
+ generates is removed by the built-in DC blocker.
+
+
+ Input gain in dB (-24..24), applied before the clipper.
+
+
+ Makeup gain in dB (-24..24), applied at the output.
+
+
+ Internal oversampling factor for the clipping stage (1, 2, 4, or 8;
+ default 4). Reduces aliasing from the clipper's harmonics.
+
+
+ Anti-zipper interpolation time in milliseconds applied to parameter changes.
+
+
+ Pass the input through unprocessed.
+
+
+ Silence the output.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/help/tap.overdrive~.maxhelp b/help/tap.overdrive~.maxhelp
new file mode 100644
index 00000000..d93bd3d9
--- /dev/null
+++ b/help/tap.overdrive~.maxhelp
@@ -0,0 +1,311 @@
+{
+ "patcher" : {
+ "fileversion" : 1,
+ "appversion" : {
+ "major" : 8,
+ "minor" : 5,
+ "revision" : 0,
+ "architecture" : "x64",
+ "modernui" : 1
+ }
+,
+ "classnamespace" : "box",
+ "rect" : [ 84.0, 104.0, 700.0, 560.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, 480.0, 23.0 ],
+ "text" : "tap.overdrive~ : voiced feedback overdrive / saturation"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-2",
+ "linecount" : 4,
+ "maxclass" : "comment",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 20.0, 38.0, 660.0, 62.0 ],
+ "text" : "The clipper sits inside a lowpass feedback loop, so bass stays tight and nearly clean while mids and highs break up first — try a low note and raise the drive. asymmetry adds even-harmonic warmth (watch the spectroscope). body voices the pedal: toward -1 fuller lows and a slight treble lift, toward +1 tighter lows and an upper-mid push. The clipping stage runs oversampled (default 4x) with an always-on DC blocker."
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-3",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "signal" ],
+ "patching_rect" : [ 20.0, 120.0, 74.0, 22.0 ],
+ "text" : "cycle~ 110"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-4",
+ "maxclass" : "comment",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 100.0, 123.0, 220.0, 20.0 ],
+ "text" : "a low note shows the bass staying tight"
+ }
+
+ }
+, {
+ "box" : {
+ "attr" : "drive",
+ "id" : "obj-5",
+ "maxclass" : "attrui",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 240.0, 155.0, 200.0, 22.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "attr" : "body",
+ "id" : "obj-6",
+ "maxclass" : "attrui",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 240.0, 183.0, 200.0, 22.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "attr" : "asymmetry",
+ "id" : "obj-7",
+ "maxclass" : "attrui",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 240.0, 211.0, 200.0, 22.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "attr" : "oversample",
+ "id" : "obj-8",
+ "maxclass" : "attrui",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 240.0, 239.0, 200.0, 22.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "attr" : "output",
+ "id" : "obj-9",
+ "maxclass" : "attrui",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 240.0, 267.0, 200.0, 22.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-10",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "signal" ],
+ "patching_rect" : [ 20.0, 310.0, 96.0, 22.0 ],
+ "text" : "tap.overdrive~"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-11",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 130.0, 275.0, 42.0, 22.0 ],
+ "text" : "clear"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-12",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "signal" ],
+ "patching_rect" : [ 20.0, 355.0, 74.0, 22.0 ],
+ "text" : "*~ 0.5"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-13",
+ "maxclass" : "newobj",
+ "numinlets" : 2,
+ "numoutlets" : 0,
+ "patching_rect" : [ 20.0, 415.0, 130.0, 22.0 ],
+ "text" : "ezdac~"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-14",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 250.0, 330.0, 180.0, 100.0 ],
+ "text" : "scope~"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-15",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 450.0, 330.0, 220.0, 100.0 ],
+ "text" : "spectroscope~"
+ }
+
+ }
+ ],
+ "lines" : [ {
+ "patchline" : {
+ "destination" : [ "obj-10", 0 ],
+ "source" : [ "obj-3", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-10", 0 ],
+ "source" : [ "obj-5", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-10", 0 ],
+ "source" : [ "obj-6", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-10", 0 ],
+ "source" : [ "obj-7", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-10", 0 ],
+ "source" : [ "obj-8", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-10", 0 ],
+ "source" : [ "obj-9", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-10", 0 ],
+ "source" : [ "obj-11", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-12", 0 ],
+ "source" : [ "obj-10", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-14", 0 ],
+ "source" : [ "obj-10", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-15", 0 ],
+ "source" : [ "obj-10", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-13", 0 ],
+ "source" : [ "obj-12", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-13", 1 ],
+ "source" : [ "obj-12", 0 ]
+ }
+
+ }
+ ],
+ "dependency_cache" : [ {
+ "name" : "tap.overdrive~.mxo",
+ "type" : "iLaX"
+ }
+ ],
+ "autosave" : 0
+ }
+
+}
diff --git a/runtime-tests/make_maxtest.py b/runtime-tests/make_maxtest.py
index 6301d69e..c7dfc3be 100644
--- a/runtime-tests/make_maxtest.py
+++ b/runtime-tests/make_maxtest.py
@@ -202,3 +202,14 @@ def control_test(filename, object_text, trigger, expected, assert_name,
description="tap.tune~: unpitched DC passes the grain engine at unity (no correction applies).",
numoutlets=2,
)
+ # tap.overdrive~: bypass is a pure passthrough regardless of the drive settings —
+ # pins that the external loads and the signal path is wired (the DSP itself is
+ # covered by the kernel repo's overdrive_test.cpp).
+ audio_test(
+ "tap.overdrive~.maxtest.maxpat",
+ object_text="tap.overdrive~ @bypass 1 @drive 1.",
+ input_value="0.5",
+ expected="0.5",
+ assert_name="tap.overdrive~-bypass-passthrough",
+ description="tap.overdrive~ @bypass 1: sig~ 0.5 passes unprocessed.",
+ )
diff --git a/runtime-tests/patchers/tap.overdrive~.maxtest.maxpat b/runtime-tests/patchers/tap.overdrive~.maxtest.maxpat
new file mode 100644
index 00000000..f38f050f
--- /dev/null
+++ b/runtime-tests/patchers/tap.overdrive~.maxtest.maxpat
@@ -0,0 +1,371 @@
+{
+ "patcher": {
+ "fileversion": 1,
+ "appversion": {
+ "major": 7,
+ "minor": 0,
+ "revision": 4,
+ "architecture": "x64",
+ "modernui": 1
+ },
+ "rect": [
+ 66.0,
+ 79.0,
+ 1070.0,
+ 480.0
+ ],
+ "bglocked": 0,
+ "openinpresentation": 0,
+ "default_fontsize": 12.0,
+ "default_fontface": 0,
+ "default_fontname": "Helvetica Neue Light",
+ "gridonopen": 1,
+ "gridsize": [
+ 5.0,
+ 5.0
+ ],
+ "gridsnaponopen": 1,
+ "statusbarvisible": 2,
+ "toolbarvisible": 1,
+ "description": "tap.overdrive~ @bypass 1: sig~ 0.5 passes unprocessed.",
+ "boxes": [
+ {
+ "box": {
+ "id": "obj-1",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 40,
+ 40,
+ 70,
+ 22.0
+ ],
+ "text": "loadbang"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-2",
+ "maxclass": "button",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "bang"
+ ],
+ "patching_rect": [
+ 40,
+ 90,
+ 24,
+ 24
+ ]
+ }
+ },
+ {
+ "box": {
+ "id": "obj-3",
+ "maxclass": "message",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 40,
+ 140,
+ 30,
+ 22.0
+ ],
+ "text": "1"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-4",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 0,
+ "outlettype": [],
+ "patching_rect": [
+ 40,
+ 190,
+ 45,
+ 22.0
+ ],
+ "text": "dac~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-5",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 120,
+ 140,
+ 110,
+ 22.0
+ ],
+ "text": "test.terminate"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-6",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 300,
+ 40,
+ 110,
+ 22.0
+ ],
+ "text": "sig~ 0.5"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-7",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 300,
+ 100,
+ 160,
+ 22.0
+ ],
+ "text": "tap.overdrive~ @bypass 1 @drive 1."
+ }
+ },
+ {
+ "box": {
+ "id": "obj-8",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 300,
+ 160,
+ 110,
+ 22.0
+ ],
+ "text": "round~ 1e-06"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-9",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300,
+ 220,
+ 90,
+ 22.0
+ ],
+ "text": "test.sample~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-10",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300,
+ 280,
+ 150,
+ 22.0
+ ],
+ "text": "test.equals 0.5"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-11",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300,
+ 340,
+ 180,
+ 22.0
+ ],
+ "text": "test.assert tap.overdrive~-bypass-passthrough"
+ }
+ }
+ ],
+ "lines": [
+ {
+ "patchline": {
+ "source": [
+ "obj-1",
+ 0
+ ],
+ "destination": [
+ "obj-2",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-2",
+ 0
+ ],
+ "destination": [
+ "obj-3",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-3",
+ 0
+ ],
+ "destination": [
+ "obj-4",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-2",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-6",
+ 0
+ ],
+ "destination": [
+ "obj-7",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-7",
+ 0
+ ],
+ "destination": [
+ "obj-8",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-8",
+ 0
+ ],
+ "destination": [
+ "obj-9",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-9",
+ 0
+ ],
+ "destination": [
+ "obj-2",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-9",
+ 0
+ ],
+ "destination": [
+ "obj-10",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-10",
+ 0
+ ],
+ "destination": [
+ "obj-11",
+ 0
+ ],
+ "hidden": 0,
+ "disabled": 0
+ }
+ }
+ ]
+ }
+}
diff --git a/source/projects/tap.overdrive_tilde/CMakeLists.txt b/source/projects/tap.overdrive_tilde/CMakeLists.txt
new file mode 100644
index 00000000..63dc6a88
--- /dev/null
+++ b/source/projects/tap.overdrive_tilde/CMakeLists.txt
@@ -0,0 +1,30 @@
+# 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/overdrive.h
+)
+
+add_library(
+ ${PROJECT_NAME}
+ MODULE
+ ${SOURCES}
+)
+
+include(${C74_MIN_API_DIR}/script/min-posttarget.cmake)
+
+# 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.overdrive_tilde/tap.overdrive_tilde.cpp b/source/projects/tap.overdrive_tilde/tap.overdrive_tilde.cpp
new file mode 100644
index 00000000..f4152c31
--- /dev/null
+++ b/source/projects/tap.overdrive_tilde/tap.overdrive_tilde.cpp
@@ -0,0 +1,134 @@
+/// @file
+/// @brief tap.overdrive~ — voiced feedback overdrive / saturation.
+/// @details A thin Min wrapper around the portable kernel in overdrive.h (all DSP lives there —
+/// see its header comment for the model). The spiritual successor to the old Jamoma
+/// TTOverdrive-based tap.overdrive~, not a port: instead of a memoryless waveshaping
+/// curve applied to the full spectrum, the clipper sits inside a lowpass feedback loop
+/// (solved zero-delay), so low frequencies stay tight and nearly clean while mids and
+/// highs take the full drive and break up first — the frequency-dependent behavior of
+/// TS-lineage feedback pedals (voiced against the Mad Professor Little Green Wonder).
+/// An asymmetry control adds even-harmonic warmth, a unity clean path keeps the
+/// transfer from ever flattening, the body control voices the lows/mids tilt, and the
+/// clipping stage runs oversampled with an always-on DC blocker behind it.
+/// Single-channel: wrap in mc. for multichannel (the kernel itself is N-channel for
+/// reuse outside Max).
+/// @author Timothy Place
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright 2004-2026 Timothy Place.
+
+#include
+
+#include "c74_min.h"
+
+using namespace c74::min;
+namespace kernel = tap::tools::od;
+
+class overdrive : public object, public vector_operator<> {
+ private:
+ // Constructed before the attributes below so attribute defaults can forward into it.
+ kernel::overdrive m_od;
+
+ public:
+ MIN_DESCRIPTION{"Voiced feedback overdrive. The clipper sits inside a lowpass feedback loop, "
+ "so bass stays tight and nearly clean at any drive while mids and highs break "
+ "up first — the frequency-dependent gain of a TS-lineage feedback pedal, "
+ "which a static waveshaper cannot do. Asymmetry adds even-harmonic warmth, a "
+ "unity clean path keeps the transfer from ever flattening into a hard "
+ "plateau, and the body control voices the low/mid balance (CCW fuller lows "
+ "with a slight treble lift, CW tighter lows with an upper-mid push). The "
+ "clipping stage runs oversampled (default 4x) with an always-on DC blocker "
+ "behind it. Single-channel: wrap in mc. for multichannel."};
+ MIN_TAGS{"audio, distortion"};
+ MIN_AUTHOR{"Timothy Place"};
+ MIN_RELATED{"overdrive~, tap.vca~, tap.svf~, tap.dcblock~"};
+
+ inlet<> m_in{this, "(signal) audio input"};
+ outlet<> m_out{this, "(signal) overdriven output", "signal"};
+
+ overdrive(const atoms& args = {}) { m_od.prepare(samplerate(), 1); }
+
+#define TAP_OD_ATTR(NAME, INDEX, DEFAULT, DESC) \
+ attribute NAME{this, #NAME, DEFAULT, \
+ setter{MIN_FUNCTION{const double v = kernel::clamp_param(INDEX, args[0]); \
+ m_od.set_param(INDEX, v); \
+ return {v}; \
+ } \
+ } \
+ , description { DESC } \
+ } \
+ ;
+
+ TAP_OD_ATTR(drive, kernel::p_drive, 0.35,
+ "Drive (0..1). 0 is edge-of-breakup — a pedal's gain knob full counterclockwise, still "
+ "warm — and 1 is heavily saturated. The sweep is perceptual (dB of gain into the "
+ "clipper); use bypass for a true clean signal.")
+ TAP_OD_ATTR(body, kernel::p_body, 0.0,
+ "Body voicing (-1..+1, 0 neutral). Toward -1: fuller lows into the clipper and a "
+ "slight treble lift. Toward +1: thinner, tighter lows and an upper-mid push. This is "
+ "linear pre/post voicing around the clipper, not part of the nonlinearity.")
+ TAP_OD_ATTR(asymmetry, kernel::p_asymmetry, 0.15,
+ "Asymmetry (0..1): even-harmonic amount. 0 is a symmetric clip (odd harmonics only); "
+ "raising it biases the clipper for even-order warmth. The DC this generates is removed "
+ "by the built-in DC blocker.")
+ TAP_OD_ATTR(preamp, kernel::p_preamp, 0.0, "Input gain in dB (-24..24), applied before the clipper.")
+ TAP_OD_ATTR(output, kernel::p_output, 0.0, "Makeup gain in dB (-24..24), applied at the output.")
+
+#undef TAP_OD_ATTR
+
+ attribute oversample{this, "oversample", 4, setter{ MIN_FUNCTION {
+ const int req = static_cast(args[0]);
+ const int v = (req >= 8) ? 8 : (req >= 4) ? 4 : (req >= 2) ? 2 : 1;
+ m_od.set_oversample(v);
+ return {v};
+ }},
+ description{"Internal oversampling factor for the clipping stage (1, 2, 4, or 8; "
+ "default 4). Reduces aliasing from the clipper's harmonics."}};
+
+ attribute smooth{this, "smooth", kernel::k_default_smooth_ms, setter{ MIN_FUNCTION {
+ const double v = std::max(0.0, static_cast(args[0]));
+ m_od.set_smooth_ms(v);
+ return {v};
+ }},
+ description{"Anti-zipper interpolation time in ms applied to parameter changes."}};
+
+ attribute bypass{this, "bypass", false, description{"Pass the input through unprocessed."}};
+
+ attribute mute{this, "mute", false, description{"Silence the output."}};
+
+ // -- messages -------------------------------------------------------------------------------
+
+ message<> clear{this, "clear", "Reset the filter, feedback, and resampler state.",
+ MIN_FUNCTION {
+ m_od.clear();
+ return {};
+ }};
+
+ message<> dspsetup{this, "dspsetup", "Reconfigure for the sample rate when DSP starts.",
+ MIN_FUNCTION {
+ m_od.prepare(samplerate(), 1);
+ 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 {
+ m_od.process(in, out, static_cast(n));
+ }
+ }
+
+ // Exposed for unit tests.
+ kernel::overdrive& od() { return m_od; }
+};
+
+MIN_EXTERNAL(overdrive);
diff --git a/source/projects/tap.overdrive_tilde/tap.overdrive_tilde_test.cpp b/source/projects/tap.overdrive_tilde/tap.overdrive_tilde_test.cpp
new file mode 100644
index 00000000..9d2c30f8
--- /dev/null
+++ b/source/projects/tap.overdrive_tilde/tap.overdrive_tilde_test.cpp
@@ -0,0 +1,77 @@
+/// @file
+/// @brief Unit tests for tap.overdrive~ (Min-level behavior; DSP correctness lives in the
+/// kernel repo's overdrive_test.cpp).
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright 2004-2026 Timothy Place.
+
+#include "c74_min_unittest.h" // required unit-test header (defines main via Catch)
+#include "tap.overdrive_tilde.cpp" // include the object source so we can instantiate it
+
+SCENARIO("tap.overdrive~ instantiates with the expected defaults") {
+ ext_main(nullptr); // configure the class (required once per test executable)
+
+ GIVEN("a default instance") {
+ test_wrapper an_instance;
+ overdrive& my_object = an_instance;
+
+ THEN("the attributes have the documented defaults") {
+ REQUIRE(static_cast(my_object.drive) == 0.35);
+ REQUIRE(static_cast(my_object.body) == 0.0);
+ REQUIRE(static_cast(my_object.asymmetry) == 0.15);
+ REQUIRE(static_cast(my_object.preamp) == 0.0);
+ REQUIRE(static_cast(my_object.output) == 0.0);
+ REQUIRE(static_cast(my_object.oversample) == 4);
+ REQUIRE(static_cast(my_object.smooth) == tap::tools::od::k_default_smooth_ms);
+ REQUIRE(static_cast(my_object.bypass) == false);
+ REQUIRE(static_cast(my_object.mute) == false);
+ }
+ }
+}
+
+SCENARIO("tap.overdrive~ clamps attribute values to their documented ranges") {
+ ext_main(nullptr);
+
+ GIVEN("a default instance") {
+ test_wrapper an_instance;
+ overdrive& my_object = an_instance;
+
+ WHEN("normalized attributes are set out of range") {
+ my_object.drive = 2.0;
+ my_object.body = -5.0;
+ my_object.asymmetry = 1.5;
+
+ THEN("they clamp to their ranges") {
+ REQUIRE(static_cast(my_object.drive) == 1.0);
+ REQUIRE(static_cast(my_object.body) == -1.0);
+ REQUIRE(static_cast(my_object.asymmetry) == 1.0);
+ }
+ }
+
+ WHEN("the gain attributes are set out of range") {
+ my_object.preamp = 100.0;
+ my_object.output = -100.0;
+
+ THEN("they clamp to +-24 dB") {
+ REQUIRE(static_cast(my_object.preamp) == tap::tools::od::k_gain_range_db);
+ REQUIRE(static_cast(my_object.output) == -tap::tools::od::k_gain_range_db);
+ }
+ }
+
+ WHEN("the oversample factor is set to a non-power-of-two") {
+ my_object.oversample = 3;
+
+ THEN("it snaps down to a legal factor") {
+ REQUIRE(static_cast(my_object.oversample) == 2);
+ REQUIRE(my_object.od().oversample() == 2);
+ }
+ }
+
+ WHEN("the smooth time is set negative") {
+ my_object.smooth = -5.0;
+
+ THEN("it clamps to zero") {
+ REQUIRE(static_cast(my_object.smooth) == 0.0);
+ }
+ }
+ }
+}
diff --git a/submodules/taptools b/submodules/taptools
index d5d3dd4c..492cfe90 160000
--- a/submodules/taptools
+++ b/submodules/taptools
@@ -1 +1 @@
-Subproject commit d5d3dd4ccd74e9736ffb7851c52f2c1687af9e63
+Subproject commit 492cfe90a40f3e9b1067780c19ecb3a64b27a4be