fix: harden note-program model against corrupt tempo/division/denomination records

Reject subnormal BPM that overflows to NaN, normalize Division equality, pin a single out-of-range-denomination interpretation across all readers, flag collapsed capture windows, add offset off-view editors and structural static_asserts, collapse duplicated CLAUDE.md facts.
This commit is contained in:
2026-07-30 20:11:06 -04:00
parent 834a6ddcc7
commit d923b352ae
10 changed files with 175 additions and 36 deletions
+17 -3
View File
@@ -1,12 +1,12 @@
// note_program — the programmed capture signal: one note length, one velocity, two anchored
// offsets, and the one resolver a preview and a bake must share.
//
// Resolved times are rate-free SECONDS relative to note-on; the caller converts against the
// live sample rate.
// Resolved times are rate-free seconds (this directory's CLAUDE.md: the standing ruling).
#pragma once
#include <cstdint>
#include <type_traits>
#include "core/instrument/note/musical_division.h"
#include "core/instrument/note/tempo.h"
@@ -52,6 +52,12 @@ double offsetSeconds(OffsetAmount amount, Tempo tempo);
// The unit toggle: the same instant restated in the other denomination.
OffsetAmount redenominate(OffsetAmount amount, Denomination to, Tempo tempo);
// Edit the magnitude via its non-stored view without changing which denomination is stored
// — a popup's ms and beats fields both stay live no matter which one the offset was entered
// in; only `redenominate` changes the stored denomination itself.
OffsetAmount withMsView(OffsetAmount amount, double ms, Tempo tempo);
OffsetAmount withBeatsView(OffsetAmount amount, double beats, Tempo tempo);
// Two types rather than one carrying an anchor field: the anchor is then unswappable at
// compile time. Sign is uniform — positive is later in time — so a capture that opens before
// the note is a negative start offset, and a negative end offset truncates before release.
@@ -75,6 +81,11 @@ private:
OffsetAmount amount_{};
};
static_assert(!std::is_constructible_v<StartOffset, EndOffset>,
"StartOffset and EndOffset must not be interchangeable at compile time");
static_assert(!std::is_convertible_v<OffsetAmount, StartOffset>,
"the anchor constructor must stay explicit");
struct NoteProgram {
Division length{};
StartOffset start{};
@@ -89,7 +100,10 @@ struct ResolvedNote {
double noteOffSeconds = 0.0; // == the note's sounding length, note-on being 0
double captureStartSeconds = 0.0; // negative when the capture opens before the note
double captureEndSeconds = 0.0;
std::uint8_t velocity = 1;
std::uint8_t velocity = 100; // resolveNote always overwrites this; matches Velocity's own default
// True when the programmed end offset inverted the window and resolveNote collapsed it
// to zero length instead — lets a popup explain an empty window rather than just show one.
bool windowCollapsed = false;
double captureLengthSeconds() const { return captureEndSeconds - captureStartSeconds; }
};