Skip to content

tune.h — the pitch-correction kernel (backends, formant, auto-key), notebook, and book chapters#20

Merged
tap merged 7 commits into
mainfrom
claude/auto-tune-discussion-w8ky0p
Jul 22, 2026
Merged

tune.h — the pitch-correction kernel (backends, formant, auto-key), notebook, and book chapters#20
tap merged 7 commits into
mainfrom
claude/auto-tune-discussion-w8ky0p

Conversation

@tap

@tap tap commented Jul 22, 2026

Copy link
Copy Markdown
Owner

The portable kernel behind tap.tune~: classic real-time monophonic pitch correction (the public-domain expired-patent pipeline), composed from the new DspTap primitives (tap/DspTap#3).

include/taptools/tune.htune::corrector:

  • YIN detection (shared DspTap primitive) every ~5 ms on worst-case geometry fixed at prepare(), so every setter is allocation-free and real-time safe.
  • Scale/key targeting with per-note enables, MIDI-target mode (nearest held note; none held = no correction), retune-speed glide (exponential time constant, 0 = hard snap), correction amount, detection range/threshold.
  • Three selectable resynthesis backends behind one seam: the period-locked two-tap grain engine (default, lowest latency), TD-PSOLA (formant-preserving on voice), and the peak-locked phase vocoder (with the LPC set_formant option). Switching live is allocation-free and click-safe.
  • The period-lock finding: a fixed-ms grain-window clamp biased the output pitch ~5 cents; locking the window to an even multiple of the detected period (taps an integer number of periods apart) makes the average retune ratio exact. Caught only because the tests measure the output's pitch with the DspTap detector as an independent oracle.
  • Auto-key detection: a leaky pitch-class histogram (~60 s memory) scored against the published Krumhansl–Kessler profiles over all 24 keys. Advisory by design — autokey_estimate() reports, autokey_apply() adopts; nothing re-aims targets on its own. D-major scale reads as D major at 0.95 confidence.

Also included:

  • Kernel test scenarios (144→145 cases total pass) driving the full chain with the oracle-based strategy; sawtooth material wherever PSOLA participates, per its documented contract.
  • Tune + Yin in the C ABI and taptools_py; notebooks/tune.ipynb (committed executed): the retune-speed family, all three backends landing 220.00 Hz on the same vibrato voice, and the MIDI-mode formant demo.
  • Three book chapters for Tools on Tap: "The note you meant" (new Part VI), and the machine appendices "Three ways to move a pitch: yin.h, psola.h, pvoc.h" and "The nearest allowed note: tune.h" — every measured claim carried by an executed notebook cell or pinned test. The drafting plan remains in book/PLAN-pitch-chapters.md.
  • submodules/dsptap pinned to the Pitch primitives: yin, psola, pvoc (+ LPC formant preservation, C ABI, notebook) DspTap#3 head.

Merge order: after tap/DspTap#3; before the TapTools-Max PR (which pins this branch).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs


Generated by Claude Code

tap and others added 6 commits July 22, 2026 02:16
Classic real-time pitch correction (the public-domain 1998 pipeline):
the shared DspTap YIN detector feeds a scale/key mapper and a two-tap
delay-line transposer — the tap.shift~ / pitchaccum engine (Hermite
taps, complementary sin^2/cos^2 envelopes) with its grain window locked
to an even multiple of the detected period. Period-locking keeps the
taps an integer number of periods apart, which measurement showed is
what makes the average retune ratio exact (a fixed-ms window clamp
biased the pitch ~5 cents); the even-multiple rule preserves the lock
when two periods would fall under the minimum window.

Controls: key + 12-bit scale mask (presets provided) with per-note
enables, MIDI-target mode (snaps to the nearest held note), retune
speed as an exponential time constant (0 = hard snap), correction
amount, detection range/threshold. Unpitched input relaxes to no
correction. Allocation-free after prepare(); setters safe while audio
runs.

Bumps submodules/dsptap to pick up tap::dsp::yin.

Tests drive the full chain with synthesized tones and measure output
pitch using the DspTap detector as oracle: chromatic snap, scale mask
steering, per-note enables, MIDI targeting, retune-speed glide,
amount scaling, unpitched relaxation, amplitude sanity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
The corrector's last stage is now backend-selectable behind one
interface (enum backend): the existing period-locked two-tap grain
engine stays the validated low-latency default; tap::dsp::psola adds
true TD-PSOLA (formant-preserving on voice-like material, latency ~2x
the deepest detectable period); tap::dsp::pvoc adds the peak-locked
phase vocoder (one-frame latency). Detector, scale/MIDI mapper, and
retune glide are shared — only the resynthesis swaps. Both alternate
backends are constructed at prepare(), so set_backend() is
allocation-free and safe while audio runs (the incoming backend starts
from cleared state).

The detected period now rides its own slew for the PSOLA backend's
per-sample period input.

Bumps submodules/dsptap to pick up psola.h and pvoc.h.

Tests: every backend snaps a 46-cent-sharp sawtooth onto the target
within 6 cents at healthy level (saw, not sine — PSOLA resamples the
spectral envelope and needs harmonics, per its header contract), and
live backend switching stays finite while continuing to correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
set_formant() wires DspTap's new LPC formant preservation into the
corrector's pvoc backend (the psola backend is inherently
formant-preserving and the grain engine waveform-preserving, so the
flag only changes the pvoc path). Survives being set before prepare().
Kernel test drives the pvoc backend with formant on and confirms the
correction still lands.

