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
36 changes: 36 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash
# Canonical Tap House SessionStart hook for Claude Code on the web.
#
# Fresh web-session containers clone the repo bare: no submodules, no
# pre-commit hook installed — so `git commit` runs unformatted and the
# clang-format CI gate fails on code a local clone would have fixed at commit
# time. This hook closes that gap at session start:
#
# 1. init submodules recursively (kernels, SDKs, vendored test harnesses)
# 2. install pre-commit and register the repo's canonical hook
# (.pre-commit-config.yaml — the Tap-wide pinned clang-format)
# 3. warm the pinned clang-format binary so the first commit doesn't
# pay the download (the container snapshot caches it)
#
# Canonical copy: taphouse (distributed by scripts/sync.sh alongside
# .clang-format / .clang-tidy / .pre-commit-config.yaml; drift-guarded where
# present). Web-only; local clones are untouched.
set -euo pipefail

if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi

cd "$CLAUDE_PROJECT_DIR"

echo "session-start: initializing submodules ..."
git submodule update --init --recursive

echo "session-start: installing pre-commit ..."
if ! python3 -m pre_commit --version >/dev/null 2>&1; then
python3 -m pip install --quiet pre-commit
fi
python3 -m pre_commit install
python3 -m pre_commit install-hooks

echo "session-start: done."
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
4 changes: 2 additions & 2 deletions .github/workflows/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ on: [push, pull_request]
jobs:
drift:
# Fails if this repo's .clang-format/.clang-tidy/STYLE.md diverge from TapHouse.
uses: tap/taphouse/.github/workflows/drift-check.yml@v4
uses: tap/taphouse/.github/workflows/drift-check.yml@v5
with:
ref: v4
ref: v5

clang-format:
runs-on: ubuntu-latest
Expand Down
21 changes: 15 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ modern package; the legacy Jamoma-era tree has been pruned. What matters here:
`submodules/taptools/include/taptools/`, in the `tap::tools` namespace, plus the kernel's own
Catch2 tests, render tools, C ABI, notebooks, and benchmarks. It is a standalone CMake project
with its own CI; develop the DSP there (or against a sibling checkout via `TAPTOOLS_KERNEL_DIR`).
See `submodules/taptools/README.md`. Most externals consume only the kernel headers, but the
spectral trio (`tap.convolve~`/`tap.nr~`/`tap.spectra~`) also link `tap::dsp` — the kernel's shared
real FFT, pinned at the kernel's own nested `submodules/dsptap` — so the root `CMakeLists.txt`
exposes that target and those three objects (and their min-tests) link it. The root also forces
See `submodules/taptools/README.md`. Most externals consume only the kernel headers, but some
also link `tap::dsp` — the shared DspTap primitives (real FFT, YIN, PSOLA, phase vocoder),
pinned at the kernel's own nested `submodules/dsptap`: the spectral trio
(`tap.convolve~`/`tap.nr~`/`tap.spectra~`) and `tap.tune~`. The root `CMakeLists.txt` exposes
that target and those objects (and their min-tests) link it. The root also forces
C++20 on every object and test target centrally (Min otherwise pins C++17).
- **`source/projects/<name>/`** — the Min-based externals (one folder per object: a `.cpp` + a
`CMakeLists.txt`), thin wrappers over the kernel headers where the DSP has been extracted.
Expand All @@ -54,7 +55,8 @@ The historical material lives on branches, not in the working tree:
with **no dependency on `min-lib`** (min-lib is the under-maintained piece; keeping DSP portable
makes the wrapper a small swappable shim). Substantial DSP belongs in a **kernel header** in the
kernel repo (`submodules/taptools/include/taptools/<name>.h`, namespace `tap::tools`, C++ stdlib
only, C++17-clean) with the wrapper `.cpp` doing only Min glue — six objects follow this today.
only) with the wrapper `.cpp` doing only Min glue — a growing set of objects follow this today
(see `REVIVAL.md` for the current list).
New/changed DSP is committed in the kernel repo, then the submodule pin is bumped here. Within
`source/projects/`, no shared global lookup tables — each object is self-contained; sharing
between kernels is allowed (and encouraged) inside the kernel repo.
Expand Down Expand Up @@ -105,7 +107,14 @@ cmake --build build --config Release

CI (`.github/workflows/build.yml`) checks out submodules recursively, builds both platforms on
every push, and **fails the macOS job if any `.mxo` is not universal** (checked with `lipo`). DSP
correctness is gated by the kernel repo's own CI, not here.
correctness is gated by the kernel repo's own CI, not here. A style workflow additionally gates
**clang-format and clang-tidy** on `source/projects/` (plus a drift check that `.clang-format` /
`.clang-tidy` / `.pre-commit-config.yaml` match the canonical taphouse copies — never hand-edit
those). Run `pre-commit install` once per clone so commits format themselves; on Claude Code web
the checked-in SessionStart hook (`.claude/hooks/session-start.sh`) initializes submodules and
installs the hook automatically. Note clang-tidy compiles with a *clang* front end: wrapper code
that GCC's mock-kernel test build accepts can still fail there (e.g. ambiguous Min `attribute`
conversions), so treat tidy as a second compiler.

