diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..b7c2a49 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -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." diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e06b033 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index ee11eaa..3006abb 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -8,9 +8,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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0e60d41 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +**TapTools** (this repo, `tap/taptools`) — the portable DSP kernel library behind the +[TapTools-Max](https://github.com/tap/TapTools-Max) package of Max/MSP externals: one +self-contained header per object under `include/taptools/`, namespace `tap::tools`, plain C++20 +with the standard library only — no Max, no Min, no frameworks. The AmbiTap / AmbiTap-Max split: +DSP lives and is tested *here*; the Max repo is thin wrappers plus a submodule pin of this repo. +Shared low-level primitives (real FFT, YIN, PSOLA, phase vocoder) live one level further down in +the [DspTap](https://github.com/tap/dsptap) submodule (`submodules/dsptap`, target `tap::dsp`) — +general-purpose signal primitives go there; musical/object-level kernels stay here. + +## Layout + +- `include/taptools/` — one header per kernel (e.g. `tune.h`, `svf.h`, `grm_pitchaccum.h`); + `stft.h` is the shared overlap-add scaffold for the spectral kernels. +- `submodules/dsptap/` — `tap::dsp` (fft/yin/psola/pvoc + vendored Ooura/CMSIS), pinned. +- `tests/` — Catch2 (FetchContent), pure C++, no Max/min-api/mock kernel; runs via `ctest`. +- `tools/capi/` + `notebooks/` — the verification layer: a C ABI over the same kernel headers the + externals compile, a ctypes bridge (`notebooks/taptools_py.py`), and *executed* verification + notebooks. `tools/` also holds render tools (e.g. `tr808_render`). +- `book/` — *Tools on Tap* (mdbook), the field guide; `bench/` — benchmarks. + +## Kernel conventions (load-bearing) + +- **Shape:** a class in `tap::tools::` with `prepare(sr)` (all allocation happens here), + per-sample `process(...)`, and setters that are allocation-free and safe while audio runs. + Worst-case geometry is bought at `prepare()` so runtime parameter changes never reallocate. +- **No zippers:** parameters ride per-sample linear ramps or one-pole slews (see `grm_comb.h` / + `grm_pitchaccum.h` for the param_index + ramp house pattern). +- **Provenance is documented.** Faithful ports keep magic constants and cite where they came from; + recreations cite papers/schematics. New DSP is implemented from published literature only — + an IP policy, not a citation habit. +- **Honest limits in the header.** Known failure modes and material contracts (e.g. PSOLA wants + harmonic input) are stated where the next reader will look. + +## Style + +`STYLE.md` + `.clang-format` + `.clang-tidy` are the canonical Tap House copies (synced from +taphouse; CI drift-checks them — never hand-edit). Run `pre-commit install` once per clone; on +Claude Code web the checked-in SessionStart hook does it automatically. clang-tidy compiles with a +clang front end — treat it as a second compiler, not a linter. + +## Build & test + +```sh +git submodule update --init --recursive +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +ctest --test-dir build --output-on-failure +``` + +Tests are Catch2 `SCENARIO`s named for the promise they pin. House patterns worth preserving: +**oracle-based measurement** (drive the kernel with synthesized material and measure the *output* +with an independently certified detector — `tap::dsp::yin` — rather than asserting internals; this +is how the tune kernel's 5-cent period-lock bias was caught), and **material contracts** (test +PSOLA-family code with sawtooth, not sine, per its documented contract). + +## Verification layer + +The notebooks drive the shipping C++ through `tools/capi` via ctypes — never Python +re-implementations. They are committed executed; re-execute when behavior changes. When adding a +kernel that notebooks (or other bindings) should reach, extend `tools/capi/taptools_capi.{h,cpp}` +and `notebooks/taptools_py.py` alongside it. + +## The book + +`book/` is *Tools on Tap* (mdbook, `src/SUMMARY.md`, `create-missing = false` so SUMMARY entries +must exist). One user-facing chapter per object family plus "The machine, file by file" +appendices. The standing promise: **every performance claim is measured, not remembered** — cite +the executed notebook cell or pinned test that carries each number. Chapter plans live as +`book/PLAN-*.md` drafting records and stay after the chapters ship. + +## Release flow + +DSP changes land here (or in dsptap, then bump `submodules/dsptap` here); TapTools-Max then bumps +its `submodules/taptools` pin. After a PR merges via rebase/squash, repoint any open consumer pins +at the identical tree on `main` so they stay reachable after branch cleanup. diff --git a/scripts/tidy.sh b/scripts/tidy.sh new file mode 100755 index 0000000..45fc009 --- /dev/null +++ b/scripts/tidy.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Local mirror of the CI clang-tidy gate (.github/workflows/style.yml): the +# TapHouse .clang-tidy naming + mandatory-braces checks over this project's +# own translation units. Run it before pushing. +# +# WHY THIS EXISTS (and is not a pre-commit hook): the pre-commit hook only +# runs clang-FORMAT, which is cheap and context-free. clang-TIDY needs a +# compile database (a configured CMake build) and compiles every TU to reason +# about types — too slow and stateful for a per-commit hook, so CI keeps it as +# its own gate. This script is the fast local equivalent of that gate. +# +# Usage: +# scripts/tidy.sh # sweep every project TU (full CI mirror) +# scripts/tidy.sh tests/test_foo.cpp … # only the given TU(s) — fast, for a change +# +# Env: +# CLANG_TIDY=clang-tidy-18 # binary to use (default: clang-tidy-18, then clang-tidy) +# TIDY_BUILD=build-tidy # compile-database build dir (kept separate from ./build) +# TIDY_RECONFIGURE=1 # force a cmake re-configure of the compile database +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +# Prefer clang-tidy-18 — the version the CI gate installs and the .clang-tidy +# checks are validated against; a different major version may disagree. +tidy="${CLANG_TIDY:-}" +if [ -z "$tidy" ]; then + if command -v clang-tidy-18 >/dev/null 2>&1; then + tidy=clang-tidy-18 + elif command -v clang-tidy >/dev/null 2>&1; then + tidy=clang-tidy + echo "warning: clang-tidy-18 not found; using '$tidy'. CI pins v18 — results may differ." >&2 + else + echo "error: no clang-tidy found. Install clang-tidy-18 (apt) or set CLANG_TIDY=..." >&2 + exit 127 + fi +fi + +build="${TIDY_BUILD:-build-tidy}" + +# A compile database is what clang-tidy needs; reuse it across runs unless it is +# missing or a reconfigure is forced. This does not touch your ./build dir. +if [ "${TIDY_RECONFIGURE:-0}" = "1" ] || [ ! -f "$build/compile_commands.json" ]; then + echo "== configuring compile database in $build/ (one-time; reuses cached deps) ==" + cmake -B "$build" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON >/dev/null +fi + +# File list: the given TUs, or — matching CI exactly — every project TU in the +# database with third_party/ and fetched deps (_deps) excluded. +if [ "$#" -gt 0 ]; then + files=("$@") +else + mapfile -t files < <(python3 -c " +import json +for e in json.load(open('$build/compile_commands.json')): + f = e['file'] + if 'third_party' not in f and '_deps' not in f: + print(f)") +fi + +echo "== clang-tidy ($tidy) over ${#files[@]} file(s) ==" +fail=0 +for f in "${files[@]}"; do + out="$("$tidy" -p "$build" "$f" 2>/dev/null || true)" + if printf '%s\n' "$out" | grep -qE "warning:|error:"; then + printf '%s\n' "$out" + fail=1 + fi +done + +if [ "$fail" -eq 0 ]; then + echo "clang-tidy clean." +else + echo "clang-tidy found violations (above); fix before pushing." >&2 + exit 1 +fi