docs: record Daniel's static-captures ruling on the wavetable direction

R1 (2026-08-03) closes F5 — no frame-indexed wavetables yet — deferring W1 with
"yet" preserved as scope rather than rejection, and confirming W3 out. Near-term
scope narrows to W0 alone. Cascades the consequences: F1's threshold amended
(static does not mean one-cycle, so detection classifies nothing now), F2
simplified, F3 and F7 firmed, F8 narrowed to a binary. F6/band-limiting left
open and explicitly not pre-ruled. PLAN.md item 4 updated to match.
This commit is contained in:
2026-08-03 15:21:03 -04:00
parent 59837abd00
commit d5280687f3
2 changed files with 253 additions and 76 deletions
+19 -7
View File
@@ -220,13 +220,25 @@ in Γ-W4-T1 and changes no wave boundary.
*"supporting smoothness at this micro scale would turn this into a wavetable synth, which
is desirable."* The product framing is `docs/product/single-cycle-and-wavetable.md`.
**It carries EIGHT open [Daniel]-class forks and none is ruled**, so it gets a pointer
here rather than a phase section: `PLAN.md` is the active on-deck specification list, and
a phase with eight unruled forks is neither briefable nor consistent with the standing
claim above that **Λ is the only phase in this plan with unanswered [Daniel]-class
questions**. That claim is unaffected by this item — there is no phase here to except.
When the forks are ruled, the doc becomes a phase's backing product doc in the ordinary
way (the Γ / Ε / Ρ / Λ pattern, not the doc-less Ψ / Ω one).
**It framed EIGHT [Daniel]-class forks. ONE is now ruled and SEVEN remain open**, so it
still gets a pointer here rather than a phase section: `PLAN.md` is the active on-deck
specification list, and a phase whose specification rests on seven unruled forks is
neither briefable nor consistent with the standing claim above that **Λ is the only phase
in this plan with unanswered [Daniel]-class questions**. That claim is unaffected by this
item — there is no phase here to except. When the remaining forks are ruled, the doc
becomes a phase's backing product doc in the ordinary way (the Γ / Ε / Ρ / Λ pattern, not
the doc-less Ψ / Ω one).
**The ruling — Daniel, 2026-08-03:** *"I don't want actual frame-indexed wave tables yet,
that is out of scope. just static captures right now."* It closes fork F5 (the
wavetable-position knob does not ship now), confirms the multi-frame wavetable kind out by
ruling rather than only by the doc's argument, and **narrows the near-term scope to the
doc's W0 — loop-grid recalibration for micro material, which is defect-class work with no
new parameter, no format change and no `process()` involvement.** *"Yet"* is recorded as a
scope ruling, not a rejection; the doc keeps the deferred case whole and names the single
trigger that would revive it. **The ruling does NOT settle band-limiting** — the question
that decides it is still outstanding with Daniel, and nothing in the doc pre-rules it. Per
the doc's §7.1.
**Two things in it are decision-grade for work already in this plan, which is why it is
flagged rather than merely filed:**
+234 -69
View File
@@ -15,11 +15,28 @@ snap-radius track already in flight and is NOT this doc's subject.** It is descr
only where the reasoning needs it, because the defect and the direction share a diagnosis
and the fix for one does not deliver the other.
Status: **framed by product-designer, 2026-08-03. NOTHING here is ruled.** Eight
[Daniel]-class forks are open (§7). **No `PLAN.md` phase exists for this work and none is
proposed until the forks are ruled** — `PLAN.md`'s "Flagged for awareness" item 4 is the
only pointer to this doc, deliberately, so the plan's standing claim about unanswered
questions is not weakened by a direction that has not been decided.
Status: **framed by product-designer 2026-08-03; ONE ruling has since landed against it —
R1, recorded in full at §7.1.** Daniel, 2026-08-03:
> *"I don't want actual frame-indexed wave tables yet, that is out of scope. just static
> captures right now."*
R1 **closes F5** (W1, the wavetable-position knob, does not ship now) and **confirms W3 out
from Daniel** rather than only from this doc's argument. **Seven forks remain open** (§7).
**R1 does NOT rule F6 / band-limiting.** The question that decides F6 — *is the material ever
anything but sine?* — is still outstanding, Daniel knows it is outstanding, and it is
deliberately not pre-ruled anywhere below. Nothing in R1 touches aliasing: a *static*
single-cycle capture of anything but a sine, played across the keyboard, is exactly the case
§1.8 describes.
**The near-term scope is therefore W0, and only W0.** Read §2 as one body of work plus two
deferrals, not as four equal candidates.
**No `PLAN.md` phase exists for this work and none is proposed until F8 is ruled** —
`PLAN.md`'s "Flagged for awareness" item 4 is the only pointer to this doc, deliberately, so
the plan's standing claim about unanswered questions is not weakened by a direction whose
shape is decided but whose dispatch is not.
Everything below is a product-designer call with its reasoning stated. Contradict it in
review with an argument, not a preference.
@@ -42,15 +59,24 @@ anything but a pure sine, played at C5, resamples by ~17× and folds every parti
~1.4 kHz. Daniel's test case is a sine, which is exactly the one waveform that hides this.
Real wavetable synths solve it with a mip pyramid of per-octave lowpassed tables. That is a
real build with a real migration cost, and it is separable from everything else here.
**After R1 it is also the only one of the four shapes whose scope is still undecided.**
**Recommended shape, four candidates, in order:**
**The four shapes, and where each now stands:**
| | What | Cost | Lean |
| | What | Cost | Standing |
|---|---|---|---|
| **W0** | Recalibrate the loop grid for micro material — period snap, whole-file default, tune-from-loop, Preserve inert | No format change, no bank field, no hot-path change, migration-clean | **Take** |
| **W1** | Loop start becomes a **wavetable position** — scrub the loop through a multi-cycle capture in whole periods | One parameter that re-expresses one that exists; reuses `period_detect` unchanged | **Take next** |
| **W2** | **Band-limited playback** — mip pyramid + a better interpolator | Real DSP build; changes the sound of existing captures unless made conditional | **Its own decision** |
| **W3** | A first-class multi-frame **wavetable kind** — N×2048 tables, morph parameter, `.wav` wavetable import | Breaks "one loaded capture, one parameter set" | **Out** |
| **W0** | Recalibrate the loop grid for micro material — period snap, whole-file default, tune-from-loop, Preserve inert | No format change, no bank field, no hot-path change, migration-clean | **THE NEAR-TERM SCOPE** |
| **W1** | Loop start becomes a **wavetable position** — scrub the loop through a multi-cycle capture in whole periods | One parameter that re-expresses one that exists; reuses `period_detect` unchanged | **DEFERRED — R1, 2026-08-03** |
| **W2** | **Band-limited playback** — mip pyramid + a better interpolator | Real DSP build; changes the sound of existing captures unless made conditional | **OPEN — F6, undecided** |
| **W3** | A first-class multi-frame **wavetable kind** — N×2048 tables, morph parameter, `.wav` wavetable import | Breaks "one loaded capture, one parameter set" | **OUT — confirmed by R1** |
**R1 is a scope ruling, not a rejection of the idea.** *"Yet"* is Daniel's word and it is
load-bearing: W1's §2 section is preserved in full, argument intact, so a future reader can
pick it back up rather than re-derive it. What would revive it is named there.
**W0 stands entirely on its own merits and always did.** It is defect-class work — Daniel
cannot currently set a correct loop on his own routine material — and none of its five parts
depends on a wavetable ambition. R1 does not shrink W0; it removes what sat on top of it.
**And the operational headline: this direction lands in the exact files Ω-W1-T5
`overlay-mapping` owns right now**, and Phase Ω's own acceptance criteria name the
@@ -179,9 +205,25 @@ was the snap forcing `L` to half the file.
`defaultLoopBounds` puts the handles in the last quarter (`loop_span.h:89-96`) — the right
answer for a sustaining instrument sample and meaningless for a single cycle, where the last
quarter is a quarter of a waveform. Combined with §1.1 this is the mechanical cause of
*"loop start snaps to the middle."* At micro scale the correct default is the whole file;
for a multi-cycle capture under W1 it is one period at the loop's own start. Fork **F1**
decides whether the default is allowed to be material-aware at all.
*"loop start snaps to the middle."* At micro scale the correct default is the whole file.
Fork **F1** decides whether the default is allowed to be material-aware at all.
**A distinction R1 forces, and it is easy to get wrong: *static* does not mean *one
cycle*.** R1 says the captures are static — the waveform does not evolve — and that is what
kills W1, because a position knob scrubbing across identical cycles is a knob that does
nothing. It does **not** say every capture is exactly one period long. A static grab that
happens to run three or four cycles is still in scope, and on it:
- the loop **length** must still be a whole number of periods, or §1.3(a)'s seam
discontinuity returns exactly as Daniel heard it;
- the loop **position** is immaterial, because every cycle is the same cycle.
So the period grid keeps its job on multi-cycle material even with W1 deferred, and
`period_detect` keeps an in-scope consumer there (it also, separately and unaffected by any
of this, keeps its Preserve/PSOLA consumer — see §4). This is what amends F1's threshold in
§3: "detection failed" and "this is table-like material" stopped being the same test the
moment W1 left, because the static multi-cycle capture is now the case that would fall
through it.
### 1.5 `period_detect` structurally cannot see a single cycle — the period must be DECLARED
@@ -272,7 +314,9 @@ default. Fork **F6**.
The brief's own framing is the right one: this is a question of what *changes*, not what
gets built new. Single-cycle captures already flow through capture → bank → instrument
today. Four shapes, in ascending commitment.
today. Four shapes, in ascending commitment. **R1 settled two of them — W1 deferred, W3
confirmed out — so what follows is one body of work (W0), one open question (W2), and two
sections kept for the record.**
### W0 — Recalibrate the loop grid. No new concept.
@@ -293,7 +337,24 @@ migration-clean** — a snap grid applies during a drag, so no saved instance's
points move and no saved instance's sound changes. This is defect-class work that makes
Daniel's material usable at all, and it is the recommended first move.
### W1 — The loop start becomes a wavetable POSITION
### W1 — The loop start becomes a wavetable POSITION — DEFERRED by R1 (2026-08-03)
**Daniel, 2026-08-03: *"I don't want actual frame-indexed wave tables yet, that is out of
scope. just static captures right now."* A loop start expressed as `k·P` IS a frame index
into a table, so this is W1 by name.** The deferral is sound on this doc's own §8 test
rather than only on the ruling: W1 is worth doing *if the captures are ever multi-cycle and
evolving*, and static material makes every frame of the table the same frame. A scrub across
identical cycles is inaudible. **The knob would have been dead weight, exactly as §8
predicted.**
**"Yet" is the operative word — this is a scope ruling, not a verdict on the idea.** The
argument below is kept whole, and unchanged, because nothing in it was refuted; only its
premise went away. **What would revive it: capture material that both runs to several cycles
AND evolves across them** — a filter sweep, a bowed attack, a slowly-detuning pair. That is a
change in Daniel's practice, not a change in the code, and it is the only trigger. If it
arrives, re-open F5 and read from here; do not re-derive.
The rest of this section is the preserved case.
The cheap, high-leverage feature, and the one that earns the word "wavetable."
@@ -319,7 +380,12 @@ The optional second half — **interpolating between adjacent period-frames** fo
rather than a step — costs a second read tap on the per-sample path and must be checked
against the `process()` guardrail before it is promised. Fork **F5** splits scrub from morph.
### W2 — Band-limited playback
### W2 — Band-limited playback — UNAFFECTED by R1, still open
**R1 does not touch this and must not be read as touching it.** Band-limiting is a property
of playing *any* short table across the keyboard; whether the table's frames evolve is
irrelevant to whether its partials fold. A static single-cycle capture of a saw, played at
C5, aliases exactly as §1.8 describes. **F6 remains open and unruled.**
§1.8. Orthogonal to W0 and W1, and the only one of the four that is a genuine DSP build. It
is also **independently valuable outside this direction**: it improves every ordinary sample
@@ -327,10 +393,13 @@ played more than an octave above its root, which is a much larger population tha
single-cycle captures. That argues for judging it on its own merits rather than as
wavetable-support. Fork **F6**.
### W3 — A first-class wavetable KIND — RECOMMENDED OUT
### W3 — A first-class wavetable KIND — OUT (recommended out; CONFIRMED by R1)
A bank entry that is N frames of a fixed size, a morph parameter across frames, and import
of third-party wavetable `.wav` files. Recommended out, with force:
of third-party wavetable `.wav` files. **This is "actual frame-indexed wave tables" in the
most literal sense, and R1 names it out of scope directly** — so it is now out by Daniel's
ruling and not merely by this doc's recommendation. The recommendation stood on its own and
is kept because the structural reason is the durable one:
- **It breaks a settled invariant.** *"The instrument holds ONE loaded capture and ONE set of
playback parameters"* (`src/core/instrument/CLAUDE.md`) is the whole result of the zone
@@ -383,6 +452,26 @@ outside it, which is the discrimination the band buys. A further tightening —
first and last frames to nearly match — is available if false positives show up in practice
and is deliberately not proposed pre-emptively.
**R1 amends condition 1, and this is a genuine correction rather than a restatement.**
Condition 1 used *detection failure* as the classifier because in the original framing
detection **succeeding** meant W1 territory, and W1 supplied its own default (one period at
the loop's start). With W1 deferred there is nothing downstream to catch that case, so a
static multi-cycle capture (§1.4) — detection succeeds, file is short, cycles are
interchangeable — falls straight through the threshold to the unimproved last-quarter
default. That is precisely the defect W0 exists to remove. The threshold should now read:
- **the frame count lies inside the band** `sr/kPeriodDetectMaxHz … sr/kPeriodDetectMinHz`,
i.e. 24 … 3200 frames at 48 kHz — this alone now carries the classification; **and**
- **a period is known**: **detected** where `detectPeriod` returns one, **declared as the
file length** where it does not (§1.5).
**Detection moves from being the classifier to being the period *source*, which is what it
was always better suited to be.** The false positive the band admits is unchanged (a very
short non-periodic one-shot), and so is the tightening available against it. **F1's lean is
unmoved — still (a), derived-only, governing defaults and the snap grid and never the
sound** — only the test inside (a) changes. F1(c), no classification at all, is untouched by
R1 and remains the serious conservative alternative.
**The named alternative, and it deserves a real hearing: no classification at all.** Make the
period grid always available, let "the period is the whole file" be simply what the user gets
when the loop spans the whole file, and accept a bad initial loop default on micro material.
@@ -409,18 +498,23 @@ conservative option on the table and it is fork **F1(c)**.
inert at one period, the detector's answer is simply unused on that path — no code changes.
**The one exception (§1.5):** the single-cycle case is permanently outside detection's reach,
so W0's period is declared from the file/loop length while W1's is detected. Two sources, one
grid.
so at the single-cycle limit the period is **declared** from the file/loop length, while on a
capture running to several cycles it is **detected**. Two sources, one grid. **R1 does not
collapse them into one** — deferring W1 removes the *scrub*, not the multi-cycle capture
(§1.4), so both arms of the split still have in-scope work to do.
**A genuine tension worth naming before someone "fixes" it into a bug.** `periodAnalysisSpan`
uses `[loopStart, loopEnd)` only when that span is at least one full probe block
(`2 · longestLagFrames`, 6400 frames at 48 kHz). Under W1 the loop is one period — always far
below that — so the detector will always fall back to analysing the whole source. **That is
correct**: you want the period of the source material, not the period of a one-period loop,
which would be circular. But once W1 ships, the detector's answer and the loop's length are
measuring deliberately different things, and the code will look like it has an inconsistency
it does not have. It should be commented at the seam, per the comment conventions' "warnings
against a plausible-but-wrong change."
(`2 · longestLagFrames`, 6400 frames at 48 kHz). **Any one-period loop on micro material is
far below that**which under W0 is the *default* case, not an exotic one — so the detector
will always fall back to analysing the whole source. **That is correct**: you want the period
of the source material, not the period of a one-period loop, which would be circular. But the
detector's answer and the loop's length are then measuring deliberately different things, and
the code will look like it has an inconsistency it does not have. It should be commented at
the seam, per the comment conventions' "warnings against a plausible-but-wrong change."
**R1 does not defer this.** The paragraph originally attached the circularity to W1; it in
fact attaches to any one-period loop, so it arrives with W0 rather than after it.
---
@@ -428,12 +522,15 @@ against a plausible-but-wrong change."
- **PPG Wave / Waldorf Microwave** — the wavetable-position knob as "which cycle of the stored
table." W1 is this control, transplanted onto captured material rather than authored tables.
**Recorded for whoever picks W1 back up; deferred by R1, and note that the PPG precedent
presumes a table whose frames differ, which static capture material does not supply.**
- **Serum / Massive / Vital** — mip-mapped, per-octave band-limited tables. This is the whole
of W2, and their universal adoption of it is the evidence that §1.8 is not a theoretical
concern.
concern. **Still live: F6 is open.**
- **Kontakt / EXS-class samplers' loop-finding tools** — snap a loop to a *detected period* and
offer a "find loop" pass, rather than snapping only to zero crossings. This is the
established precedent for F2's period grid; we would not be inventing it.
established precedent for F2's period grid; we would not be inventing it. **This is now the
load-bearing precedent, since W0 is the near-term scope.**
- **Serum's import-a-sample-as-a-wavetable flow** — it asks the user to declare or confirm the
cycle length rather than detecting it from a single cycle. That is independent confirmation
of §1.5's declare-vs-detect split, arrived at from the same constraint.
@@ -473,22 +570,29 @@ Stated in §2 and the sole structural reason W3 is recommended out.
move and no saved instance changes sound. A changed *default* only affects a capture that
has no stored loop.
- **W1 is migration-clean** if the position parameter defaults to the position the stored loop
start already denotes.
start already denotes. *(Deferred by R1; kept for the record.)*
- **W2 is not.** Both halves change the rendered audio of existing captures. Fork **F6**
exists to decide whether that cost is paid unconditionally or made conditional and declared.
**After R1 the whole near-term scope is migration-clean**, and the only migration risk left
in this doc sits behind an open fork rather than inside the work.
### 6.6 The hot path — HELD, and the guardrails are satisfiable by construction
- The period grid, the classification and the tune-from-loop computation are **load-time and
UI-thread only**.
- W1's scrub is a **reload-tier or note-on-latched** parameter, not a per-sample one; if it is
wanted live, it belongs in the existing three-tier commit classification
(`deckParamCommit` / `liveCommitFor`) rather than in a new mechanism.
(`deckParamCommit` / `liveCommitFor`) rather than in a new mechanism. *(Deferred by R1.)*
- W2's mip level selection is **per note-on**. The interpolator change is a straight-line
4-tap with no dispatch.
- **W1's optional morph (F5c) is the one item that would touch `process()`** — a second read
tap per frame. It must be measured against the guardrail before it is promised, and "drop
it" is a legitimate outcome.
- **W1's optional morph (F5c) was the one item that would have touched `process()`** — a
second read tap per frame, to be measured against the guardrail before it was promised.
**R1 closes F5 at (a), so that item is off the table for now.**
**Consequence of R1 worth stating plainly: the entire near-term scope is load-time and
UI-thread only. Nothing in W0 goes near `process()`.** The guardrail question that would have
needed a measurement before anything could be promised no longer gates this direction.
### 6.7 Phase Ω and the in-flight snap-radius track — the real sequencing constraint
@@ -517,38 +621,89 @@ one annotated exception from Ω-W1-T4, so a second annotation is cheap precedent
freeze the moment it ships**, so a position knob's scale must be right first time. W0 adds no
parameter at all, which is one more reason to take it first.
**R1 removes the near-term exposure here entirely.** With W1 deferred, the only prospective
new parameter left in this doc is W2's mode, which sits behind an open fork — so **nothing in
the near-term scope opens a one-way door.** A position knob's taper, the item that most
needed to be right first time, is a decision this direction no longer has to make under time
pressure.
### 6.9 The editor's space budget
W1's position control and W2's mode toggle both want deck real estate against
`instrument-control-surface.md` §1.6's 82 px headroom, and `reasampler_editor.h` is already
tight against the ~600-line ceiling with Phase Ω spending its margin. This is a real
constraint on how many of these shapes can ship together, not a footnote.
tight against the ~600-line ceiling with Phase Ω spending its margin. This was a real
constraint on how many of these shapes could ship together, not a footnote.
**R1 relieves it for now.** W0's five parts add no control to the deck — a snap grid, a
default, and two greyed affordances are all changes to existing surfaces — and F3's
"Tune to loop" is the one item that might want a hit target, which is a far smaller ask than
a knob. **The 82 px is uncontended by the near-term scope.** It comes back the moment F6 is
ruled toward a mode toggle, so the headroom should not be spent elsewhere on the assumption
that this direction has released it permanently.
---
## 7. Forks — EIGHT, all OPEN, none ruled
## 7. Forks — EIGHT total: ONE settled (F5), SEVEN open
Each carries a product-designer lean and the cost of taking it. **Every one is Daniel's to
rule.** None is pre-ruled here and none should be treated as settled by the lean.
rule.** None is pre-ruled here and none should be treated as settled by the lean. Rulings
recorded against this doc are logged in §7.1; the lean column below states, per fork, whether
R1 moved it.
| Fork | Question | Options | Lean |
| Fork | Question | Options | Standing / lean |
|---|---|---|---|
| **F1** | Is there a "micro" classification at all, and where does it come from? | (a) derived-only, governing **defaults and snap grid only, never the sound**; (b) derived + a user override toggle; (c) **no classification** — the period grid is always available and a bad default on micro material is accepted | **(a)**, with (c) as the serious conservative alternative |
| **F2** | What grid does a loop-mark drag snap to? | (a) zero-crossing always (today); (b) **period** when a period is known, zero-crossing otherwise; (c) both, modifier-selected — note `param_taper`'s modifier law explicitly excludes spline points, so a waveform-mark modifier is available but needs its own ruling | **(b)** — it makes the octave error unreachable rather than unlikely |
| **F3** | Does the instrument correct tuning from the loop length? | (a) never — the user dials Pitch (today); (b) a **one-shot "Tune to loop"** that writes `pitchOffsetSemitones`; (c) automatic and continuous, derived and unstored | **(b)** — honest, visible, and uses a parameter that already exists. (c) is a hidden pitch modifier |
| **F4** | Fractional loop bounds? | (a) **no** — integer forever; buy resolution by capturing low, and document that; (b) yes — fractional loop end, which changes a **bank intrinsic** on `Sample` and takes a payload rung | **(a)**, hard. ~1 ¢ at Daniel's own working range does not justify a bank-format change |
| **F5** | Does W1 ship, and does it morph? | (a) not now; (b) **scrub only** — whole periods, no interpolation; (c) scrub + interpolate between adjacent period-frames **a second read tap on `process()`; measure before promising** | **(b)** if it ships |
| **F6** | Band-limiting (W2) — in scope, and conditional? | (a) out for now; (b) **mip pyramid + better interpolator, gated behind a mode** so the migration bar holds; (c) unconditional, accept the break with a version note | **(b)** if it ships. This is the fork that decides whether "wavetable synth" is rhetoric or a commitment |
| **F7** | Preserve, and the XFADE mark, at one-period loops | (a) leave both live and let the user discover they do nothing; (b) **refuse/grey both** when the loop is one period; (c) auto-switch the engine to Varispeed | **(b)**. (c) changes the sound of a saved instance and should be rejected |
| **F8** | Does this become a Phase, and when? | (a) a phase sized **W0-only** first, dispatched after Ω-W1-T5 and the snap-radius track land; (b) one phase covering W0+W1; (c) not now | **(a)** — W0 is defect-class and unblocks Daniel's material; W1 is a feature that can be scoped once W0's grid exists |
| **F1** | Is there a "micro" classification at all, and where does it come from? | (a) derived-only, governing **defaults and snap grid only, never the sound**; (b) derived + a user override toggle; (c) **no classification** — the period grid is always available and a bad default on micro material is accepted | **OPEN. Lean unmoved: (a)**, with (c) as the serious conservative alternative. **But R1 amends the test inside (a)** — the frame-count band now classifies and detection supplies the period, because "detection failed" stopped meaning "table-like material" when W1 left. See §3 |
| **F2** | What grid does a loop-mark drag snap to? | (a) zero-crossing always (today); (b) **period** when a period is known, zero-crossing otherwise; (c) both, modifier-selected — note `param_taper`'s modifier law explicitly excludes spline points, so a waveform-mark modifier is available but needs its own ruling | **OPEN. Lean unmoved: (b)** — it makes the octave error unreachable rather than unlikely. **Simplified by R1:** on a true one-cycle file the period grid admits exactly ONE loop (start 0, whole file), so "grid" there means "a single admissible loop"; the grid only has multiple positions on a multi-cycle capture, which R1 leaves in scope (§1.4) |
| **F3** | Does the instrument correct tuning from the loop length? | (a) never — the user dials Pitch (today); (b) a **one-shot "Tune to loop"** that writes `pitchOffsetSemitones`; (c) automatic and continuous, derived and unstored | **OPEN. Lean (b) FIRMS.** (c)'s only real motivation was keeping tuning correct while a scrub moved the loop; R1 removes the scrub, so with a static loop length a one-shot write is unambiguous and (c) is now pure hidden-pitch-modifier with nothing bought |
| **F4** | Fractional loop bounds? | (a) **no** — integer forever; buy resolution by capturing low, and document that; (b) yes — fractional loop end, which changes a **bank intrinsic** on `Sample` and takes a payload rung | **OPEN. Lean unmoved: (a)**, hard. ~1 ¢ at Daniel's own working range does not justify a bank-format change. R1 does not touch this: the quantizer is one frame regardless of what the material does |
| **F5** | Does W1 ship, and does it morph? | (a) not now; (b) scrub only — whole periods, no interpolation; (c) scrub + interpolate between adjacent period-frames | **SETTLED (a), Daniel 2026-08-03 (R1).** This inverts the lean, which was (b) — the lean assumed multi-cycle *evolving* material, and §8's own test said so. "Yet": revisit if the material changes, per §2's W1 section |
| **F6** | Band-limiting (W2) — in scope, and conditional? | (a) out for now; (b) **mip pyramid + better interpolator, gated behind a mode** so the migration bar holds; (c) unconditional, accept the break with a version note | **OPEN — explicitly NOT ruled by R1, and now the largest open question here.** Lean unchanged: **(b)** if it ships. This is the fork that decides whether "wavetable synth" is rhetoric or a commitment |
| **F7** | Preserve, and the XFADE mark, at one-period loops | (a) leave both live and let the user discover they do nothing; (b) **refuse/grey both** when the loop is one period; (c) auto-switch the engine to Varispeed | **OPEN. Lean (b) FIRMS and simplifies.** R1 makes the one-period loop the *typical* shape rather than one of several, so the affordance is wrong more of the time; and with a static loop the condition is evaluated once rather than tracked across a moving one. (c) still changes the sound of a saved instance and should still be rejected |
| **F8** | Does this become a Phase, and when? | (a) a phase sized **W0-only**, dispatched after Ω-W1-T5 and the snap-radius track land; ~~(b) one phase covering W0+W1~~**eliminated by R1**; (c) not now | **OPEN, but narrowed to a binary (a) vs (c).** Lean unmoved: **(a)** — W0 is defect-class and unblocks Daniel's material. R1 makes (a) cleaner than it was: a W0-only phase is now the whole of the direction's near-term scope rather than the first slice of it |
**What is NOT a fork, and should not be re-opened as one:**
- The pre-seam crossfade cannot deliver micro-scale smoothness (§1.2). Structural.
- `period_detect` cannot see a single cycle (§1.5). Structural.
- W3 breaks "one capture, one parameter set" (§2). If it returns, it returns as its own
product question.
- Sub-50 Hz is first-class. Nothing here narrows a range to avoid it.
- The pre-seam crossfade cannot deliver micro-scale smoothness (§1.2). Structural — and
**untouched by R1**, which changes nothing about the loop's arithmetic.
- `period_detect` cannot see a single cycle (§1.5). Structural — and **untouched by R1**. The
declare-vs-detect split survives intact; deferring W1 removes the scrub, not the
multi-cycle capture that detection serves (§1.4).
- W3 breaks "one capture, one parameter set" (§2), **and is now out by Daniel's ruling as
well as by that argument.** If it returns, it returns as its own product question.
- Sub-50 Hz is first-class. Nothing here narrows a range to avoid it, and R1 does not change
that: capturing low remains the correct practice for the reason in §1.3, not a tolerance
the design absorbs.
- **W1's deferral is a scope ruling, not a structural result** — the one item in this list
that IS revisitable, on the single named trigger in §2 (material that is both multi-cycle
and evolving).
### 7.1 Rulings recorded
**R1 — Daniel, 2026-08-03.** Verbatim:
> *"I don't want actual frame-indexed wave tables yet, that is out of scope. just static
> captures right now."*
**What it settles:**
- **F5 → (a), not now.** A loop start expressed as `k·P` is a frame index into a table; on
static material every frame of that table is the same frame, so the knob scrubs between
identical cycles. This is §8's own kill test, met.
- **W3 confirmed out**, now by ruling and not only by this doc's argument.
- **W1 deferred, with "yet" recorded as Daniel's word.** Scope, not rejection. §2 keeps the
case whole and names the single trigger that revives it.
- **F8's option (b) eliminated** — there is no W0+W1 phase to size.
**What it explicitly does NOT settle:**
- **F6 / band-limiting.** The deciding question — *is the material ever anything but sine?*
is outstanding, and Daniel knows it is. Aliasing is orthogonal to whether a table's frames
evolve; a static single-cycle saw played at C5 folds exactly as §1.8 says. **Do not read
R1 as answering it.**
- **F1, F2, F3, F4, F7, F8** — all still Daniel's. R1 shifts what some of them are choosing
between (the table above says which) without choosing.
- **The Phase Ω conflict in §6.7.** Still open, still with Daniel and whoever owns Ω's
acceptance criteria, and not this doc's to resolve.
---
@@ -556,17 +711,21 @@ rule.** None is pre-ruled here and none should be treated as settled by the lean
Stated plainly so the direction can be killed cheaply if it is not:
- **W0 is worth doing regardless of the wavetable ambition.** Daniel cannot currently set a
correct loop on his own routine material, and the radius fix alone leaves a one-pixel drag
able to produce a 0.997-period loop. That is defect-class.
- **W1 is worth doing if Daniel's captures are ever multi-cycle and evolving.** If every
single-cycle capture is a static one-cycle grab, the position knob has nothing to scrub and
W1 is dead weight. **This is the one question this doc cannot answer from the code, and it
is the thing most worth asking before F5 is ruled.**
- **W2 is worth doing if the material is ever anything but sine.** A sine played up four
octaves is fine; a captured saw is not. If Daniel's practice is harmonically rich
single-cycle captures played across the keyboard, W2 stops being optional and becomes the
actual answer to "turn this into a wavetable synth."
- **W0 is worth doing regardless of the wavetable ambition — and R1 leaves that argument
exactly as it was.** Daniel cannot currently set a correct loop on his own routine material,
and the radius fix alone leaves a one-pixel drag able to produce a 0.997-period loop. That
is defect-class. **It is now also the whole of the near-term scope.**
- **W1 is worth doing if Daniel's captures are ever multi-cycle and evolving. ANSWERED
2026-08-03: they are not — *"just static captures right now."*** The position knob has
nothing to scrub, and the test written here before the answer arrived is the reason F5
closes cleanly rather than by preference. The condition is unchanged and still live as a
trigger: if the practice changes, this bullet is what re-opens F5.
- **W2 is worth doing if the material is ever anything but sine. STILL UNANSWERED — this is
the outstanding question.** A sine played up four octaves is fine; a captured saw is not. If
Daniel's practice is harmonically rich single-cycle captures played across the keyboard, W2
stops being optional and becomes the actual answer to "turn this into a wavetable synth."
**R1 does not bear on it either way**, because staticness and harmonic content are
independent properties of the same capture.
---
@@ -577,9 +736,15 @@ product-designer may not edit `CLAUDE.md`. **Flagged for staff-engineer or doc-k
`single-cycle-and-wavetable.md` to that list.**
`docs/PLAN.md` carries one pointer to this doc, as item 4 under "Flagged for awareness — not
blocking, but decision-grade." **No phase, no wave and no track is proposed there**, because
every fork above is open and `PLAN.md` is the *active on-deck specification list*. Adding a
phase with eight unruled forks would both put an unbriefable phase in that list and falsify
blocking, but decision-grade." **No phase, no wave and no track is proposed there**, and R1
does not change that — `PLAN.md` is the *active on-deck specification list*, and putting a
phase in it while F8 is unruled would both put an unbriefable phase in that list and falsify
the plan's standing claim that Λ is the only phase carrying unanswered [Daniel]-class
questions. When the forks are ruled, this doc becomes the backing product doc for a phase in
the ordinary way — the Γ / Ε / Ρ / Λ pattern, not the doc-less Ψ / Ω one.
questions.
**What R1 does change is that the proposable phase is now well-defined: W0-only, F8(a).**
Five forks still sit inside W0's own content (F1, F2, F3, F4, F7), F8 governs whether and
when it dispatches, and F6 belongs to W2 rather than to any of this — so the *shape* is
settled while the *specification* is not. When F8 is ruled, this doc becomes the backing
product doc for that phase in the ordinary way — the Γ / Ε / Ρ / Λ pattern, not the doc-less
Ψ / Ω one.