## Adding / porting an object

Expand Down
40 changes: 40 additions & 0 deletions REVIVAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,46 @@ square shaper resolved via Open303's measured constants, and the author-approved
gate-hold). Remaining: **runtime validation in Max** for all three objects (help
patchers, maxtests, the render WAVs, and the notebook as the evaluation material).

**12. Net-new object — `tap.tune~` (2026-07-22).** ✅ **v1 shipped** — real-time monophonic
pitch correction (the classic retune / hard-snap effect). Cross-repo vertical slice: the YIN
detector landed in **DspTap** as the shared `tap::dsp::yin` primitive (full-rate, cumulative-mean
normalized, parabolic sub-sample lag; double golden model + float32 profile, the `fft.h`
backend pattern — promoted from the decimated follower inside `grm_pitchaccum.h`); the corrector
kernel landed in **`tap/taptools`** as `taptools/tune.h` (`tune::corrector`: scale/key mapper
with per-note enables + MIDI-target mode, retune-speed glide, and the two-tap `tap.shift~`
transposer engine with its window locked to an **even multiple of the detected period** — the
lock measurably makes the average retune ratio exact); this repo adds the Min wrapper, wrapper
tests, reference page, and help patcher. IP framing (per the pitch-correction handoff doc): the
implemented pipeline is the public-domain expired-patent design; polyphonic per-note editing
(Celemony DNA) deliberately out of scope; "tune", not the trademarked product name.
**Backends shipped (2026-07-22, same day):** resynthesis is now selectable via `@backend` —
`grain` (the validated low-latency two-tap default), `psola` (true TD-PSOLA, new DspTap
primitive `tap::dsp::psola`; formant-preserving on voice, and honestly documented as a
spectral-envelope resampler — pure tones far from a new harmonic thin out, pinned by its
tests), and `pvoc` (peak-locked phase vocoder, new DspTap primitive `tap::dsp::pvoc` on the
shared real FFT; Laroche–Dolson-style rigid peak-region shifting, exact waveform identity at
ratio 1). Detector, mapper, and retune glide are shared; only the last stage swaps, and
switching live is click-safe (the incoming engine starts from silence).
**Formant preservation shipped (2026-07-22, same day):** `@formant` enables the LPC
source-filter method on the pvoc backend (DspTap: autocorrelation LPC + Levinson–Durbin,
order 48, `envelope(target)/envelope(source)` per relocated bin — from the published
literature only, per the IP policy); psola preserves formants inherently. **Verification
notebooks shipped** in both upstream repos, driving the shipping C++ through each repo's
C ABI: DspTap `notebooks/pitchshift.ipynb` (YIN accuracy; Finding 1 — PSOLA is a
spectral-envelope resampler; Finding 2 — naive phase-vocoder remapping measurably loses
half its level vs the peak-locked design; the formant demo) and TapTools
`notebooks/tune.ipynb` (retune-speed family, the three backends compared on the same
vibrato voice, MIDI-mode formant demo; `Tune`/`Yin` added to the kernel C ABI).
**Deferred list closed (2026-07-22, same day):** auto-key detection shipped (kernel:
Krumhansl–Kessler profile scoring over a slowly-forgetting pitch-class histogram —
learning-only, `@autokey` + `getkey`/`applykey` in the wrapper); the **detected-pitch
outlet** shipped (right outlet, timer-driven `pitch <midi> <hz>` reports every `@interval`
ms with an atomic audio→scheduler handoff, plus the `key ...` replies); and the **runtime
maxtest** shipped (`runtime-tests/patchers/tap.tune~.maxtest.maxpat`, generated via
`make_maxtest.py` — unpitched DC passes the grain engine at exactly unity). Remaining for
this object: the in-Max validation pass itself (help patcher + maxtest under a licensed
Max), like the rest of the DSP set.

Remaining (ongoing, now cross-repo — DSP lands in `tap/taptools`, then bump the submodule pin
here): lift the remaining simple inline-DSP objects' math into kernel headers opportunistically as
they're touched. Control/utility and Jitter objects never move — they are Max message-logic, not
Expand Down
145 changes: 145 additions & 0 deletions docs/tap.tune~.maxref.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<?xml-stylesheet href="./_c74_ref.xsl" type="text/xsl"?>

<c74object name="tap.tune~" module="msp" category="TapTools, MSP Modifiers">

<digest>
Real-time monophonic pitch correction
</digest>

<description>
The <o>tap.tune~</o> object is the classic retune effect: it detects the pitch of a
monophonic input (voice or instrument), snaps it to the nearest allowed note, and glides
the correction in at a settable retune speed. Slow speeds correct transparently while
letting vibrato through; a speed of zero snaps instantly — the well-known hard-quantize
vocal effect. Allowed notes come from a key and scale (with individual per-note enables via
the <m>notes</m> message), or in midi mode from whatever MIDI notes are currently held
(sent as <m>note</m> messages). The detector is a YIN-family pitch tracker. Resynthesis
is selectable via <at>backend</at>: a period-locked granular transposer sharing its
engine with <o>tap.shift~</o> (the low-latency default), formant-preserving TD-PSOLA,
or a peak-locked phase vocoder. The right outlet reports the detected pitch
(<m>pitch</m> messages, every <at>interval</at> milliseconds while the input is voiced)
and answers <m>getkey</m>/<m>applykey</m> with the auto-key estimate learned when
<at>autokey</at> is on. The object is monophonic by design — pitch detection needs a
single voice. For multichannel signals, wrap it in an mc. operator.
</description>


