# src/core/instrument/note — the programmed capture signal

## Scope

The pure model of the note the sampler plays to itself when it resamples: how long it
sounds, how hard, and how far around it the capture window opens. A fourth peer of
`engine/` / `map/` / `ui/` under `core/instrument/`, and pure by the same rule — no REAPER
types, no VST3 types, no host at all.

It exists as its own directory because it is neither engine (it renders nothing), mapping
(it resolves no capture and builds no `SampleData`), nor UI (it computes no geometry). It
is a performance *description* plus its arithmetic, read by two consumers that must not
diverge: the bake that renders it, and any surface that comes to edit it. Today the bake
DERIVES the whole record from the dialed sound rather than asking for it — the one field a
derivation cannot supply is the Gate-over-a-loop note length, and that arrives as a single
`Division` (see `bake/CLAUDE.md`), not as a hand-programmed record. Everything else it
derives is an exact duration; see the note-length invariant below.

## Invariants

- **One record, one resolver.** `NoteProgram` is the single source of truth and
  `resolveNote` the single way to turn it into times. A preview that computes its own
  window, or a bake that does, is the exact divergence this module exists to prevent — the
  criterion is structural (one path), not "the numbers looked close."
- **The tempo comes in as a parameter.** The BPM in effect at the project cursor is read by
  the shell. Nothing here may reach for it, and no tempo is hardcoded anywhere in the
  directory — `Tempo` has no default and cannot be constructed without one.
- **Resolved times are rate-free seconds.** The standing ruling: no sample rate appears
  here; the caller converts seconds to frames against the live rate.
- **A note length is EITHER a musical division or an exact duration, and which one says who
  produced it.** *(Amends the earlier "musical-division-only" rule, which was settled when
  every length was hand-programmed through a picker.)* A PICKED length stays a `Division`: a
  picker's rungs are the point, and the tempo-relative reading — the same record meaning a
  different duration at a different tempo — is what the user asked for. A DERIVED length is
  exact seconds, because the ladder is finite: a source longer than its top rung has no rung
  that covers it, so quantizing up saturates and releases the note mid-sound, and on every
  shorter source it buys trailing silence for nothing. `NoteLength` holds one or the other and
  `noteLengthSeconds` resolves both, so no reader can pick the wrong denomination. Today the
  bake is the only producer of each: its window derivations are exact, and the Gate-over-a-loop
  Hold knob is the one picker.
- **A division persists as its `{quarterExponent, modifier}` pair, never as its picker
  index.** The index is presentation order and would silently re-map every saved record if
  the ladder ever gained a rung or a modifier.
- **An offset stores the denomination it was entered in** — see `OffsetAmount` in
  `note_program.h` for why.
- **Every value type establishes its domain at construction, so every field `resolveNote`
  returns is finite for every constructible program and tempo.** `Tempo::fromBpm` rejects,
  alone, because an unusable BPM has no nearest usable one to fall to. `Division`,
  `OffsetAmount`, and `Velocity` clamp, because an off-ladder rung, an unrepresentable
  magnitude, and an out-of-range velocity each do. Each has exactly one door (`makeDivision`,
  `offsetOf`, `Velocity::of`); `Division` and `OffsetAmount` block any other path with a
  private value constructor, `Velocity` with a private member that only `of()` writes —
  either way an out-of-domain value cannot be held, only passed in. That is what lets every
  reader branch without a fallback, equality compare fields raw, and `resolveNote` return
  finite times for every constructible input with no failure path and no validity flag.
- **The module will not tell a caller a record is junk, because a junk record cannot exist
  here.** Corruption is only visible where raw bytes are: a codec sees both the bytes it
  read and the value construction produced, and reporting the difference is the codec's job.
  Do not add a validity flag to `NoteProgram` or `ResolvedNote` to carry that signal upward
  — `windowCollapsed` describes a legal program, and is not the seed of an error channel.
- **Does not carry a MIDI note number.** `NoteProgram` describes timing and velocity only;
  render pitch is deferred to a later additive field (Ξ-W2) rather than assumed to live
  here.

## Modules

- `musical_division` — the note-length ladder: 1/64 through 64/1 (a rung is the base-2
  exponent of its length in quarter notes, -4..8), each straight, dotted (x3/2), or triplet
  (x2/3); the 39-entry picker order; and the `"1/8."` / `"1/4t"` label notation. Beats only
  — see `musical_division.h` for why it links no tempo.
- `tempo` — a validated project tempo plus every beats <-> seconds <-> ms conversion, and
  `kMaxConvertibleMagnitude`, the beats-or-ms ceiling the whole directory caps its domains
  to. `fromBpm` validates by running the extreme conversions rather than by testing the
  `60/bpm` reciprocal they start from — that reciprocal stays finite well past the point the
  multiply after it overflows.
- `note_program` — `Velocity` (clamped 1..127), the two-denomination `NoteLength` and its
  resolver, the denominated `OffsetAmount` and its unit toggle, the anchored `StartOffset` /
  `EndOffset`, the `NoteProgram` record, and `resolveNote`.

## Gotchas

- **A beat is a quarter note** — see `tempo.h` for why. A beats *readout* that should track
  a compound meter's dotted-quarter pulse would need the time signature threaded in — it is
  not, deliberately. `src/core/ui/card_meta.cpp` is the sibling module that *does* fold
  `timeSigDenom` into its own seconds-per-beat — a different, also-correct convention for a
  different job; don't read the divergence as a bug in either.
- **`StartOffset` and `EndOffset` are distinct types on purpose.** They hold the same
  payload and differ only in what they anchor to (note-on and note-off respectively);
  collapsing them into one type with an anchor field makes the swap a runtime bug instead
  of a compile error.
- **Signs are uniform: positive is later in time.** So Daniel's "capture from 20 ms before
  note-on" is a *negative* start offset, and a negative end offset truncates before release.
  Both are legal; `resolveNote` only refuses to invert the window.
- **ms <-> beats round-trips are lossless to double precision, not bit-identical.** The
  conversion is a multiply/divide pair; compare with an epsilon.
- **`Division` and `OffsetAmount` are trivially copyable, so a `memcpy` of a wire record
  bypasses every door.** Decode field-by-field through `makeDivision`/`offsetOf` (the pattern
  `src/core/wire/bytes.h` already uses) instead — never `memcpy` raw bytes into either type.
- **Editing the ms field of a beats-stored offset stores beats, and the ms readout will then
  move with the tempo.** `withMsView` keeps the stored denomination on purpose, so typing 250
  into the ms field of a beats offset stores 0.5 beats at 120 BPM. That is the intended
  semantic, but it is a UI-visible surprise worth a word in the popup: `redenominate` — the
  unit toggle — is the only thing that changes which denomination is stored.
