# 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 capture-signal popup that edits it and the bake that renders it.

## 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.
- **Note length is musical-division-only.** Offsets carry the ms/beats duality; the note
  length does not. A free-duration note length would make two records describe the same
  performance at one tempo and different performances at another.
- **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 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.