<!--METADATA-->
<metadatalist>
<metadata name="author">74 Objects</metadata>
<metadata name="tag">TapTools</metadata>
<metadata name="tag">Audio</metadata>
<metadata name="tag">Effect</metadata>
</metadatalist>


<!--MESSAGES-->
<methodlist>
<method name="notes">
<arglist>
<arg name="input" type="list" optional="0" id="0" />
</arglist>
<description>
Set the allowed notes directly, as absolute pitch classes, replacing the key/scale
derivation. Send either twelve 0/1 toggles (C through B) or a list of the enabled
pitch classes (0 = C .. 11 = B).
</description>
</method>
<method name="note">
<arglist>
<arg name="pitch" type="int" optional="0" id="0" />
<arg name="velocity" type="int" optional="1" id="1" />
</arglist>
<description>
MIDI note-on/off for midi mode: a pitch (0-127) and a velocity. Velocity 0 releases
the note; anything else holds it as a correction target.
</description>
</method>
<method name="flush">
<arglist/>
<description>Release all held MIDI notes.</description>
</method>
<method name="getkey">
<arglist/>
<description>Report the auto-key estimate from the right outlet: 'key root
major|minor confidence', or 'key none' while there is not yet enough voiced
material. Enable learning with the autokey attribute.</description>
</method>
<method name="applykey">
<arglist/>
<description>Adopt the auto-key estimate as the key and scale attributes, then
report it from the right outlet.</description>
</method>
<method name="clear">
<arglist/>
<description>Reset the audio state (buffers and glides).</description>
</method>
</methodlist>


<!--ATTRIBUTES-->
<attributelist>
<attribute name="key" get="1" set="1" type="symbol" size="1">
<description>Key root for the scale (c, c#, d .. b). Setting it re-derives the allowed
notes from key + scale, replacing any custom notes mask.</description>
</attribute>
<attribute name="scale" get="1" set="1" type="symbol" size="1">
<description>Scale the corrector snaps to, relative to the key: chromatic, major,
minor, harmonic, melodic, pentatonic, or minorpentatonic.</description>
</attribute>
<attribute name="mode" get="1" set="1" type="symbol" size="1">
<description>Target selection: scale snaps to the key/scale notes; midi snaps to the
nearest held MIDI note and leaves the signal untouched when none are held.</description>
</attribute>
<attribute name="backend" get="1" set="1" type="symbol" size="1">
<description>Resynthesis engine: grain (low-latency two-tap default), psola
(formant-preserving TD-PSOLA — best on voice, needs harmonic-rich input), or pvoc
(peak-locked phase vocoder, one FFT frame of latency).</description>
</attribute>
<attribute name="autokey" get="1" set="1" type="int" size="1">
<description>Learn the key from the incoming pitches (Krumhansl-Kessler profile
scoring over a slowly-forgetting histogram, about a minute of memory). Learning
only — query with getkey, adopt with applykey.</description>
</attribute>
<attribute name="interval" get="1" set="1" type="float" size="1">
<description>Pitch-report period in milliseconds for the right outlet. 0 disables
reporting.</description>
</attribute>
<attribute name="formant" get="1" set="1" type="int" size="1">
<description>Formant preservation for the pvoc backend (LPC source-filter): the
correction shifts the pitch while the spectral envelope stays put. Matters for large
corrections (midi mode). The psola backend preserves formants inherently.</description>
</attribute>
<attribute name="speed" get="1" set="1" type="float" size="1">
<description>Retune speed in milliseconds — the time constant of the glide onto the
target note. 0 snaps instantly (the hard quantize effect).</description>
</attribute>
<attribute name="amount" get="1" set="1" type="float" size="1">
<description>Correction depth in percent (0-100). 100 lands on the target; 50 corrects
half the distance.</description>
</attribute>
<attribute name="minfreq" get="1" set="1" type="float" size="1">
<description>Lowest input frequency (Hz) treated as pitched.</description>
</attribute>
<attribute name="maxfreq" get="1" set="1" type="float" size="1">
<description>Highest input frequency (Hz) treated as pitched.</description>
</attribute>
<attribute name="bypass" get="1" set="1" type="int" size="1">
<description>Pass the input through unprocessed.</description>
</attribute>
<attribute name="mute" get="1" set="1" type="int" size="1">
<description>Silence the output.</description>
</attribute>
</attributelist>


<!--SEEALSO-->
<seealsolist>
<seealso name="tap.shift~"/>
<seealso name="tap.pitchaccum~"/>
<seealso name="fzero~"/>
<seealso name="retune~"/>
</seealsolist>

</c74object>
Loading
Loading