The C ABI gains tap.tune~'s full corrector (Tune: key/scale/notes,
mode, backend, speed, amount, range, threshold, formant, MIDI notes,
meters, buffer processing) plus a passthrough of the shared DspTap
detector (Yin) so the notebooks track pitch with the same detector the
corrector uses.

notebooks/tune.ipynb (committed executed) measures the shipping
kernel: the retune-speed family (hard snap to transparent) as pitch
tracks, the three backends on the same vibrato voice (all land the
correction; character and latency differ), and a 5.5-semitone
MIDI-mode correction with and without formant preservation. The
primitive-level findings live in DspTap's notebooks/pitchshift.ipynb.

Bumps submodules/dsptap to the formant-capable pvoc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
An opt-in key learner on the corrector: every voiced analysis folds its
pitch class into a slowly-forgetting histogram (~60 s memory, leak
applied per analysis), scored on demand against the published
Krumhansl-Kessler major/minor profiles (Krumhansl 1990) by Pearson
correlation over all 24 keys. Deliberately learning-only: the estimate
never flips the corrector's targets on its own — autokey_apply() adopts
it as key + scale when asked, and a mass guard (~0.5 s of voiced
material) withholds estimates from silence. autokey_reset() forgets.

Tests: a tonic-weighted D major scale reads as D major (confidence
0.95), an A harmonic-minor melody as A minor, the fresh corrector
offers nothing, reset withdraws the estimate, apply sets key + scale.

C ABI + taptools_py grow the matching autokey surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
Outlines for three chapters covering the tap.tune~ work: the
user-facing 'The note you meant' (new Part VI — Staying in tune), and
two machine appendices — 'Three ways to move a pitch: yin.h, psola.h,
pvoc.h' (the two findings, told as they happened, plus the LPC formant
section) and 'The nearest allowed note: tune.h' (the period-lock bug
hunt as centerpiece, the backend seam, the auto-key learner). Each
section lists the executed notebook cell or pinned test that already
carries its measured claims, so drafting needs no new lab work.
SUMMARY placement included; the plan file itself stays out of src/ so
nothing half-written publishes into the built book.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
Three chapters from the drafted plan. 'The note you meant' (new Part VI
— Staying in tune): the user-facing tap.tune~ chapter — retune speed as
the instrument, targeting (key/scale, per-note toggles, MIDI mode), the
three-backend trade table, formant, the advisory auto-key learner, the
pitch outlet, recipes, and the honest when-not list, with the history
paragraph handled plainly (expired patent, live trademark, polyphonic
fence). 'Three ways to move a pitch: yin.h, psola.h, pvoc.h' (machine):
the DspTap primitives as three claims about what a pitched sound is,
with both development findings told as they happened — PSOLA's
envelope-resampling double face and the naive phase vocoder's measured
level collapse, including the frame-relative-modulator phase bug — plus
the LPC formant trade. 'The nearest allowed note: tune.h' (machine):
the composition appendix with the period-lock bug hunt as centerpiece,
the target/glide policies, the backend seam, the key learner's
advisory-only design, and the ledger.

Every measured number is carried by an executed notebook cell or a
pinned test, per the book's standing promise. SUMMARY gains Part VI and
the two machine entries (machine part becomes Part VII); the plan file
stays as the drafting record, marked drafted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
tap/DspTap#3 merged (rebase); the pin moves from the side-branch SHA to
the identical tree on main (b3e9ee5) so it stays reachable after branch
cleanup. Full kernel suite re-run against the merged pin: 145 cases
pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
@tap
tap merged commit d5d3dd4 into main Jul 22, 2026
12 checks passed
tap added a commit to tap/TapTools-Max that referenced this pull request Jul 22, 2026
tap/TapTools#20 merged (rebase); the pin moves from the side-branch SHA
to the identical tree on main (d5d3dd4) so it stays reachable after
branch cleanup. No code change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
tap added a commit to tap/TapTools-Max that referenced this pull request Jul 22, 2026
tap/TapTools#20 merged (rebase); the pin moves from the side-branch SHA
to the identical tree on main (d5d3dd4) so it stays reachable after
branch cleanup. No code change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuUawafae7RR2ySWabuJcs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant